nado-protocol 0.1.7__py3-none-any.whl → 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nado_protocol/indexer_client/query.py +23 -0
- nado_protocol/indexer_client/types/models.py +1 -1
- nado_protocol/indexer_client/types/query.py +35 -0
- nado_protocol/utils/backend.py +3 -3
- nado_protocol/utils/balance.py +249 -0
- nado_protocol/utils/margin_manager.py +841 -0
- {nado_protocol-0.1.7.dist-info → nado_protocol-0.2.2.dist-info}/METADATA +1 -1
- {nado_protocol-0.1.7.dist-info → nado_protocol-0.2.2.dist-info}/RECORD +10 -8
- {nado_protocol-0.1.7.dist-info → nado_protocol-0.2.2.dist-info}/entry_points.txt +1 -0
- {nado_protocol-0.1.7.dist-info → nado_protocol-0.2.2.dist-info}/WHEEL +0 -0
|
@@ -49,6 +49,8 @@ from nado_protocol.indexer_client.types.query import (
|
|
|
49
49
|
IndexerMerkleProofsData,
|
|
50
50
|
IndexerInterestAndFundingParams,
|
|
51
51
|
IndexerInterestAndFundingData,
|
|
52
|
+
IndexerAccountSnapshotsParams,
|
|
53
|
+
IndexerAccountSnapshotsData,
|
|
52
54
|
IndexerTickersData,
|
|
53
55
|
IndexerPerpContractsData,
|
|
54
56
|
IndexerHistoricalTradesData,
|
|
@@ -459,3 +461,24 @@ class IndexerQueryClient:
|
|
|
459
461
|
if max_trade_id is not None:
|
|
460
462
|
url += f"&max_trade_id={max_trade_id}"
|
|
461
463
|
return ensure_data_type(self._query_v2(url), list)
|
|
464
|
+
|
|
465
|
+
def get_multi_subaccount_snapshots(
|
|
466
|
+
self, params: IndexerAccountSnapshotsParams
|
|
467
|
+
) -> IndexerAccountSnapshotsData:
|
|
468
|
+
"""
|
|
469
|
+
Retrieves subaccount snapshots at specified timestamps.
|
|
470
|
+
Each snapshot is a view of the subaccount's balances at that point in time,
|
|
471
|
+
with tracked variables for interest, funding, etc.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
params (IndexerAccountSnapshotsParams): Parameters specifying subaccounts,
|
|
475
|
+
timestamps, and whether to include isolated positions.
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
IndexerAccountSnapshotsData: Dict mapping subaccount hex -> timestamp -> snapshot data.
|
|
479
|
+
Each snapshot contains balances with trackedVars including netEntryUnrealized.
|
|
480
|
+
"""
|
|
481
|
+
return ensure_data_type(
|
|
482
|
+
self.query(IndexerAccountSnapshotsParams.parse_obj(params)).data,
|
|
483
|
+
IndexerAccountSnapshotsData,
|
|
484
|
+
)
|
|
@@ -182,7 +182,7 @@ class IndexerEventTrackedData(NadoBaseModel):
|
|
|
182
182
|
net_funding_cumulative: str
|
|
183
183
|
net_entry_unrealized: str
|
|
184
184
|
net_entry_cumulative: str
|
|
185
|
-
|
|
185
|
+
quote_volume_cumulative: str
|
|
186
186
|
|
|
187
187
|
|
|
188
188
|
class IndexerEvent(IndexerBaseModel, IndexerEventTrackedData):
|
|
@@ -49,6 +49,7 @@ class IndexerQueryType(StrEnum):
|
|
|
49
49
|
REFERRAL_CODE = "referral_code"
|
|
50
50
|
SUBACCOUNTS = "subaccounts"
|
|
51
51
|
USDC_PRICE = "usdc_price"
|
|
52
|
+
ACCOUNT_SNAPSHOTS = "account_snapshots"
|
|
52
53
|
# TODO: revise once this endpoint is live
|
|
53
54
|
TOKEN_MERKLE_PROOFS = "token_merkle_proofs"
|
|
54
55
|
FOUNDATION_REWARDS_MERKLE_PROOFS = "foundation_rewards_merkle_proofs"
|
|
@@ -292,6 +293,17 @@ class IndexerInterestAndFundingParams(NadoBaseModel):
|
|
|
292
293
|
limit: int
|
|
293
294
|
|
|
294
295
|
|
|
296
|
+
class IndexerAccountSnapshotsParams(NadoBaseModel):
|
|
297
|
+
"""
|
|
298
|
+
Parameters for querying account snapshots.
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
subaccounts: list[str]
|
|
302
|
+
timestamps: list[int]
|
|
303
|
+
isolated: Optional[bool] = None
|
|
304
|
+
active: Optional[bool] = None
|
|
305
|
+
|
|
306
|
+
|
|
295
307
|
IndexerParams = Union[
|
|
296
308
|
IndexerSubaccountHistoricalOrdersParams,
|
|
297
309
|
IndexerHistoricalOrdersByDigestParams,
|
|
@@ -314,6 +326,7 @@ IndexerParams = Union[
|
|
|
314
326
|
IndexerTokenMerkleProofsParams,
|
|
315
327
|
IndexerFoundationRewardsMerkleProofsParams,
|
|
316
328
|
IndexerInterestAndFundingParams,
|
|
329
|
+
IndexerAccountSnapshotsParams,
|
|
317
330
|
]
|
|
318
331
|
|
|
319
332
|
|
|
@@ -487,6 +500,14 @@ class IndexerInterestAndFundingRequest(NadoBaseModel):
|
|
|
487
500
|
interest_and_funding: IndexerInterestAndFundingParams
|
|
488
501
|
|
|
489
502
|
|
|
503
|
+
class IndexerAccountSnapshotsRequest(NadoBaseModel):
|
|
504
|
+
"""
|
|
505
|
+
Request object for querying account snapshots.
|
|
506
|
+
"""
|
|
507
|
+
|
|
508
|
+
account_snapshots: IndexerAccountSnapshotsParams
|
|
509
|
+
|
|
510
|
+
|
|
490
511
|
IndexerRequest = Union[
|
|
491
512
|
IndexerHistoricalOrdersRequest,
|
|
492
513
|
IndexerMatchesRequest,
|
|
@@ -508,6 +529,7 @@ IndexerRequest = Union[
|
|
|
508
529
|
IndexerTokenMerkleProofsRequest,
|
|
509
530
|
IndexerFoundationRewardsMerkleProofsRequest,
|
|
510
531
|
IndexerInterestAndFundingRequest,
|
|
532
|
+
IndexerAccountSnapshotsRequest,
|
|
511
533
|
]
|
|
512
534
|
|
|
513
535
|
|
|
@@ -681,6 +703,14 @@ class IndexerInterestAndFundingData(NadoBaseModel):
|
|
|
681
703
|
IndexerLiquidationFeedData = list[IndexerLiquidatableAccount]
|
|
682
704
|
|
|
683
705
|
|
|
706
|
+
class IndexerAccountSnapshotsData(NadoBaseModel):
|
|
707
|
+
"""
|
|
708
|
+
Data object for subaccount snapshots grouped by subaccount and timestamp.
|
|
709
|
+
"""
|
|
710
|
+
|
|
711
|
+
snapshots: Dict[str, Dict[str, list[IndexerEvent]]]
|
|
712
|
+
|
|
713
|
+
|
|
684
714
|
IndexerResponseData = Union[
|
|
685
715
|
IndexerHistoricalOrdersData,
|
|
686
716
|
IndexerMatchesData,
|
|
@@ -702,6 +732,7 @@ IndexerResponseData = Union[
|
|
|
702
732
|
IndexerInterestAndFundingData,
|
|
703
733
|
IndexerLiquidationFeedData,
|
|
704
734
|
IndexerFundingRatesData,
|
|
735
|
+
IndexerAccountSnapshotsData,
|
|
705
736
|
]
|
|
706
737
|
|
|
707
738
|
|
|
@@ -809,6 +840,10 @@ def to_indexer_request(params: IndexerParams) -> IndexerRequest:
|
|
|
809
840
|
IndexerInterestAndFundingRequest,
|
|
810
841
|
IndexerQueryType.INTEREST_AND_FUNDING.value,
|
|
811
842
|
),
|
|
843
|
+
IndexerAccountSnapshotsParams: (
|
|
844
|
+
IndexerAccountSnapshotsRequest,
|
|
845
|
+
IndexerQueryType.ACCOUNT_SNAPSHOTS.value,
|
|
846
|
+
),
|
|
812
847
|
}
|
|
813
848
|
|
|
814
849
|
RequestClass, field_name = indexer_request_mapping[type(params)]
|
nado_protocol/utils/backend.py
CHANGED
|
@@ -14,9 +14,9 @@ class NadoBackendURL(StrEnum):
|
|
|
14
14
|
DEVNET_TRIGGER = "http://localhost:8080"
|
|
15
15
|
|
|
16
16
|
# testnets
|
|
17
|
-
TESTNET_GATEWAY = "https://gateway.test.nado
|
|
18
|
-
TESTNET_INDEXER = "https://archive.test.nado
|
|
19
|
-
TESTNET_TRIGGER = "https://trigger.test.nado
|
|
17
|
+
TESTNET_GATEWAY = "https://gateway.test.nado.xyz/v1"
|
|
18
|
+
TESTNET_INDEXER = "https://archive.test.nado.xyz/v1"
|
|
19
|
+
TESTNET_TRIGGER = "https://trigger.test.nado.xyz/v1"
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
PrivateKey = str
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Balance Value Calculation Utilities
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from typing import Union
|
|
7
|
+
from nado_protocol.engine_client.types.models import (
|
|
8
|
+
SpotProduct,
|
|
9
|
+
PerpProduct,
|
|
10
|
+
SpotProductBalance,
|
|
11
|
+
PerpProductBalance,
|
|
12
|
+
)
|
|
13
|
+
from nado_protocol.utils.math import from_x18
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def calculate_spot_balance_value(
|
|
17
|
+
amount: Union[Decimal, float, str], oracle_price: Union[Decimal, float, str]
|
|
18
|
+
) -> Decimal:
|
|
19
|
+
"""
|
|
20
|
+
Calculate the quote value of a spot balance.
|
|
21
|
+
|
|
22
|
+
Formula: amount * oracle_price
|
|
23
|
+
|
|
24
|
+
This is used for:
|
|
25
|
+
- Calculating health contributions
|
|
26
|
+
- Determining deposits vs borrows
|
|
27
|
+
- Portfolio value calculations
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
amount: Token amount (can be negative for borrows)
|
|
31
|
+
oracle_price: Oracle price in quote currency
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Value in quote currency (positive for deposits, negative for borrows)
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
>>> calculate_spot_balance_value(100, 2000) # 100 ETH at $2000
|
|
38
|
+
Decimal('200000')
|
|
39
|
+
>>> calculate_spot_balance_value(-50, 2000) # 50 ETH borrowed
|
|
40
|
+
Decimal('-100000')
|
|
41
|
+
"""
|
|
42
|
+
amount_dec = Decimal(str(amount))
|
|
43
|
+
price_dec = Decimal(str(oracle_price))
|
|
44
|
+
return amount_dec * price_dec
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def calculate_perp_balance_notional_value(
|
|
48
|
+
amount: Union[Decimal, float, str], oracle_price: Union[Decimal, float, str]
|
|
49
|
+
) -> Decimal:
|
|
50
|
+
"""
|
|
51
|
+
Calculate the notional value of a perp position.
|
|
52
|
+
|
|
53
|
+
Formula: abs(amount * oracle_price)
|
|
54
|
+
|
|
55
|
+
This represents the total size of the position in quote currency terms,
|
|
56
|
+
regardless of direction (long or short).
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
amount: Position size (positive for long, negative for short)
|
|
60
|
+
oracle_price: Oracle price in quote currency
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Absolute notional value in quote currency
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
>>> calculate_perp_balance_notional_value(10, 50000) # 10 BTC long
|
|
67
|
+
Decimal('500000')
|
|
68
|
+
>>> calculate_perp_balance_notional_value(-10, 50000) # 10 BTC short
|
|
69
|
+
Decimal('500000')
|
|
70
|
+
"""
|
|
71
|
+
amount_dec = Decimal(str(amount))
|
|
72
|
+
price_dec = Decimal(str(oracle_price))
|
|
73
|
+
return abs(amount_dec * price_dec)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def calculate_perp_balance_value(
|
|
77
|
+
amount: Union[Decimal, float, str],
|
|
78
|
+
oracle_price: Union[Decimal, float, str],
|
|
79
|
+
v_quote_balance: Union[Decimal, float, str],
|
|
80
|
+
) -> Decimal:
|
|
81
|
+
"""
|
|
82
|
+
Calculate the true quote value of a perp balance (unrealized PnL).
|
|
83
|
+
|
|
84
|
+
Formula: (amount * oracle_price) + v_quote_balance
|
|
85
|
+
|
|
86
|
+
The v_quote_balance represents:
|
|
87
|
+
- Unrealized PnL from price changes
|
|
88
|
+
- Accumulated funding payments
|
|
89
|
+
- Entry cost adjustments
|
|
90
|
+
|
|
91
|
+
This value is what would be added to your balance if the position were closed.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
amount: Position size
|
|
95
|
+
oracle_price: Oracle price in quote currency
|
|
96
|
+
v_quote_balance: Virtual quote balance (unsettled PnL)
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Total value in quote currency (can be positive or negative)
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
>>> # Long 10 BTC at $50k, now at $51k, with funding
|
|
103
|
+
>>> calculate_perp_balance_value(10, 51000, -500000)
|
|
104
|
+
Decimal('10000') # $10k profit
|
|
105
|
+
"""
|
|
106
|
+
amount_dec = Decimal(str(amount))
|
|
107
|
+
price_dec = Decimal(str(oracle_price))
|
|
108
|
+
v_quote_dec = Decimal(str(v_quote_balance))
|
|
109
|
+
return (amount_dec * price_dec) + v_quote_dec
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def parse_spot_balance_value(
|
|
113
|
+
balance: SpotProductBalance, product: SpotProduct
|
|
114
|
+
) -> Decimal:
|
|
115
|
+
"""
|
|
116
|
+
Parse spot balance value from raw SDK types.
|
|
117
|
+
|
|
118
|
+
This is a convenience function that extracts values from the SDK types
|
|
119
|
+
and calls calculate_spot_balance_value.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
balance: Spot balance from subaccount info
|
|
123
|
+
product: Spot product information
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Balance value in quote currency
|
|
127
|
+
"""
|
|
128
|
+
amount = Decimal(from_x18(int(balance.balance.amount)))
|
|
129
|
+
oracle_price = Decimal(from_x18(int(product.oracle_price_x18)))
|
|
130
|
+
return calculate_spot_balance_value(amount, oracle_price)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def parse_perp_balance_notional_value(
|
|
134
|
+
balance: PerpProductBalance, product: PerpProduct
|
|
135
|
+
) -> Decimal:
|
|
136
|
+
"""
|
|
137
|
+
Parse perp notional value from raw SDK types.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
balance: Perp balance from subaccount info
|
|
141
|
+
product: Perp product information
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Notional value in quote currency
|
|
145
|
+
"""
|
|
146
|
+
amount = Decimal(from_x18(int(balance.balance.amount)))
|
|
147
|
+
oracle_price = Decimal(from_x18(int(product.oracle_price_x18)))
|
|
148
|
+
return calculate_perp_balance_notional_value(amount, oracle_price)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def parse_perp_balance_value(
|
|
152
|
+
balance: PerpProductBalance, product: PerpProduct
|
|
153
|
+
) -> Decimal:
|
|
154
|
+
"""
|
|
155
|
+
Parse perp balance value (unrealized PnL) from raw SDK types.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
balance: Perp balance from subaccount info
|
|
159
|
+
product: Perp product information
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Balance value in quote currency
|
|
163
|
+
"""
|
|
164
|
+
amount = Decimal(from_x18(int(balance.balance.amount)))
|
|
165
|
+
oracle_price = Decimal(from_x18(int(product.oracle_price_x18)))
|
|
166
|
+
v_quote = Decimal(from_x18(int(balance.balance.v_quote_balance)))
|
|
167
|
+
return calculate_perp_balance_value(amount, oracle_price, v_quote)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def calculate_total_spot_deposits_and_borrows(
|
|
171
|
+
balances: list[tuple[SpotProductBalance, SpotProduct]]
|
|
172
|
+
) -> tuple[Decimal, Decimal]:
|
|
173
|
+
"""
|
|
174
|
+
Calculate total spot deposits and borrows across all balances.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
balances: List of (balance, product) tuples
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Tuple of (total_deposits, total_borrows) in quote currency
|
|
181
|
+
Both values are positive (borrows is absolute value)
|
|
182
|
+
|
|
183
|
+
Example:
|
|
184
|
+
>>> balances = [(usdt_balance, usdt_product), (eth_balance, eth_product)]
|
|
185
|
+
>>> deposits, borrows = calculate_total_spot_deposits_and_borrows(balances)
|
|
186
|
+
>>> deposits # Total deposits
|
|
187
|
+
Decimal('10000')
|
|
188
|
+
>>> borrows # Total borrows (absolute value)
|
|
189
|
+
Decimal('5000')
|
|
190
|
+
"""
|
|
191
|
+
total_deposits = Decimal(0)
|
|
192
|
+
total_borrows = Decimal(0)
|
|
193
|
+
|
|
194
|
+
for balance, product in balances:
|
|
195
|
+
value = parse_spot_balance_value(balance, product)
|
|
196
|
+
if value > 0:
|
|
197
|
+
total_deposits += value
|
|
198
|
+
else:
|
|
199
|
+
total_borrows += abs(value)
|
|
200
|
+
|
|
201
|
+
return total_deposits, total_borrows
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def calculate_total_perp_notional(
|
|
205
|
+
balances: list[tuple[PerpProductBalance, PerpProduct]]
|
|
206
|
+
) -> Decimal:
|
|
207
|
+
"""
|
|
208
|
+
Calculate total notional value across all perp positions.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
balances: List of (balance, product) tuples
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Total notional value in quote currency
|
|
215
|
+
|
|
216
|
+
Example:
|
|
217
|
+
>>> balances = [(btc_perp_balance, btc_perp_product)]
|
|
218
|
+
>>> total = calculate_total_perp_notional(balances)
|
|
219
|
+
>>> total
|
|
220
|
+
Decimal('500000') # Total position size
|
|
221
|
+
"""
|
|
222
|
+
total = Decimal(0)
|
|
223
|
+
for balance, product in balances:
|
|
224
|
+
total += parse_perp_balance_notional_value(balance, product)
|
|
225
|
+
return total
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def calculate_total_perp_value(
|
|
229
|
+
balances: list[tuple[PerpProductBalance, PerpProduct]]
|
|
230
|
+
) -> Decimal:
|
|
231
|
+
"""
|
|
232
|
+
Calculate total unrealized PnL across all perp positions.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
balances: List of (balance, product) tuples
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Total unrealized PnL in quote currency (can be positive or negative)
|
|
239
|
+
|
|
240
|
+
Example:
|
|
241
|
+
>>> balances = [(btc_perp_balance, btc_perp_product)]
|
|
242
|
+
>>> total_pnl = calculate_total_perp_value(balances)
|
|
243
|
+
>>> total_pnl
|
|
244
|
+
Decimal('10000') # $10k unrealized profit
|
|
245
|
+
"""
|
|
246
|
+
total = Decimal(0)
|
|
247
|
+
for balance, product in balances:
|
|
248
|
+
total += parse_perp_balance_value(balance, product)
|
|
249
|
+
return total
|
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Margin Manager - Comprehensive margin calculations for Nado Protocol.
|
|
3
|
+
|
|
4
|
+
This module calculates all margin-related metrics including health, margin usage,
|
|
5
|
+
leverage, and position-level details. All calculations use oracle prices.
|
|
6
|
+
|
|
7
|
+
Key Concepts:
|
|
8
|
+
- Health Types: Initial (strictest), Maintenance (liquidation), Unweighted (raw)
|
|
9
|
+
- Cross Margin: Shared across all positions
|
|
10
|
+
- Isolated Margin: Dedicated per position (perp only, USDT only)
|
|
11
|
+
- Health = Assets - Liabilities, calculated per balance using oracle_price * weight
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from decimal import Decimal
|
|
15
|
+
from time import time
|
|
16
|
+
from typing import Optional, Union, TYPE_CHECKING
|
|
17
|
+
from pydantic import BaseModel
|
|
18
|
+
from nado_protocol.engine_client.types.models import (
|
|
19
|
+
SpotProduct,
|
|
20
|
+
PerpProduct,
|
|
21
|
+
SpotProductBalance,
|
|
22
|
+
PerpProductBalance,
|
|
23
|
+
SubaccountHealth,
|
|
24
|
+
IsolatedPosition,
|
|
25
|
+
)
|
|
26
|
+
from nado_protocol.engine_client.types.query import SubaccountInfoData
|
|
27
|
+
from nado_protocol.indexer_client.types.models import IndexerEvent
|
|
28
|
+
from nado_protocol.indexer_client.types.query import IndexerAccountSnapshotsParams
|
|
29
|
+
from nado_protocol.utils.bytes32 import subaccount_to_hex
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from nado_protocol.client import NadoClient
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
TEN_TO_18 = Decimal(10) ** 18
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _from_x18_decimal(value: Union[int, str]) -> Decimal:
|
|
39
|
+
"""Convert an x18 fixed-point integer (str or int) to Decimal without precision loss."""
|
|
40
|
+
return Decimal(str(value)) / TEN_TO_18
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class HealthMetrics(BaseModel):
|
|
44
|
+
"""Initial and maintenance health metrics."""
|
|
45
|
+
|
|
46
|
+
initial: Decimal
|
|
47
|
+
maintenance: Decimal
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class MarginUsageFractions(BaseModel):
|
|
51
|
+
"""Margin usage as a fraction [0, 1]."""
|
|
52
|
+
|
|
53
|
+
initial: Decimal
|
|
54
|
+
maintenance: Decimal
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BalanceWithProduct(BaseModel):
|
|
58
|
+
"""Balance combined with its product information."""
|
|
59
|
+
|
|
60
|
+
product_id: int
|
|
61
|
+
amount: Decimal
|
|
62
|
+
oracle_price: Decimal
|
|
63
|
+
long_weight_initial: Decimal
|
|
64
|
+
long_weight_maintenance: Decimal
|
|
65
|
+
short_weight_initial: Decimal
|
|
66
|
+
short_weight_maintenance: Decimal
|
|
67
|
+
balance_type: str # "spot" or "perp"
|
|
68
|
+
v_quote_balance: Optional[Decimal] = None
|
|
69
|
+
|
|
70
|
+
class Config:
|
|
71
|
+
arbitrary_types_allowed = True
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class CrossPositionMetrics(BaseModel):
|
|
75
|
+
"""Metrics for a cross margin position."""
|
|
76
|
+
|
|
77
|
+
product_id: int
|
|
78
|
+
symbol: str
|
|
79
|
+
position_size: Decimal
|
|
80
|
+
notional_value: Decimal
|
|
81
|
+
est_pnl: Optional[Decimal] # Estimated PnL (requires indexer data)
|
|
82
|
+
unsettled: Decimal # Unsettled quote (v_quote_balance)
|
|
83
|
+
margin_used: Decimal
|
|
84
|
+
initial_health: Decimal
|
|
85
|
+
maintenance_health: Decimal
|
|
86
|
+
long_weight_initial: Decimal
|
|
87
|
+
long_weight_maintenance: Decimal
|
|
88
|
+
short_weight_initial: Decimal
|
|
89
|
+
short_weight_maintenance: Decimal
|
|
90
|
+
|
|
91
|
+
class Config:
|
|
92
|
+
arbitrary_types_allowed = True
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class IsolatedPositionMetrics(BaseModel):
|
|
96
|
+
"""Metrics for an isolated margin position."""
|
|
97
|
+
|
|
98
|
+
product_id: int
|
|
99
|
+
symbol: str
|
|
100
|
+
position_size: Decimal
|
|
101
|
+
notional_value: Decimal
|
|
102
|
+
net_margin: Decimal
|
|
103
|
+
leverage: Decimal
|
|
104
|
+
initial_health: Decimal
|
|
105
|
+
maintenance_health: Decimal
|
|
106
|
+
|
|
107
|
+
class Config:
|
|
108
|
+
arbitrary_types_allowed = True
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class AccountSummary(BaseModel):
|
|
112
|
+
"""Complete account margin summary."""
|
|
113
|
+
|
|
114
|
+
# Overall health
|
|
115
|
+
initial_health: Decimal
|
|
116
|
+
maintenance_health: Decimal
|
|
117
|
+
unweighted_health: Decimal
|
|
118
|
+
|
|
119
|
+
# Margin usage [0, 1]
|
|
120
|
+
margin_usage_fraction: Decimal
|
|
121
|
+
maint_margin_usage_fraction: Decimal
|
|
122
|
+
|
|
123
|
+
# Available margins
|
|
124
|
+
funds_available: Decimal
|
|
125
|
+
funds_until_liquidation: Decimal
|
|
126
|
+
|
|
127
|
+
# Portfolio metrics
|
|
128
|
+
portfolio_value: Decimal
|
|
129
|
+
account_leverage: Decimal
|
|
130
|
+
|
|
131
|
+
# Positions
|
|
132
|
+
cross_positions: list[CrossPositionMetrics]
|
|
133
|
+
isolated_positions: list[IsolatedPositionMetrics]
|
|
134
|
+
spot_positions: list[BalanceWithProduct]
|
|
135
|
+
|
|
136
|
+
# Spot balances
|
|
137
|
+
total_spot_deposits: Decimal
|
|
138
|
+
total_spot_borrows: Decimal
|
|
139
|
+
|
|
140
|
+
class Config:
|
|
141
|
+
arbitrary_types_allowed = True
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class MarginManager:
|
|
145
|
+
"""
|
|
146
|
+
Comprehensive margin calculator for Nado Protocol.
|
|
147
|
+
|
|
148
|
+
Calculates all margin metrics for a subaccount including health, margin usage,
|
|
149
|
+
leverage, and position-level details. Matches TypeScript SDK implementation.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
QUOTE_PRODUCT_ID = 0 # USDT product ID
|
|
153
|
+
|
|
154
|
+
def __init__(
|
|
155
|
+
self,
|
|
156
|
+
subaccount_info: SubaccountInfoData,
|
|
157
|
+
isolated_positions: Optional[list[IsolatedPosition]] = None,
|
|
158
|
+
indexer_snapshot_events: Optional[list[IndexerEvent]] = None,
|
|
159
|
+
):
|
|
160
|
+
"""
|
|
161
|
+
Initialize margin manager with subaccount data.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
subaccount_info: Subaccount information from engine
|
|
165
|
+
isolated_positions: List of isolated positions (if any)
|
|
166
|
+
indexer_snapshot_events: Optional indexer events for Est. PnL calculations
|
|
167
|
+
"""
|
|
168
|
+
self.subaccount_info = subaccount_info
|
|
169
|
+
self.isolated_positions = isolated_positions or []
|
|
170
|
+
self.indexer_events = indexer_snapshot_events or []
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def from_client(
|
|
174
|
+
cls,
|
|
175
|
+
client: "NadoClient",
|
|
176
|
+
*,
|
|
177
|
+
subaccount: Optional[str] = None,
|
|
178
|
+
subaccount_name: str = "default",
|
|
179
|
+
include_indexer_events: bool = True,
|
|
180
|
+
snapshot_timestamp: Optional[int] = None,
|
|
181
|
+
snapshot_isolated: Optional[bool] = False,
|
|
182
|
+
snapshot_active_only: bool = True,
|
|
183
|
+
) -> "MarginManager":
|
|
184
|
+
"""
|
|
185
|
+
Initialize a MarginManager by fetching data via a NadoClient.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
client: Configured Nado client with engine/indexer connectivity.
|
|
189
|
+
subaccount: Optional subaccount hex (bytes32). If omitted, derives the default
|
|
190
|
+
subaccount using the client's signer and ``subaccount_name``.
|
|
191
|
+
subaccount_name: Subaccount suffix (e.g. ``default``) used when deriving the
|
|
192
|
+
subaccount hex. Ignored when ``subaccount`` is provided.
|
|
193
|
+
include_indexer_events: When True (default), fetch indexer snapshot balances
|
|
194
|
+
for estimated PnL calculations.
|
|
195
|
+
snapshot_timestamp: Epoch seconds to request from the indexer. Defaults to
|
|
196
|
+
``int(time.time())`` when indexer data is requested.
|
|
197
|
+
snapshot_isolated: Passed through to the indexer request to limit snapshots
|
|
198
|
+
to isolated (True), cross (False), or all (None) balances. Defaults to
|
|
199
|
+
``False`` to match cross-margin behaviour.
|
|
200
|
+
snapshot_active_only: When True (default), enables the indexer's ``active``
|
|
201
|
+
filter so only live balances are returned.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
MarginManager instance populated with fresh engine and optional indexer data.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
engine_client = client.context.engine_client
|
|
208
|
+
|
|
209
|
+
resolved_subaccount = subaccount
|
|
210
|
+
if resolved_subaccount is None:
|
|
211
|
+
signer = client.context.signer
|
|
212
|
+
if signer is None:
|
|
213
|
+
raise ValueError(
|
|
214
|
+
"subaccount must be provided when the client has no signer"
|
|
215
|
+
)
|
|
216
|
+
resolved_subaccount = subaccount_to_hex(signer.address, subaccount_name)
|
|
217
|
+
|
|
218
|
+
subaccount_info = engine_client.get_subaccount_info(resolved_subaccount)
|
|
219
|
+
isolated_positions_data = engine_client.get_isolated_positions(
|
|
220
|
+
resolved_subaccount
|
|
221
|
+
)
|
|
222
|
+
isolated_positions = isolated_positions_data.isolated_positions
|
|
223
|
+
|
|
224
|
+
indexer_events: list[IndexerEvent] = []
|
|
225
|
+
if include_indexer_events:
|
|
226
|
+
requested_timestamp = snapshot_timestamp or int(time())
|
|
227
|
+
indexer_events = cls._fetch_snapshot_events(
|
|
228
|
+
client,
|
|
229
|
+
resolved_subaccount,
|
|
230
|
+
requested_timestamp,
|
|
231
|
+
snapshot_isolated,
|
|
232
|
+
snapshot_active_only,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return cls(
|
|
236
|
+
subaccount_info,
|
|
237
|
+
isolated_positions,
|
|
238
|
+
indexer_snapshot_events=indexer_events,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
@staticmethod
|
|
242
|
+
def _fetch_snapshot_events(
|
|
243
|
+
client: "NadoClient",
|
|
244
|
+
subaccount: str,
|
|
245
|
+
timestamp: int,
|
|
246
|
+
isolated: Optional[bool],
|
|
247
|
+
active_only: bool,
|
|
248
|
+
) -> list[IndexerEvent]:
|
|
249
|
+
snapshot_response = (
|
|
250
|
+
client.context.indexer_client.get_multi_subaccount_snapshots(
|
|
251
|
+
IndexerAccountSnapshotsParams(
|
|
252
|
+
subaccounts=[subaccount],
|
|
253
|
+
timestamps=[timestamp],
|
|
254
|
+
isolated=isolated,
|
|
255
|
+
active=active_only,
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
snapshots_map = snapshot_response.snapshots or {}
|
|
261
|
+
if not snapshots_map:
|
|
262
|
+
return []
|
|
263
|
+
|
|
264
|
+
snapshots_for_subaccount = snapshots_map.get(subaccount) or next(
|
|
265
|
+
iter(snapshots_map.values())
|
|
266
|
+
)
|
|
267
|
+
if not snapshots_for_subaccount:
|
|
268
|
+
return []
|
|
269
|
+
|
|
270
|
+
latest_key = max(snapshots_for_subaccount.keys(), key=int)
|
|
271
|
+
events = snapshots_for_subaccount.get(latest_key, [])
|
|
272
|
+
return list(events) if events else []
|
|
273
|
+
|
|
274
|
+
def calculate_account_summary(self) -> AccountSummary:
|
|
275
|
+
"""
|
|
276
|
+
Calculate complete account margin summary.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
AccountSummary with all margin calculations
|
|
280
|
+
"""
|
|
281
|
+
# Parse health from subaccount info
|
|
282
|
+
# healths is a list: [initial, maintenance, unweighted]
|
|
283
|
+
initial_health = self._parse_health(self.subaccount_info.healths[0])
|
|
284
|
+
maint_health = self._parse_health(self.subaccount_info.healths[1])
|
|
285
|
+
unweighted_health = self._parse_health(self.subaccount_info.healths[2])
|
|
286
|
+
|
|
287
|
+
# Calculate margin usage
|
|
288
|
+
margin_usage = self.calculate_margin_usage_fractions(
|
|
289
|
+
initial_health, maint_health, unweighted_health
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Process all balances
|
|
293
|
+
spot_balances = self._create_spot_balances()
|
|
294
|
+
perp_balances = self._create_perp_balances()
|
|
295
|
+
|
|
296
|
+
# Calculate cross position metrics
|
|
297
|
+
cross_positions: list[CrossPositionMetrics] = []
|
|
298
|
+
for balance in perp_balances:
|
|
299
|
+
if balance.amount != 0:
|
|
300
|
+
cross_metric = self.calculate_cross_position_metrics(balance)
|
|
301
|
+
cross_positions.append(cross_metric)
|
|
302
|
+
|
|
303
|
+
# Calculate isolated position metrics
|
|
304
|
+
isolated_position_metrics: list[IsolatedPositionMetrics] = []
|
|
305
|
+
total_iso_net_margin = Decimal(0)
|
|
306
|
+
for iso_pos in self.isolated_positions:
|
|
307
|
+
isolated_metric = self.calculate_isolated_position_metrics(iso_pos)
|
|
308
|
+
isolated_position_metrics.append(isolated_metric)
|
|
309
|
+
total_iso_net_margin += isolated_metric.net_margin
|
|
310
|
+
|
|
311
|
+
# Calculate spot metrics
|
|
312
|
+
total_deposits = Decimal(0)
|
|
313
|
+
total_borrows = Decimal(0)
|
|
314
|
+
for balance in spot_balances:
|
|
315
|
+
value = self.calculate_spot_balance_value(balance)
|
|
316
|
+
if value > 0:
|
|
317
|
+
total_deposits += value
|
|
318
|
+
else:
|
|
319
|
+
total_borrows += abs(value)
|
|
320
|
+
|
|
321
|
+
# Calculate leverage
|
|
322
|
+
leverage = self.calculate_account_leverage(
|
|
323
|
+
spot_balances + perp_balances, unweighted_health
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# Portfolio value = cross value + isolated net margins
|
|
327
|
+
portfolio_value = unweighted_health + total_iso_net_margin
|
|
328
|
+
|
|
329
|
+
return AccountSummary(
|
|
330
|
+
initial_health=initial_health,
|
|
331
|
+
maintenance_health=maint_health,
|
|
332
|
+
unweighted_health=unweighted_health,
|
|
333
|
+
margin_usage_fraction=margin_usage.initial,
|
|
334
|
+
maint_margin_usage_fraction=margin_usage.maintenance,
|
|
335
|
+
funds_available=max(Decimal(0), initial_health),
|
|
336
|
+
funds_until_liquidation=max(Decimal(0), maint_health),
|
|
337
|
+
portfolio_value=portfolio_value,
|
|
338
|
+
account_leverage=leverage,
|
|
339
|
+
cross_positions=cross_positions,
|
|
340
|
+
isolated_positions=isolated_position_metrics,
|
|
341
|
+
spot_positions=spot_balances,
|
|
342
|
+
total_spot_deposits=total_deposits,
|
|
343
|
+
total_spot_borrows=total_borrows,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
def calculate_spot_balance_value(self, balance: BalanceWithProduct) -> Decimal:
|
|
347
|
+
"""
|
|
348
|
+
Calculate quote value of a spot balance.
|
|
349
|
+
|
|
350
|
+
Formula: amount * oracle_price
|
|
351
|
+
"""
|
|
352
|
+
return balance.amount * balance.oracle_price
|
|
353
|
+
|
|
354
|
+
def calculate_perp_balance_notional_value(
|
|
355
|
+
self, balance: BalanceWithProduct
|
|
356
|
+
) -> Decimal:
|
|
357
|
+
"""
|
|
358
|
+
Calculate notional value of a perp position.
|
|
359
|
+
|
|
360
|
+
Formula: abs(amount * oracle_price)
|
|
361
|
+
"""
|
|
362
|
+
return abs(balance.amount * balance.oracle_price)
|
|
363
|
+
|
|
364
|
+
def calculate_perp_balance_value(self, balance: BalanceWithProduct) -> Decimal:
|
|
365
|
+
"""
|
|
366
|
+
Calculate true quote value of a perp balance (unrealized PnL).
|
|
367
|
+
|
|
368
|
+
Formula: (amount * oracle_price) + v_quote_balance
|
|
369
|
+
"""
|
|
370
|
+
if balance.v_quote_balance is None:
|
|
371
|
+
raise ValueError("Perp balance must have v_quote_balance")
|
|
372
|
+
return (balance.amount * balance.oracle_price) + balance.v_quote_balance
|
|
373
|
+
|
|
374
|
+
def calculate_spot_balance_health(
|
|
375
|
+
self, balance: BalanceWithProduct
|
|
376
|
+
) -> HealthMetrics:
|
|
377
|
+
"""
|
|
378
|
+
Calculate health contribution for a spot balance.
|
|
379
|
+
|
|
380
|
+
Formula: amount * oracle_price * weight
|
|
381
|
+
(weight is long_weight if amount >= 0, else short_weight)
|
|
382
|
+
"""
|
|
383
|
+
weights = self._get_health_weights(balance)
|
|
384
|
+
value = balance.amount * balance.oracle_price
|
|
385
|
+
|
|
386
|
+
return HealthMetrics(
|
|
387
|
+
initial=value * weights.initial, maintenance=value * weights.maintenance
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
def calculate_perp_balance_health_without_pnl(
|
|
391
|
+
self, balance: BalanceWithProduct
|
|
392
|
+
) -> HealthMetrics:
|
|
393
|
+
"""
|
|
394
|
+
Calculate perp balance health WITHOUT the impact of unsettled PnL.
|
|
395
|
+
|
|
396
|
+
Shows "margin used" by the position, excluding PnL.
|
|
397
|
+
Formula: -1 * abs(notional_value) * (1 - long_weight)
|
|
398
|
+
"""
|
|
399
|
+
initial_leverage_adjustment = Decimal(1) - balance.long_weight_initial
|
|
400
|
+
maint_leverage_adjustment = Decimal(1) - balance.long_weight_maintenance
|
|
401
|
+
|
|
402
|
+
base_margin_value = abs(balance.amount) * balance.oracle_price
|
|
403
|
+
|
|
404
|
+
return HealthMetrics(
|
|
405
|
+
initial=base_margin_value * initial_leverage_adjustment * Decimal(-1),
|
|
406
|
+
maintenance=base_margin_value * maint_leverage_adjustment * Decimal(-1),
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
def calculate_cross_position_margin_without_pnl(
|
|
410
|
+
self, balance: BalanceWithProduct
|
|
411
|
+
) -> Decimal:
|
|
412
|
+
"""
|
|
413
|
+
Calculate margin used for a cross position excluding unsettled PnL impact.
|
|
414
|
+
|
|
415
|
+
Used in margin manager "Margin Used" column.
|
|
416
|
+
Formula: max(0, -(initial_health - perp_value))
|
|
417
|
+
"""
|
|
418
|
+
health_with_pnl = self.calculate_spot_balance_health(balance).initial
|
|
419
|
+
perp_value = self.calculate_perp_balance_value(balance)
|
|
420
|
+
|
|
421
|
+
without_unsettled_pnl = health_with_pnl - perp_value
|
|
422
|
+
return max(Decimal(0), -without_unsettled_pnl)
|
|
423
|
+
|
|
424
|
+
def calculate_isolated_position_net_margin(
|
|
425
|
+
self, base_balance: BalanceWithProduct, quote_balance: BalanceWithProduct
|
|
426
|
+
) -> Decimal:
|
|
427
|
+
"""
|
|
428
|
+
Calculate net margin in an isolated position.
|
|
429
|
+
|
|
430
|
+
Formula: quote_amount + (base_amount * oracle_price + v_quote_balance)
|
|
431
|
+
"""
|
|
432
|
+
total_margin = quote_balance.amount
|
|
433
|
+
unsettled_quote = self.calculate_perp_balance_value(base_balance)
|
|
434
|
+
return total_margin + unsettled_quote
|
|
435
|
+
|
|
436
|
+
def calculate_isolated_position_leverage(
|
|
437
|
+
self, base_balance: BalanceWithProduct, net_margin: Decimal
|
|
438
|
+
) -> Decimal:
|
|
439
|
+
"""
|
|
440
|
+
Calculate leverage for an isolated position.
|
|
441
|
+
|
|
442
|
+
Formula: notional_value / net_margin
|
|
443
|
+
"""
|
|
444
|
+
if net_margin == 0:
|
|
445
|
+
return Decimal(0)
|
|
446
|
+
|
|
447
|
+
notional_value = self.calculate_perp_balance_notional_value(base_balance)
|
|
448
|
+
return notional_value / net_margin
|
|
449
|
+
|
|
450
|
+
def calculate_margin_usage_fractions(
|
|
451
|
+
self, initial_health: Decimal, maint_health: Decimal, unweighted_health: Decimal
|
|
452
|
+
) -> MarginUsageFractions:
|
|
453
|
+
"""
|
|
454
|
+
Calculate margin usage fractions bounded to [0, 1].
|
|
455
|
+
|
|
456
|
+
Formula: (unweighted_health - health) / unweighted_health
|
|
457
|
+
Returns 0 if no borrows/perps or unweighted_health is 0.
|
|
458
|
+
"""
|
|
459
|
+
if unweighted_health == 0:
|
|
460
|
+
return MarginUsageFractions(initial=Decimal(0), maintenance=Decimal(0))
|
|
461
|
+
|
|
462
|
+
if not self._has_borrows_or_perps():
|
|
463
|
+
return MarginUsageFractions(initial=Decimal(0), maintenance=Decimal(0))
|
|
464
|
+
|
|
465
|
+
initial_usage = (unweighted_health - initial_health) / unweighted_health
|
|
466
|
+
maint_usage = (unweighted_health - maint_health) / unweighted_health
|
|
467
|
+
|
|
468
|
+
# If health is negative, max out margin usage
|
|
469
|
+
return MarginUsageFractions(
|
|
470
|
+
initial=(
|
|
471
|
+
Decimal(1) if initial_health < 0 else min(initial_usage, Decimal(1))
|
|
472
|
+
),
|
|
473
|
+
maintenance=(
|
|
474
|
+
Decimal(1) if maint_health < 0 else min(maint_usage, Decimal(1))
|
|
475
|
+
),
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
def calculate_account_leverage(
|
|
479
|
+
self, balances: list[BalanceWithProduct], unweighted_health: Decimal
|
|
480
|
+
) -> Decimal:
|
|
481
|
+
"""
|
|
482
|
+
Calculate overall account leverage.
|
|
483
|
+
|
|
484
|
+
Formula: sum(abs(unweighted health for non-quote balances)) / unweighted_health
|
|
485
|
+
"""
|
|
486
|
+
if unweighted_health == 0:
|
|
487
|
+
return Decimal(0)
|
|
488
|
+
|
|
489
|
+
if not self._has_borrows_or_perps():
|
|
490
|
+
return Decimal(0)
|
|
491
|
+
|
|
492
|
+
numerator = Decimal(0)
|
|
493
|
+
for balance in balances:
|
|
494
|
+
if balance.product_id == self.QUOTE_PRODUCT_ID:
|
|
495
|
+
continue
|
|
496
|
+
|
|
497
|
+
if self._is_zero_health(balance):
|
|
498
|
+
continue
|
|
499
|
+
|
|
500
|
+
if balance.balance_type == "spot":
|
|
501
|
+
value = abs(balance.amount * balance.oracle_price)
|
|
502
|
+
else:
|
|
503
|
+
value = self.calculate_perp_balance_notional_value(balance)
|
|
504
|
+
|
|
505
|
+
numerator += value
|
|
506
|
+
|
|
507
|
+
return numerator / unweighted_health
|
|
508
|
+
|
|
509
|
+
def calculate_cross_position_metrics(
|
|
510
|
+
self, balance: BalanceWithProduct
|
|
511
|
+
) -> CrossPositionMetrics:
|
|
512
|
+
"""Calculate all metrics for a cross margin position."""
|
|
513
|
+
notional = self.calculate_perp_balance_notional_value(balance)
|
|
514
|
+
health_metrics = self.calculate_spot_balance_health(balance)
|
|
515
|
+
margin_used = abs(
|
|
516
|
+
self.calculate_perp_balance_health_without_pnl(balance).initial
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# Unsettled = full perp balance value (amount × oracle_price + v_quote_balance)
|
|
520
|
+
# This represents the unrealized PnL
|
|
521
|
+
unsettled = self.calculate_perp_balance_value(balance)
|
|
522
|
+
|
|
523
|
+
# Calculate Est. PnL if indexer data is available
|
|
524
|
+
# Formula: (amount × oracle_price) - netEntryUnrealized
|
|
525
|
+
# where netEntryUnrealized excludes funding, fees, slippage
|
|
526
|
+
est_pnl = self._calculate_est_pnl(balance)
|
|
527
|
+
|
|
528
|
+
return CrossPositionMetrics(
|
|
529
|
+
product_id=balance.product_id,
|
|
530
|
+
symbol=f"Product_{balance.product_id}",
|
|
531
|
+
position_size=balance.amount,
|
|
532
|
+
notional_value=notional,
|
|
533
|
+
est_pnl=est_pnl,
|
|
534
|
+
unsettled=unsettled,
|
|
535
|
+
margin_used=margin_used,
|
|
536
|
+
initial_health=health_metrics.initial,
|
|
537
|
+
maintenance_health=health_metrics.maintenance,
|
|
538
|
+
long_weight_initial=balance.long_weight_initial,
|
|
539
|
+
long_weight_maintenance=balance.long_weight_maintenance,
|
|
540
|
+
short_weight_initial=balance.short_weight_initial,
|
|
541
|
+
short_weight_maintenance=balance.short_weight_maintenance,
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
def _calculate_est_pnl(self, balance: BalanceWithProduct) -> Optional[Decimal]:
|
|
545
|
+
"""
|
|
546
|
+
Calculate estimated PnL if indexer snapshot is available.
|
|
547
|
+
|
|
548
|
+
Formula: (position_amount × oracle_price) - netEntryUnrealized
|
|
549
|
+
|
|
550
|
+
Returns None if indexer data is not available.
|
|
551
|
+
"""
|
|
552
|
+
if not self.indexer_events or balance.product_id == self.QUOTE_PRODUCT_ID:
|
|
553
|
+
return None
|
|
554
|
+
|
|
555
|
+
for event in self.indexer_events:
|
|
556
|
+
if event.product_id != balance.product_id:
|
|
557
|
+
continue
|
|
558
|
+
if event.isolated:
|
|
559
|
+
continue
|
|
560
|
+
|
|
561
|
+
try:
|
|
562
|
+
net_entry_int = int(event.net_entry_unrealized)
|
|
563
|
+
except (TypeError, ValueError):
|
|
564
|
+
continue
|
|
565
|
+
|
|
566
|
+
net_entry_unrealized = Decimal(net_entry_int) / Decimal(10**18)
|
|
567
|
+
|
|
568
|
+
current_value = balance.amount * balance.oracle_price
|
|
569
|
+
return current_value - net_entry_unrealized
|
|
570
|
+
|
|
571
|
+
return None
|
|
572
|
+
|
|
573
|
+
def calculate_isolated_position_metrics(
|
|
574
|
+
self, iso_pos: IsolatedPosition
|
|
575
|
+
) -> IsolatedPositionMetrics:
|
|
576
|
+
"""Calculate all metrics for an isolated position."""
|
|
577
|
+
base_balance = self._create_balance_from_isolated(iso_pos, is_base=True)
|
|
578
|
+
quote_balance = self._create_balance_from_isolated(iso_pos, is_base=False)
|
|
579
|
+
|
|
580
|
+
net_margin = self.calculate_isolated_position_net_margin(
|
|
581
|
+
base_balance, quote_balance
|
|
582
|
+
)
|
|
583
|
+
leverage = self.calculate_isolated_position_leverage(base_balance, net_margin)
|
|
584
|
+
notional = self.calculate_perp_balance_notional_value(base_balance)
|
|
585
|
+
|
|
586
|
+
initial_health = (
|
|
587
|
+
self._parse_health(iso_pos.healths[0]) if iso_pos.healths else Decimal(0)
|
|
588
|
+
)
|
|
589
|
+
maint_health = (
|
|
590
|
+
self._parse_health(iso_pos.healths[1])
|
|
591
|
+
if len(iso_pos.healths) > 1
|
|
592
|
+
else Decimal(0)
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
return IsolatedPositionMetrics(
|
|
596
|
+
product_id=base_balance.product_id,
|
|
597
|
+
symbol=f"Product_{base_balance.product_id}",
|
|
598
|
+
position_size=base_balance.amount,
|
|
599
|
+
notional_value=notional,
|
|
600
|
+
net_margin=net_margin,
|
|
601
|
+
leverage=leverage,
|
|
602
|
+
initial_health=initial_health,
|
|
603
|
+
maintenance_health=maint_health,
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
# Helper methods
|
|
607
|
+
|
|
608
|
+
def _get_health_weights(self, balance: BalanceWithProduct) -> HealthMetrics:
|
|
609
|
+
"""Get appropriate weights based on position direction."""
|
|
610
|
+
if balance.amount >= 0:
|
|
611
|
+
return HealthMetrics(
|
|
612
|
+
initial=balance.long_weight_initial,
|
|
613
|
+
maintenance=balance.long_weight_maintenance,
|
|
614
|
+
)
|
|
615
|
+
else:
|
|
616
|
+
return HealthMetrics(
|
|
617
|
+
initial=balance.short_weight_initial,
|
|
618
|
+
maintenance=balance.short_weight_maintenance,
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
def _has_borrows_or_perps(self) -> bool:
|
|
622
|
+
"""Check if account has any borrows or perp positions."""
|
|
623
|
+
for spot_bal in self.subaccount_info.spot_balances:
|
|
624
|
+
amount = _from_x18_decimal(spot_bal.balance.amount)
|
|
625
|
+
if amount < 0:
|
|
626
|
+
return True
|
|
627
|
+
|
|
628
|
+
for perp_bal in self.subaccount_info.perp_balances:
|
|
629
|
+
amount = _from_x18_decimal(perp_bal.balance.amount)
|
|
630
|
+
if amount != 0:
|
|
631
|
+
return True
|
|
632
|
+
|
|
633
|
+
return False
|
|
634
|
+
|
|
635
|
+
def _is_zero_health(self, balance: BalanceWithProduct) -> bool:
|
|
636
|
+
"""Check if product has zero health (long_weight=0, short_weight=2)."""
|
|
637
|
+
return balance.long_weight_initial == 0 and balance.short_weight_initial == 2
|
|
638
|
+
|
|
639
|
+
def _parse_health(self, health: SubaccountHealth) -> Decimal:
|
|
640
|
+
"""Parse health from SubaccountHealth model."""
|
|
641
|
+
return _from_x18_decimal(health.health)
|
|
642
|
+
|
|
643
|
+
def _create_spot_balances(self) -> list[BalanceWithProduct]:
|
|
644
|
+
"""Create BalanceWithProduct objects for all spot balances."""
|
|
645
|
+
balances: list[BalanceWithProduct] = []
|
|
646
|
+
for spot_bal, spot_prod in zip(
|
|
647
|
+
self.subaccount_info.spot_balances, self.subaccount_info.spot_products
|
|
648
|
+
):
|
|
649
|
+
balance = self._create_balance_with_product(spot_bal, spot_prod, "spot")
|
|
650
|
+
balances.append(balance)
|
|
651
|
+
return balances
|
|
652
|
+
|
|
653
|
+
def _create_perp_balances(self) -> list[BalanceWithProduct]:
|
|
654
|
+
"""Create BalanceWithProduct objects for all perp balances."""
|
|
655
|
+
balances: list[BalanceWithProduct] = []
|
|
656
|
+
for perp_bal, perp_prod in zip(
|
|
657
|
+
self.subaccount_info.perp_balances, self.subaccount_info.perp_products
|
|
658
|
+
):
|
|
659
|
+
balance = self._create_balance_with_product(perp_bal, perp_prod, "perp")
|
|
660
|
+
balances.append(balance)
|
|
661
|
+
return balances
|
|
662
|
+
|
|
663
|
+
def _create_balance_with_product(
|
|
664
|
+
self,
|
|
665
|
+
balance: Union[SpotProductBalance, PerpProductBalance],
|
|
666
|
+
product: Union[SpotProduct, PerpProduct],
|
|
667
|
+
balance_type: str,
|
|
668
|
+
) -> BalanceWithProduct:
|
|
669
|
+
"""Create a BalanceWithProduct from raw balance and product data."""
|
|
670
|
+
amount = _from_x18_decimal(balance.balance.amount)
|
|
671
|
+
oracle_price = _from_x18_decimal(product.oracle_price_x18)
|
|
672
|
+
|
|
673
|
+
v_quote = None
|
|
674
|
+
if balance_type == "perp":
|
|
675
|
+
assert isinstance(
|
|
676
|
+
balance, PerpProductBalance
|
|
677
|
+
), "Perp balances must be PerpProductBalance"
|
|
678
|
+
v_quote = _from_x18_decimal(balance.balance.v_quote_balance)
|
|
679
|
+
|
|
680
|
+
return BalanceWithProduct(
|
|
681
|
+
product_id=balance.product_id,
|
|
682
|
+
amount=amount,
|
|
683
|
+
oracle_price=oracle_price,
|
|
684
|
+
long_weight_initial=_from_x18_decimal(product.risk.long_weight_initial_x18),
|
|
685
|
+
long_weight_maintenance=_from_x18_decimal(
|
|
686
|
+
product.risk.long_weight_maintenance_x18
|
|
687
|
+
),
|
|
688
|
+
short_weight_initial=_from_x18_decimal(
|
|
689
|
+
product.risk.short_weight_initial_x18
|
|
690
|
+
),
|
|
691
|
+
short_weight_maintenance=_from_x18_decimal(
|
|
692
|
+
product.risk.short_weight_maintenance_x18
|
|
693
|
+
),
|
|
694
|
+
balance_type=balance_type,
|
|
695
|
+
v_quote_balance=v_quote,
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
def _create_balance_from_isolated(
|
|
699
|
+
self, iso_pos: IsolatedPosition, is_base: bool
|
|
700
|
+
) -> BalanceWithProduct:
|
|
701
|
+
"""Create BalanceWithProduct from isolated position data."""
|
|
702
|
+
if is_base:
|
|
703
|
+
perp_balance: PerpProductBalance = iso_pos.base_balance
|
|
704
|
+
perp_product: PerpProduct = iso_pos.base_product
|
|
705
|
+
return self._create_balance_with_product(perp_balance, perp_product, "perp")
|
|
706
|
+
|
|
707
|
+
spot_balance: SpotProductBalance = iso_pos.quote_balance
|
|
708
|
+
spot_product: SpotProduct = iso_pos.quote_product
|
|
709
|
+
return self._create_balance_with_product(spot_balance, spot_product, "spot")
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def print_account_summary(summary: AccountSummary) -> None:
|
|
713
|
+
"""Print formatted account summary matching UI layout."""
|
|
714
|
+
print("\n" + "=" * 80)
|
|
715
|
+
print("MARGIN MANAGER")
|
|
716
|
+
print("=" * 80)
|
|
717
|
+
|
|
718
|
+
# Overview
|
|
719
|
+
initial_margin_used = summary.unweighted_health - summary.initial_health
|
|
720
|
+
print("\n━━━ Overview ━━━")
|
|
721
|
+
print(f"Total Equity: ${summary.portfolio_value:,.2f}")
|
|
722
|
+
print(f"Initial Margin Used: ${initial_margin_used:,.2f}")
|
|
723
|
+
print(f"Initial Margin Available: ${summary.funds_available:,.2f}")
|
|
724
|
+
print(f"Leverage: {summary.account_leverage:.2f}x")
|
|
725
|
+
|
|
726
|
+
# 1. Unified Margin Section
|
|
727
|
+
print("\n━━━ UNIFIED MARGIN ━━━")
|
|
728
|
+
print(f"Margin Usage: {summary.margin_usage_fraction * 100:.2f}%")
|
|
729
|
+
print(
|
|
730
|
+
f"Maint. Margin Usage: {summary.maint_margin_usage_fraction * 100:.2f}%"
|
|
731
|
+
)
|
|
732
|
+
print(f"Available Margin: ${summary.funds_available:,.2f}")
|
|
733
|
+
print(f"Funds Until Liquidation: ${summary.funds_until_liquidation:,.2f}")
|
|
734
|
+
|
|
735
|
+
# USDT0 Balance
|
|
736
|
+
total_unsettled = sum(pos.unsettled for pos in summary.cross_positions)
|
|
737
|
+
cash_balance = summary.total_spot_deposits - summary.total_spot_borrows
|
|
738
|
+
net_balance = cash_balance + total_unsettled
|
|
739
|
+
|
|
740
|
+
print("\n┌─ USDT0 Balance")
|
|
741
|
+
print(f"│ Cash Balance: ${cash_balance:,.2f}")
|
|
742
|
+
print(f"│ Unsettled PnL: ${total_unsettled:,.2f}")
|
|
743
|
+
print(f"│ Net Balance: ${net_balance:,.2f}")
|
|
744
|
+
print(f"│ Init. Weight / Margin: 1.00 / ${net_balance:,.2f}")
|
|
745
|
+
print(f"│ Maint. Weight / Margin: 1.00 / ${net_balance:,.2f}")
|
|
746
|
+
|
|
747
|
+
# 2. Spot Balances
|
|
748
|
+
print("\n┌─ Balances")
|
|
749
|
+
spot_shown = False
|
|
750
|
+
for spot_pos in summary.spot_positions:
|
|
751
|
+
if spot_pos.amount == 0:
|
|
752
|
+
continue
|
|
753
|
+
spot_shown = True
|
|
754
|
+
balance_type = "Deposit" if spot_pos.amount > 0 else "Borrow"
|
|
755
|
+
value = abs(spot_pos.amount * spot_pos.oracle_price)
|
|
756
|
+
|
|
757
|
+
# Use appropriate weight based on position direction
|
|
758
|
+
if spot_pos.amount > 0: # Deposit (asset)
|
|
759
|
+
init_weight = spot_pos.long_weight_initial
|
|
760
|
+
maint_weight = spot_pos.long_weight_maintenance
|
|
761
|
+
else: # Borrow (liability)
|
|
762
|
+
init_weight = spot_pos.short_weight_initial
|
|
763
|
+
maint_weight = spot_pos.short_weight_maintenance
|
|
764
|
+
|
|
765
|
+
init_margin = value * init_weight
|
|
766
|
+
maint_margin = value * maint_weight
|
|
767
|
+
|
|
768
|
+
print(f"│ Product_{spot_pos.product_id} ({balance_type})")
|
|
769
|
+
print(f"│ Balance: {abs(spot_pos.amount):,.4f}")
|
|
770
|
+
print(f"│ Value: ${value:,.2f}")
|
|
771
|
+
print(f"│ Init. Weight / Margin: {init_weight:.2f} / ${init_margin:,.2f}")
|
|
772
|
+
print(f"│ Maint. Weight / Margin: {maint_weight:.2f} / ${maint_margin:,.2f}")
|
|
773
|
+
|
|
774
|
+
if not spot_shown:
|
|
775
|
+
print("│ No spot balances")
|
|
776
|
+
|
|
777
|
+
# 3. Perps
|
|
778
|
+
print("\n┌─ Perps")
|
|
779
|
+
if summary.cross_positions:
|
|
780
|
+
for cross_pos in summary.cross_positions:
|
|
781
|
+
position_type = "Long" if cross_pos.position_size > 0 else "Short"
|
|
782
|
+
print(f"│ {cross_pos.symbol} ({position_type} / Cross)")
|
|
783
|
+
print(f"│ Position: {cross_pos.position_size:,.3f}")
|
|
784
|
+
print(f"│ Notional: ${cross_pos.notional_value:,.2f}")
|
|
785
|
+
|
|
786
|
+
if cross_pos.est_pnl is not None:
|
|
787
|
+
pnl_sign = "+" if cross_pos.est_pnl >= 0 else ""
|
|
788
|
+
print(f"│ Est. PnL: {pnl_sign}${cross_pos.est_pnl:,.2f}")
|
|
789
|
+
else:
|
|
790
|
+
print(f"│ Est. PnL: N/A")
|
|
791
|
+
|
|
792
|
+
print(f"│ Unsettled: {cross_pos.unsettled:,.2f} USDT0")
|
|
793
|
+
|
|
794
|
+
# Use correct weight based on position direction
|
|
795
|
+
if cross_pos.position_size > 0: # Long
|
|
796
|
+
init_weight = cross_pos.long_weight_initial
|
|
797
|
+
maint_weight = cross_pos.long_weight_maintenance
|
|
798
|
+
else: # Short
|
|
799
|
+
init_weight = cross_pos.short_weight_initial
|
|
800
|
+
maint_weight = cross_pos.short_weight_maintenance
|
|
801
|
+
|
|
802
|
+
# Margin = notional × |1 - weight|
|
|
803
|
+
# For longs: weight < 1, so (1 - weight) > 0
|
|
804
|
+
# For shorts: weight > 1, so (1 - weight) < 0, we need abs
|
|
805
|
+
init_margin = cross_pos.notional_value * abs(1 - init_weight)
|
|
806
|
+
maint_margin = cross_pos.notional_value * abs(1 - maint_weight)
|
|
807
|
+
|
|
808
|
+
print(
|
|
809
|
+
f"│ Init. Weight / Margin: {init_weight:.2f} / ${init_margin:,.2f}"
|
|
810
|
+
)
|
|
811
|
+
print(
|
|
812
|
+
f"│ Maint. Weight / Margin: {maint_weight:.2f} / ${maint_margin:,.2f}"
|
|
813
|
+
)
|
|
814
|
+
else:
|
|
815
|
+
print("│ No perp positions")
|
|
816
|
+
|
|
817
|
+
# Spreads
|
|
818
|
+
print("\n┌─ Spreads")
|
|
819
|
+
print("│ No spreads")
|
|
820
|
+
|
|
821
|
+
# 4. Isolated Positions
|
|
822
|
+
print("\n━━━ ISOLATED POSITIONS ━━━")
|
|
823
|
+
total_isolated_margin = sum(pos.net_margin for pos in summary.isolated_positions)
|
|
824
|
+
print(f"Total Margin in Isolated Positions: ${total_isolated_margin:,.2f}")
|
|
825
|
+
|
|
826
|
+
if summary.isolated_positions:
|
|
827
|
+
print("\n┌─ Perps")
|
|
828
|
+
for iso_pos in summary.isolated_positions:
|
|
829
|
+
position_type = "Long" if iso_pos.position_size > 0 else "Short"
|
|
830
|
+
print(f"│ {iso_pos.symbol} ({position_type} / Isolated)")
|
|
831
|
+
print(f"│ Position: {iso_pos.position_size:,.3f}")
|
|
832
|
+
print(f"│ Notional: ${iso_pos.notional_value:,.2f}")
|
|
833
|
+
print(f"│ Margin: ${iso_pos.net_margin:,.2f}")
|
|
834
|
+
print(f"│ Leverage: {iso_pos.leverage:.2f}x")
|
|
835
|
+
print(f"│ Init. Health: ${iso_pos.initial_health:,.2f}")
|
|
836
|
+
print(f"│ Maint. Health: ${iso_pos.maintenance_health:,.2f}")
|
|
837
|
+
else:
|
|
838
|
+
print("\n┌─ Perps")
|
|
839
|
+
print("│ No isolated positions")
|
|
840
|
+
|
|
841
|
+
print("\n" + "=" * 80)
|
|
@@ -47,10 +47,10 @@ nado_protocol/engine_client/types/models.py,sha256=hf9HD8rct2XZIqPznfydBq7jqKiJ3
|
|
|
47
47
|
nado_protocol/engine_client/types/query.py,sha256=N6kPuWSUowMeZnUKpxF9NnMKZBKKe-0Q_B9ccFZkjQA,12185
|
|
48
48
|
nado_protocol/engine_client/types/stream.py,sha256=F_AKqU4pClzUcM6D9Dc79f6RmraahJzj2-hQSXtQ0vQ,159
|
|
49
49
|
nado_protocol/indexer_client/__init__.py,sha256=ea2exePtguCxJsBlisOQPtnFk8hRmFugeQAuSAaPhoc,922
|
|
50
|
-
nado_protocol/indexer_client/query.py,sha256=
|
|
50
|
+
nado_protocol/indexer_client/query.py,sha256=Tm4IYmU0T_9ayFTce8SMRPi_NN8QgyHDr4rFC3h7hSw,17102
|
|
51
51
|
nado_protocol/indexer_client/types/__init__.py,sha256=r3-jxMjrFNbA1nMRSGZjsE3qypmyWab6k20_gasdwL4,3548
|
|
52
|
-
nado_protocol/indexer_client/types/models.py,sha256=
|
|
53
|
-
nado_protocol/indexer_client/types/query.py,sha256=
|
|
52
|
+
nado_protocol/indexer_client/types/models.py,sha256=vD1YHHeK15hcU5_O4al7001Fi-gTl6EARMuR-Y8stOc,7533
|
|
53
|
+
nado_protocol/indexer_client/types/query.py,sha256=JlI2TXjezTSgvaP8zzb3bO9GADzeQkay1CScO954RAw,19857
|
|
54
54
|
nado_protocol/trigger_client/__init__.py,sha256=kD_WJWGOCDwX7GvGF5VGZibWR2uPYRcWpIWht31PYR4,545
|
|
55
55
|
nado_protocol/trigger_client/execute.py,sha256=VkVla3SF6MX-ZJC_wZG72em41MPAKX-jv1_Lh4ydezU,15089
|
|
56
56
|
nado_protocol/trigger_client/query.py,sha256=7M7opYEddNo0Wf9VQ7rha-WaoFQVv5F5OI-YLSRWrpk,2705
|
|
@@ -59,13 +59,15 @@ nado_protocol/trigger_client/types/execute.py,sha256=Ij_gCl3ZzhouWF7JxEY_U6hUbe9
|
|
|
59
59
|
nado_protocol/trigger_client/types/models.py,sha256=ZVDF3MFWoR39JBaTmSOTl1WnRnw46hjX-WN_a-g6zKk,1638
|
|
60
60
|
nado_protocol/trigger_client/types/query.py,sha256=O6qhFLL2IREHc_mf-jFMyuVSzH1RuwjM-8noolLmEaQ,5288
|
|
61
61
|
nado_protocol/utils/__init__.py,sha256=heFEgVHHce8nAqQcmEMR69aZ8aaJAYXukJ2-W_KTFwY,1446
|
|
62
|
-
nado_protocol/utils/backend.py,sha256=
|
|
62
|
+
nado_protocol/utils/backend.py,sha256=aYSmXuY8fO2Xmsem1UaVkjwSBm_--IkP2nLaRFFggAk,3757
|
|
63
|
+
nado_protocol/utils/balance.py,sha256=Pf6tdcxZSn2Ou8M3XJnBIhsMB85-E7VkZDUIaMuEIbg,7435
|
|
63
64
|
nado_protocol/utils/bytes32.py,sha256=FHeHs9Sf-S7GdFTF6ikuz_sEkM-fpRW-g3kbZawlMd4,5299
|
|
64
65
|
nado_protocol/utils/enum.py,sha256=_Ij7Ai1H_Bj0OPBjmLhGvQjATXYyqD0DLqpUC-br99s,99
|
|
65
66
|
nado_protocol/utils/exceptions.py,sha256=j5U-7JEQRnMTe_nFOrF2x-9kaYKgTrPBG9XkwXVm0cI,1644
|
|
66
67
|
nado_protocol/utils/execute.py,sha256=zK3j2oTzFQlm-075thB_53iePjXpBnKF02RMEAqzl8I,11794
|
|
67
68
|
nado_protocol/utils/expiration.py,sha256=8g0KI8v-Vnoj8rr-8ik3RxmGp9fXdiVbCVGof_2CDm4,532
|
|
68
69
|
nado_protocol/utils/interest.py,sha256=pl5N2s7sux9Z0orPthdmFkldljIPwFTpLx1XC9k1oQk,2498
|
|
70
|
+
nado_protocol/utils/margin_manager.py,sha256=N5jFrcOZLLARWKi4JHwarKcxBqWsw-uPjMeOVdl20bo,31792
|
|
69
71
|
nado_protocol/utils/math.py,sha256=TyuMb9ZpwfjLh5fMrNnr_QQWSl3bVZnjODglcxT_XTI,1771
|
|
70
72
|
nado_protocol/utils/model.py,sha256=feU7cp0mSgNp-Z0dmC6VSFA6Mkjv3rNfoXdqsoZ7uGU,2087
|
|
71
73
|
nado_protocol/utils/nonce.py,sha256=DHNn5mzXdJRope8QLcDCMFe7Bk4PXLoR9VaFopfse7I,886
|
|
@@ -73,7 +75,7 @@ nado_protocol/utils/order.py,sha256=Q9TlcotvnB395dPhaKpn0EeN1WNTkpYBTUovlirr1_Y,
|
|
|
73
75
|
nado_protocol/utils/subaccount.py,sha256=WJ7lgU2RekuzJAZH-hhCTbIBlRsl2oHozBm7OEMRV74,495
|
|
74
76
|
nado_protocol/utils/time.py,sha256=tEwmrkc5VdzKLlgkJIAq2ce-nhrduJZNtVPydrrnTHs,360
|
|
75
77
|
nado_protocol/utils/twap.py,sha256=hfBVK0CBa8m4uBArxTnNRoJr3o1rJucyopR_8_9gkOo,6197
|
|
76
|
-
nado_protocol-0.
|
|
77
|
-
nado_protocol-0.
|
|
78
|
-
nado_protocol-0.
|
|
79
|
-
nado_protocol-0.
|
|
78
|
+
nado_protocol-0.2.2.dist-info/METADATA,sha256=1AfhycOzMdvGX7EfySXt4szIMP1dbl5R80mSNKXy-hI,9558
|
|
79
|
+
nado_protocol-0.2.2.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
80
|
+
nado_protocol-0.2.2.dist-info/entry_points.txt,sha256=Df0O9lFc-m0SyOh6_d9FHeG1OT-esxGm-p_z7rTT9h0,340
|
|
81
|
+
nado_protocol-0.2.2.dist-info/RECORD,,
|
|
@@ -3,6 +3,7 @@ client-sanity=sanity.nado_client:run
|
|
|
3
3
|
contracts-sanity=sanity.contracts:run
|
|
4
4
|
engine-sanity=sanity.engine_client:run
|
|
5
5
|
indexer-sanity=sanity.indexer_client:run
|
|
6
|
+
margin-sanity=sanity.margin_manager:run
|
|
6
7
|
rewards-sanity=sanity.rewards:run
|
|
7
8
|
signing-sanity=sanity.signing:run
|
|
8
9
|
test=pytest:main
|
|
File without changes
|