Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 113 additions & 17 deletions nado_protocol/utils/margin_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ class CrossPositionMetrics(BaseModel):
symbol: str
position_size: Decimal
notional_value: Decimal
avg_entry_price: Optional[Decimal] # Average entry price (requires indexer data)
est_liq_price: Optional[Decimal] # Estimated liquidation price
est_pnl: Optional[Decimal] # Estimated PnL (requires indexer data)
unsettled: Decimal # Unsettled quote (v_quote_balance)
margin_used: Decimal
Expand Down Expand Up @@ -520,16 +522,18 @@ def calculate_cross_position_metrics(
# This represents the unrealized PnL
unsettled = self.calculate_perp_balance_value(balance)

# Calculate Est. PnL if indexer data is available
# Formula: (amount × oracle_price) - netEntryUnrealized
# where netEntryUnrealized excludes funding, fees, slippage
# Calculate metrics (requires indexer data for avg_entry_price and est_pnl)
avg_entry_price = self._calculate_avg_entry_price(balance)
est_liq_price = self._calculate_est_liq_price(balance)
est_pnl = self._calculate_est_pnl(balance)

return CrossPositionMetrics(
product_id=balance.product_id,
symbol=f"Product_{balance.product_id}",
position_size=balance.amount,
notional_value=notional,
avg_entry_price=avg_entry_price,
est_liq_price=est_liq_price,
est_pnl=est_pnl,
unsettled=unsettled,
margin_used=margin_used,
Expand All @@ -541,6 +545,24 @@ def calculate_cross_position_metrics(
short_weight_maintenance=balance.short_weight_maintenance,
)

def _get_indexer_event_for_product(self, product_id: int) -> Optional[IndexerEvent]:
"""
Get indexer event for a specific product (cross margin only).

Returns None if indexer data is not available or product not found.
"""
if not self.indexer_events or product_id == self.QUOTE_PRODUCT_ID:
return None

for event in self.indexer_events:
if event.product_id != product_id:
continue
if event.isolated:
continue
return event

return None

def _calculate_est_pnl(self, balance: BalanceWithProduct) -> Optional[Decimal]:
"""
Calculate estimated PnL if indexer snapshot is available.
Expand All @@ -549,26 +571,90 @@ def _calculate_est_pnl(self, balance: BalanceWithProduct) -> Optional[Decimal]:

Returns None if indexer data is not available.
"""
if not self.indexer_events or balance.product_id == self.QUOTE_PRODUCT_ID:
event = self._get_indexer_event_for_product(balance.product_id)
if event is None:
return None

for event in self.indexer_events:
if event.product_id != balance.product_id:
continue
if event.isolated:
continue
try:
net_entry_int = int(event.net_entry_unrealized)
except (TypeError, ValueError):
return None

try:
net_entry_int = int(event.net_entry_unrealized)
except (TypeError, ValueError):
continue
net_entry_unrealized = Decimal(net_entry_int) / Decimal(10**18)
current_value = balance.amount * balance.oracle_price
return current_value - net_entry_unrealized

def _calculate_avg_entry_price(
self, balance: BalanceWithProduct
) -> Optional[Decimal]:
"""
Calculate average entry price if indexer snapshot is available.

net_entry_unrealized = Decimal(net_entry_int) / Decimal(10**18)
Formula: abs(netEntryUnrealized / position_amount)

current_value = balance.amount * balance.oracle_price
return current_value - net_entry_unrealized
Returns None if indexer data is not available or position is zero.
"""
if balance.amount == 0:
return None

return None
event = self._get_indexer_event_for_product(balance.product_id)
if event is None:
return None

try:
net_entry_int = int(event.net_entry_unrealized)
except (TypeError, ValueError):
return None

net_entry_unrealized = Decimal(net_entry_int) / Decimal(10**18)
return abs(net_entry_unrealized / balance.amount)

def _calculate_est_liq_price(
self, balance: BalanceWithProduct
) -> Optional[Decimal]:
"""
Calculate estimated liquidation price.

Formula:
- If long: oracle_price - (maint_health / amount / long_weight)
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formula documentation uses inconsistent operator precedence notation. The division operations should be explicitly grouped with parentheses to match the implementation: oracle_price - ((maint_health / amount) / long_weight)

Copilot uses AI. Check for mistakes.
- If short: oracle_price + (maint_health / abs(amount) * short_weight)

Returns None if:
- Position is zero
- Long liq price <= 0 (prevents -infinity)
- Short liq price >= 10x oracle price (prevents +infinity)
"""
if balance.amount == 0:
return None

# Get maintenance health from subaccount info
maint_health = self._parse_health(self.subaccount_info.healths[1])

is_long = balance.amount > 0

if is_long:
# Long: oracle_price - (maint_health / amount / long_weight)
if balance.long_weight_maintenance == 0:
return None

liq_price = balance.oracle_price - (
maint_health / balance.amount / balance.long_weight_maintenance
)

# If liquidation price is 0 or less, return None (prevents -infinity)
if liq_price <= 0:
return None
else:
# Short: oracle_price + (maint_health / abs(amount) * short_weight)
liq_price = balance.oracle_price + (
maint_health / abs(balance.amount) * balance.short_weight_maintenance
)

# If liquidation price is 10x oracle price, return None (prevents +infinity)
if liq_price >= balance.oracle_price * 10:
return None

return liq_price

def calculate_isolated_position_metrics(
self, iso_pos: IsolatedPosition
Expand Down Expand Up @@ -783,6 +869,16 @@ def print_account_summary(summary: AccountSummary) -> None:
print(f"│ Position: {cross_pos.position_size:,.3f}")
print(f"│ Notional: ${cross_pos.notional_value:,.2f}")

if cross_pos.avg_entry_price is not None:
print(f"│ Avg Entry Price: ${cross_pos.avg_entry_price:,.2f}")
else:
print(f"│ Avg Entry Price: N/A")

if cross_pos.est_liq_price is not None:
print(f"│ Est. Liq Price: ${cross_pos.est_liq_price:,.2f}")
else:
print(f"│ Est. Liq Price: N/A")

if cross_pos.est_pnl is not None:
pnl_sign = "+" if cross_pos.est_pnl >= 0 else ""
print(f"│ Est. PnL: {pnl_sign}${cross_pos.est_pnl:,.2f}")
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "nado-protocol"
version = "0.2.8"
version = "0.3.0"
description = "Nado Protocol SDK"
authors = ["Jeury Mejia <jeury@inkfnd.com>"]
homepage = "https://nado.xyz"
Expand Down