nado-protocol 0.1.7__tar.gz → 0.2.0__tar.gz

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.
Files changed (80) hide show
  1. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/PKG-INFO +1 -1
  2. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/indexer_client/query.py +23 -0
  3. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/indexer_client/types/models.py +1 -1
  4. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/indexer_client/types/query.py +35 -0
  5. nado_protocol-0.2.0/nado_protocol/utils/balance.py +249 -0
  6. nado_protocol-0.2.0/nado_protocol/utils/margin_manager.py +841 -0
  7. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/pyproject.toml +2 -1
  8. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/README.md +0 -0
  9. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/__init__.py +0 -0
  10. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/client/__init__.py +0 -0
  11. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/client/apis/__init__.py +0 -0
  12. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/client/apis/base.py +0 -0
  13. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/client/apis/market/__init__.py +0 -0
  14. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/client/apis/market/execute.py +0 -0
  15. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/client/apis/market/query.py +0 -0
  16. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/client/apis/perp/__init__.py +0 -0
  17. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/client/apis/perp/query.py +0 -0
  18. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/client/apis/rewards/__init__.py +0 -0
  19. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/client/apis/rewards/execute.py +0 -0
  20. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/client/apis/rewards/query.py +0 -0
  21. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/client/apis/spot/__init__.py +0 -0
  22. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/client/apis/spot/base.py +0 -0
  23. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/client/apis/spot/execute.py +0 -0
  24. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/client/apis/spot/query.py +0 -0
  25. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/client/apis/subaccount/__init__.py +0 -0
  26. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/client/apis/subaccount/execute.py +0 -0
  27. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/client/apis/subaccount/query.py +0 -0
  28. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/client/context.py +0 -0
  29. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/contracts/__init__.py +0 -0
  30. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/contracts/abis/Endpoint.json +0 -0
  31. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/contracts/abis/FQuerier.json +0 -0
  32. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/contracts/abis/IAirdrop.json +0 -0
  33. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/contracts/abis/IClearinghouse.json +0 -0
  34. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/contracts/abis/IEndpoint.json +0 -0
  35. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/contracts/abis/IFoundationRewardsAirdrop.json +0 -0
  36. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/contracts/abis/IPerpEngine.json +0 -0
  37. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/contracts/abis/IProductEngine.json +0 -0
  38. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/contracts/abis/ISpotEngine.json +0 -0
  39. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/contracts/abis/IStaking.json +0 -0
  40. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/contracts/abis/MockERC20.json +0 -0
  41. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/contracts/deployments/deployment.testing.json +0 -0
  42. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/contracts/deployments/deployment.testnet.json +0 -0
  43. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/contracts/eip712/__init__.py +0 -0
  44. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/contracts/eip712/domain.py +0 -0
  45. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/contracts/eip712/sign.py +0 -0
  46. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/contracts/eip712/types.py +0 -0
  47. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/contracts/loader.py +0 -0
  48. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/contracts/types.py +0 -0
  49. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/engine_client/__init__.py +0 -0
  50. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/engine_client/execute.py +0 -0
  51. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/engine_client/query.py +0 -0
  52. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/engine_client/types/__init__.py +0 -0
  53. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/engine_client/types/execute.py +0 -0
  54. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/engine_client/types/models.py +0 -0
  55. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/engine_client/types/query.py +0 -0
  56. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/engine_client/types/stream.py +0 -0
  57. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/indexer_client/__init__.py +0 -0
  58. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/indexer_client/types/__init__.py +0 -0
  59. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/trigger_client/__init__.py +0 -0
  60. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/trigger_client/execute.py +0 -0
  61. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/trigger_client/query.py +0 -0
  62. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/trigger_client/types/__init__.py +0 -0
  63. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/trigger_client/types/execute.py +0 -0
  64. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/trigger_client/types/models.py +0 -0
  65. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/trigger_client/types/query.py +0 -0
  66. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/utils/__init__.py +0 -0
  67. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/utils/backend.py +0 -0
  68. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/utils/bytes32.py +0 -0
  69. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/utils/enum.py +0 -0
  70. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/utils/exceptions.py +0 -0
  71. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/utils/execute.py +0 -0
  72. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/utils/expiration.py +0 -0
  73. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/utils/interest.py +0 -0
  74. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/utils/math.py +0 -0
  75. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/utils/model.py +0 -0
  76. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/utils/nonce.py +0 -0
  77. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/utils/order.py +0 -0
  78. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/utils/subaccount.py +0 -0
  79. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/utils/time.py +0 -0
  80. {nado_protocol-0.1.7 → nado_protocol-0.2.0}/nado_protocol/utils/twap.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nado-protocol
3
- Version: 0.1.7
3
+ Version: 0.2.0
4
4
  Summary: Nado Protocol SDK
5
5
  Keywords: nado protocol,nado sdk,nado protocol api
6
6
  Author: Jeury Mejia
@@ -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
- cumulative_volume: str
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)]
@@ -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