ddx-python 1.0.5__cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.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.
Files changed (104) hide show
  1. ddx/.gitignore +1 -0
  2. ddx/__init__.py +58 -0
  3. ddx/_rust/__init__.pyi +2009 -0
  4. ddx/_rust/common/__init__.pyi +17 -0
  5. ddx/_rust/common/accounting.pyi +6 -0
  6. ddx/_rust/common/enums.pyi +3 -0
  7. ddx/_rust/common/requests/__init__.pyi +21 -0
  8. ddx/_rust/common/requests/intents.pyi +19 -0
  9. ddx/_rust/common/specs.pyi +17 -0
  10. ddx/_rust/common/state/__init__.pyi +41 -0
  11. ddx/_rust/common/state/keys.pyi +29 -0
  12. ddx/_rust/common/transactions.pyi +7 -0
  13. ddx/_rust/decimal.pyi +3 -0
  14. ddx/_rust/h256.pyi +3 -0
  15. ddx/_rust.abi3.so +0 -0
  16. ddx/app_config/ethereum/addresses.json +541 -0
  17. ddx/auditor/README.md +32 -0
  18. ddx/auditor/__init__.py +0 -0
  19. ddx/auditor/auditor_driver.py +1034 -0
  20. ddx/auditor/websocket_message.py +54 -0
  21. ddx/common/__init__.py +0 -0
  22. ddx/common/epoch_params.py +28 -0
  23. ddx/common/fill_context.py +144 -0
  24. ddx/common/item_utils.py +38 -0
  25. ddx/common/logging.py +184 -0
  26. ddx/common/market_specs.py +64 -0
  27. ddx/common/trade_mining_params.py +19 -0
  28. ddx/common/transaction_utils.py +85 -0
  29. ddx/common/transactions/__init__.py +0 -0
  30. ddx/common/transactions/advance_epoch.py +91 -0
  31. ddx/common/transactions/advance_settlement_epoch.py +63 -0
  32. ddx/common/transactions/all_price_checkpoints.py +84 -0
  33. ddx/common/transactions/cancel.py +76 -0
  34. ddx/common/transactions/cancel_all.py +88 -0
  35. ddx/common/transactions/complete_fill.py +103 -0
  36. ddx/common/transactions/disaster_recovery.py +97 -0
  37. ddx/common/transactions/event.py +48 -0
  38. ddx/common/transactions/fee_distribution.py +119 -0
  39. ddx/common/transactions/funding.py +294 -0
  40. ddx/common/transactions/futures_expiry.py +123 -0
  41. ddx/common/transactions/genesis.py +108 -0
  42. ddx/common/transactions/inner/__init__.py +0 -0
  43. ddx/common/transactions/inner/adl_outcome.py +25 -0
  44. ddx/common/transactions/inner/fill.py +227 -0
  45. ddx/common/transactions/inner/liquidated_position.py +41 -0
  46. ddx/common/transactions/inner/liquidation_entry.py +41 -0
  47. ddx/common/transactions/inner/liquidation_fill.py +118 -0
  48. ddx/common/transactions/inner/outcome.py +32 -0
  49. ddx/common/transactions/inner/trade_fill.py +125 -0
  50. ddx/common/transactions/insurance_fund_update.py +142 -0
  51. ddx/common/transactions/insurance_fund_withdraw.py +99 -0
  52. ddx/common/transactions/liquidation.py +357 -0
  53. ddx/common/transactions/partial_fill.py +125 -0
  54. ddx/common/transactions/pnl_realization.py +122 -0
  55. ddx/common/transactions/post.py +72 -0
  56. ddx/common/transactions/post_order.py +95 -0
  57. ddx/common/transactions/price_checkpoint.py +96 -0
  58. ddx/common/transactions/signer_registered.py +62 -0
  59. ddx/common/transactions/specs_update.py +61 -0
  60. ddx/common/transactions/strategy_update.py +156 -0
  61. ddx/common/transactions/tradable_product_update.py +98 -0
  62. ddx/common/transactions/trade_mining.py +147 -0
  63. ddx/common/transactions/trader_update.py +105 -0
  64. ddx/common/transactions/withdraw.py +91 -0
  65. ddx/common/transactions/withdraw_ddx.py +74 -0
  66. ddx/common/utils.py +176 -0
  67. ddx/config.py +17 -0
  68. ddx/derivadex_client.py +254 -0
  69. ddx/py.typed +0 -0
  70. ddx/realtime_client/__init__.py +2 -0
  71. ddx/realtime_client/config.py +2 -0
  72. ddx/realtime_client/logs/pytest.log +0 -0
  73. ddx/realtime_client/models/__init__.py +683 -0
  74. ddx/realtime_client/realtime_client.py +567 -0
  75. ddx/rest_client/__init__.py +0 -0
  76. ddx/rest_client/clients/__init__.py +0 -0
  77. ddx/rest_client/clients/base_client.py +60 -0
  78. ddx/rest_client/clients/market_client.py +1241 -0
  79. ddx/rest_client/clients/on_chain_client.py +432 -0
  80. ddx/rest_client/clients/signed_client.py +301 -0
  81. ddx/rest_client/clients/system_client.py +843 -0
  82. ddx/rest_client/clients/trade_client.py +335 -0
  83. ddx/rest_client/constants/__init__.py +0 -0
  84. ddx/rest_client/constants/endpoints.py +67 -0
  85. ddx/rest_client/contracts/__init__.py +0 -0
  86. ddx/rest_client/contracts/checkpoint/__init__.py +560 -0
  87. ddx/rest_client/contracts/ddx/__init__.py +1949 -0
  88. ddx/rest_client/contracts/dummy_token/__init__.py +1014 -0
  89. ddx/rest_client/contracts/i_collateral/__init__.py +1414 -0
  90. ddx/rest_client/contracts/i_stake/__init__.py +696 -0
  91. ddx/rest_client/exceptions/__init__.py +0 -0
  92. ddx/rest_client/exceptions/exceptions.py +32 -0
  93. ddx/rest_client/http/__init__.py +0 -0
  94. ddx/rest_client/http/http_client.py +305 -0
  95. ddx/rest_client/models/__init__.py +0 -0
  96. ddx/rest_client/models/market.py +683 -0
  97. ddx/rest_client/models/signed.py +60 -0
  98. ddx/rest_client/models/system.py +390 -0
  99. ddx/rest_client/models/trade.py +140 -0
  100. ddx/rest_client/utils/__init__.py +0 -0
  101. ddx/rest_client/utils/encryption_utils.py +26 -0
  102. ddx_python-1.0.5.dist-info/METADATA +63 -0
  103. ddx_python-1.0.5.dist-info/RECORD +104 -0
  104. ddx_python-1.0.5.dist-info/WHEEL +4 -0
@@ -0,0 +1,683 @@
1
+ from datetime import datetime
2
+ from enum import IntEnum
3
+ from typing import List, Optional, Dict
4
+ from pydantic import BaseModel, Field, ConfigDict, field_validator
5
+ from pydantic.alias_generators import to_camel
6
+ from dateutil.parser import parse
7
+ from dataclasses import dataclass
8
+ from itertools import groupby
9
+ from operator import attrgetter
10
+
11
+ from ddx._rust.decimal import Decimal
12
+
13
+
14
+ def parse_datetime_string(value):
15
+ """Utility function to parse datetime strings."""
16
+ if isinstance(value, str):
17
+ return parse(value)
18
+ return value
19
+
20
+
21
+ # Enums
22
+ class OrderSide(IntEnum):
23
+ """Order side enum."""
24
+
25
+ BID = 0
26
+ ASK = 1
27
+
28
+
29
+ class OrderType(IntEnum):
30
+ """Order type enum."""
31
+
32
+ LIMIT = 0
33
+ MARKET = 1
34
+ STOP = 2
35
+ LIMIT_POST_ONLY = 3
36
+
37
+
38
+ class OrderUpdateReason(IntEnum):
39
+ """Order update reason enum."""
40
+
41
+ TRADE = 0
42
+ LIQUIDATION = 1
43
+ CANCEL = 2
44
+ ORDER_REJECTION = 3
45
+ CANCEL_REJECTION = 4
46
+
47
+
48
+ class StrategyUpdateReason(IntEnum):
49
+ """Strategy update reason enum."""
50
+
51
+ DEPOSIT = 0
52
+ WITHDRAW = 1
53
+ WITHDRAW_INTENT = 2
54
+ FUNDING_PAYMENT = 3
55
+ REALIZED_PNL = 4
56
+ LIQUIDATION = 5
57
+ ADL = 6
58
+ WITHDRAW_REJECTION = 7
59
+
60
+
61
+ class TraderUpdateReason(IntEnum):
62
+ """Trader update reason enum."""
63
+
64
+ DEPOSIT = 0
65
+ WITHDRAW_DDX = 1
66
+ WITHDRAW_DDX_INTENT = 2
67
+ TRADE_MINING_REWARD = 3
68
+ PROFILE_UPDATE = 4
69
+ FEE_DISTRIBUTION = 5
70
+ WITHDRAW_DDX_REJECTION = 6
71
+
72
+
73
+ # Mark Price History Models
74
+ class MarkPrice(BaseModel):
75
+ """Mark price data model."""
76
+
77
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
78
+
79
+ global_ordinal: int
80
+ epoch_id: int
81
+ symbol: str
82
+ price: str
83
+ funding_rate: str
84
+ created_at: datetime
85
+
86
+ @field_validator("created_at", mode="before")
87
+ @classmethod
88
+ def parse_datetime_field(cls, value):
89
+ return parse_datetime_string(value)
90
+
91
+
92
+ class MarkPriceHistoryResponse(BaseModel):
93
+ """Response model for mark price history endpoint."""
94
+
95
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
96
+
97
+ next_global_ordinal: Optional[int] = None
98
+ value: List[MarkPrice]
99
+ success: bool
100
+ timestamp: int
101
+
102
+
103
+ # Order Book L3 Models
104
+ class OrderBookL3(BaseModel):
105
+ """L3 order book entry model."""
106
+
107
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
108
+
109
+ book_ordinal: int
110
+ order_hash: str
111
+ symbol: str
112
+ side: int = Field(..., description="0: Bid, 1: Ask")
113
+ original_amount: str
114
+ amount: str
115
+ price: str
116
+ trader_address: str
117
+ strategy_id_hash: str
118
+
119
+
120
+ class OrderBookL3Response(BaseModel):
121
+ """Response model for L3 order book endpoint."""
122
+
123
+ model_config = ConfigDict(populate_by_name=True)
124
+
125
+ value: List[OrderBookL3]
126
+ success: bool
127
+ timestamp: int
128
+
129
+
130
+ # Order Update History Models
131
+ class OrderIntent(BaseModel):
132
+ """Order intent data model."""
133
+
134
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
135
+
136
+ epoch_id: int
137
+ order_hash: str
138
+ symbol: str
139
+ side: int = Field(..., description="0: Bid, 1: Ask")
140
+ amount: str
141
+ price: str
142
+ trader_address: str
143
+ strategy_id_hash: str
144
+ order_type: int = Field(
145
+ ..., description="0: Limit, 1: Market, 2: Stop, 3: LimitPostOnly"
146
+ )
147
+ stop_price: str
148
+ nonce: str
149
+ signature: str
150
+ created_at: datetime
151
+
152
+ @field_validator("created_at", mode="before")
153
+ @classmethod
154
+ def parse_datetime_field(cls, value):
155
+ return parse_datetime_string(value)
156
+
157
+
158
+ class OrderUpdate(BaseModel):
159
+ """Order update data model."""
160
+
161
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
162
+
163
+ global_ordinal: int
164
+ epoch_id: int
165
+ tx_ordinal: Optional[int] = None
166
+ ordinal: Optional[int] = None
167
+ order_rejection: Optional[int] = Field(
168
+ None, description="0-5: only applicable when rejection"
169
+ )
170
+ cancel_rejection: Optional[int] = None
171
+ reason: int = Field(
172
+ ...,
173
+ description="0: Trade, 1: Liquidation, 2: Cancel, 3: Order Rejection, 4: Cancel Rejection",
174
+ )
175
+ amount: str
176
+ quote_asset_amount: Optional[str] = None
177
+ symbol: str
178
+ price: Optional[str] = None
179
+ maker_fee_collateral: Optional[str] = None
180
+ maker_fee_ddx: Optional[str] = None
181
+ maker_realized_pnl: Optional[str] = None
182
+ taker_order_intent: Optional[OrderIntent] = None
183
+ taker_fee_collateral: Optional[str] = None
184
+ taker_fee_ddx: Optional[str] = None
185
+ taker_realized_pnl: Optional[str] = None
186
+ liquidated_trader_address: Optional[str] = None
187
+ liquidated_strategy_id_hash: Optional[str] = None
188
+ maker_order_intent: OrderIntent
189
+ created_at: datetime
190
+
191
+ @field_validator("created_at", mode="before")
192
+ @classmethod
193
+ def parse_datetime_field(cls, value):
194
+ return parse_datetime_string(value)
195
+
196
+
197
+ class OrderUpdateHistoryResponse(BaseModel):
198
+ """Response model for order update history endpoint."""
199
+
200
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
201
+
202
+ next_global_ordinal: Optional[int] = None
203
+ value: List[OrderUpdate]
204
+ success: bool
205
+ timestamp: int
206
+
207
+
208
+ # Strategy Update History Models
209
+ class StrategyPosition(BaseModel):
210
+ """Strategy position data within a strategy update."""
211
+
212
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
213
+
214
+ symbol: str
215
+ side: Optional[int] = Field(
216
+ None, description="0: Bid, 1: Ask. Only present on ADL strategy update"
217
+ )
218
+ avg_entry_price: Optional[str] = Field(
219
+ None, description="New average entry price after RealizedPnl strategy update"
220
+ )
221
+ realized_pnl: str = Field(
222
+ ..., description="Realized PnL after RealizedPnl or ADL strategy update"
223
+ )
224
+
225
+
226
+ class StrategyUpdate(BaseModel):
227
+ """Strategy update data model."""
228
+
229
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
230
+
231
+ global_ordinal: int
232
+ epoch_id: int
233
+ tx_ordinal: Optional[int] = None
234
+ ordinal: Optional[int] = None
235
+ withdraw_rejection: Optional[int] = Field(
236
+ None, description="0-4: Withdraw rejection type. Only present on rejections"
237
+ )
238
+ reason: int = Field(
239
+ ...,
240
+ description="0: Deposit, 1: Withdraw, 2: WithdrawIntent, 3: FundingPayment, 4: RealizedPnl, 5: Liquidation, 6: ADL, 7: Withdraw Rejection",
241
+ )
242
+ trader_address: str
243
+ strategy_id_hash: str
244
+ collateral_address: Optional[str] = None
245
+ collateral_symbol: Optional[str] = None
246
+ amount: Optional[str] = None
247
+ new_avail_collateral: Optional[str] = Field(
248
+ None, description="Not present on ADL reason"
249
+ )
250
+ new_locked_collateral: Optional[str] = Field(
251
+ None, description="Not present on ADL reason"
252
+ )
253
+ block_number: Optional[int] = Field(None, description="Not present on ADL reason")
254
+ tx_hash: Optional[str] = None
255
+ positions: Optional[List[StrategyPosition]] = None
256
+ created_at: datetime
257
+
258
+ @field_validator("created_at", mode="before")
259
+ @classmethod
260
+ def parse_datetime_field(cls, value):
261
+ return parse_datetime_string(value)
262
+
263
+
264
+ class StrategyUpdateHistoryResponse(BaseModel):
265
+ """Response model for strategy update history endpoint."""
266
+
267
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
268
+
269
+ next_global_ordinal: Optional[int] = None
270
+ value: List[StrategyUpdate]
271
+ success: bool
272
+ timestamp: int
273
+
274
+
275
+ # Ticker Models
276
+ class Ticker(BaseModel):
277
+ """Market ticker data model."""
278
+
279
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
280
+
281
+ symbol: str
282
+ high_price_24h: str
283
+ low_price_24h: str
284
+ prev_price_24h: str
285
+ last_price: str
286
+ mark_price: str
287
+ index_price: str
288
+ next_funding_time: datetime
289
+ volume_24h: str
290
+ amount_24h: Optional[str] = None
291
+ funding_rate: str
292
+ open_interest: str
293
+ open_interest_value: str
294
+
295
+ @field_validator("next_funding_time", mode="before")
296
+ @classmethod
297
+ def parse_datetime_field(cls, value):
298
+ return parse_datetime_string(value)
299
+
300
+
301
+ class TickersResponse(BaseModel):
302
+ """Response model for tickers endpoint."""
303
+
304
+ model_config = ConfigDict(populate_by_name=True)
305
+
306
+ value: List[Ticker]
307
+ success: bool
308
+ timestamp: int
309
+
310
+
311
+ # Trader Update History Models
312
+ class TraderUpdate(BaseModel):
313
+ """Trader update data model."""
314
+
315
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
316
+
317
+ global_ordinal: int
318
+ epoch_id: int
319
+ tx_ordinal: Optional[int] = None
320
+ ordinal: Optional[int] = None
321
+ withdraw_ddx_rejection: Optional[int] = Field(
322
+ None, description="0-1: only present on rejections"
323
+ )
324
+ reason: int = Field(
325
+ ...,
326
+ description="0: Deposit, 1: WithdrawDDX, 2: WithdrawDDXIntent, 3: TradeMiningReward, 4: ProfileUpdate, 5: FeeDistribution, 6: WithdrawDDXRejection",
327
+ )
328
+ trader_address: str
329
+ amount: Optional[str] = None
330
+ new_avail_ddx_balance: Optional[str] = None
331
+ new_locked_ddx_balance: Optional[str] = None
332
+ pay_fees_in_ddx: Optional[bool] = None
333
+ block_number: Optional[int] = None
334
+ tx_hash: Optional[str] = None
335
+ created_at: datetime
336
+
337
+ @field_validator("created_at", mode="before")
338
+ @classmethod
339
+ def parse_datetime_field(cls, value):
340
+ return parse_datetime_string(value)
341
+
342
+
343
+ class TraderUpdateHistoryResponse(BaseModel):
344
+ """Response model for trader update history endpoint."""
345
+
346
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
347
+
348
+ next_global_ordinal: Optional[int] = None
349
+ value: List[TraderUpdate]
350
+ success: bool
351
+ timestamp: int
352
+
353
+
354
+ # Balance Aggregation Models
355
+ class BalanceAggregation(BaseModel):
356
+ """Balance aggregation data model."""
357
+
358
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
359
+
360
+ trader: str
361
+ strategy_id_hash: str
362
+ amount: str
363
+ timestamp: int
364
+
365
+
366
+ class BalanceAggregationResponse(BaseModel):
367
+ """Response model for balance aggregation endpoint."""
368
+
369
+ model_config = ConfigDict(populate_by_name=True)
370
+
371
+ value: List[BalanceAggregation]
372
+ success: bool
373
+ timestamp: int
374
+
375
+
376
+ # Fees Aggregation Models
377
+ class FeesAggregation(BaseModel):
378
+ """Fees aggregation data model."""
379
+
380
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
381
+
382
+ timestamp: int
383
+
384
+ def get_fees_value(self, fee_symbol: str) -> Optional[str]:
385
+ """Get fees value for a specific fee symbol (e.g., 'USDC', 'DDX')."""
386
+ return getattr(self, f"fees_{fee_symbol}", None)
387
+
388
+
389
+ class FeesAggregationResponse(BaseModel):
390
+ """Response model for fees aggregation endpoint."""
391
+
392
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
393
+
394
+ next_lookback_timestamp: Optional[int] = None
395
+ value: List[FeesAggregation]
396
+ success: bool
397
+ timestamp: int
398
+
399
+
400
+ # Funding Rate Comparison Models
401
+ class FundingRateComparison(BaseModel):
402
+ """Funding rate comparison data model."""
403
+
404
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
405
+
406
+ symbol: str
407
+ derivadex_funding_rate: float
408
+ binance_funding_rate: float
409
+ derivadex_binance_arbitrage: float
410
+ bybit_funding_rate: float
411
+ derivadex_bybit_arbitrage: float
412
+ hyperliquid_funding_rate: float
413
+ derivadex_hyperliquid_arbitrage: float
414
+
415
+
416
+ class FundingRateComparisonResponse(BaseModel):
417
+ """Response model for funding rate comparison aggregation endpoint."""
418
+
419
+ model_config = ConfigDict(populate_by_name=True)
420
+
421
+ value: List[FundingRateComparison]
422
+ success: bool
423
+ timestamp: int
424
+
425
+
426
+ # Top Traders Models
427
+ class TopTrader(BaseModel):
428
+ """Top trader data model."""
429
+
430
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
431
+
432
+ trader: str
433
+ volume: Optional[str] = None
434
+ realized_pnl: Optional[str] = None
435
+ account_value: Optional[str] = None
436
+
437
+
438
+ class TopTradersAggregationResponse(BaseModel):
439
+ """Response model for top traders aggregation endpoint."""
440
+
441
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
442
+
443
+ value: List[TopTrader]
444
+ next_cursor: Optional[int] = None
445
+ success: bool
446
+ timestamp: int
447
+
448
+
449
+ # Volume Aggregation Models
450
+ class VolumeAggregation(BaseModel):
451
+ """Volume aggregation data model."""
452
+
453
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
454
+
455
+ timestamp: int
456
+
457
+ def get_volume_value(self, field_name: str) -> Optional[str]:
458
+ """Get value for a specific volume field."""
459
+ return getattr(self, f"volume_{field_name}", None)
460
+
461
+
462
+ class VolumeAggregationResponse(BaseModel):
463
+ """Response model for volume aggregation endpoint."""
464
+
465
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
466
+
467
+ next_lookback_timestamp: Optional[int] = None
468
+ value: List[VolumeAggregation]
469
+ success: bool
470
+ timestamp: int
471
+
472
+
473
+ # Funding Rate History Models
474
+ class FundingRateHistory(BaseModel):
475
+ """Funding rate history data model."""
476
+
477
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
478
+
479
+ epoch_id: int
480
+ tx_ordinal: int
481
+ symbol: str
482
+ funding_rate: str
483
+ created_at: datetime
484
+
485
+ @field_validator("created_at", mode="before")
486
+ @classmethod
487
+ def parse_datetime_field(cls, value):
488
+ return parse_datetime_string(value)
489
+
490
+
491
+ class FundingRateHistoryResponse(BaseModel):
492
+ """Response model for funding rate history endpoint."""
493
+
494
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
495
+
496
+ next_epoch: Optional[int] = None
497
+ next_tx_ordinal: Optional[int] = None
498
+ next_ordinal: Optional[int] = None
499
+ value: List[FundingRateHistory]
500
+ success: bool
501
+ timestamp: int
502
+
503
+
504
+ # Open Interest History Models
505
+ class OpenInterestHistory(BaseModel):
506
+ """Open interest history data model."""
507
+
508
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
509
+
510
+ symbol: str
511
+ amount: str
512
+ created_at: datetime
513
+
514
+ @field_validator("created_at", mode="before")
515
+ @classmethod
516
+ def parse_datetime_field(cls, value):
517
+ return parse_datetime_string(value)
518
+
519
+
520
+ class OpenInterestHistoryResponse(BaseModel):
521
+ """Response model for open interest history endpoint."""
522
+
523
+ model_config = ConfigDict(populate_by_name=True)
524
+
525
+ value: List[OpenInterestHistory]
526
+ success: bool
527
+ timestamp: int
528
+
529
+
530
+ # Order Book L2 Models
531
+ class OrderBookL2Item(BaseModel):
532
+ """L2 order book item model."""
533
+
534
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
535
+
536
+ symbol: str
537
+ side: int = Field(..., description="0: Bid, 1: Ask")
538
+ amount: str
539
+ price: str
540
+
541
+
542
+ class OrderBookL2Response(BaseModel):
543
+ """Response model for L2 order book endpoint."""
544
+
545
+ model_config = ConfigDict(populate_by_name=True)
546
+
547
+ value: List[OrderBookL2Item]
548
+ success: bool
549
+ timestamp: int
550
+
551
+
552
+ @dataclass
553
+ class OrderBook:
554
+ """
555
+ Complete order book for a single market.
556
+
557
+ Attributes
558
+ ----------
559
+ symbol : str
560
+ The symbol of the market
561
+ bids : List[OrderBookL2Item]
562
+ List of bid orders, sorted by price (descending)
563
+ asks : List[OrderBookL2Item]
564
+ List of ask orders, sorted by price (ascending)
565
+ timestamp : int
566
+ Timestamp of the order book snapshot
567
+ """
568
+
569
+ symbol: str
570
+ bids: List[OrderBookL2Item]
571
+ asks: List[OrderBookL2Item]
572
+ timestamp: int
573
+
574
+ @classmethod
575
+ def from_order_book_l2_items(
576
+ cls, symbol: str, order_book_l2_items: List[OrderBookL2Item], timestamp: int
577
+ ) -> "OrderBook":
578
+ """
579
+ Create instance from a list of entries for a single symbol.
580
+
581
+ Parameters
582
+ ----------
583
+ symbol : str
584
+ The market symbol
585
+ order_book_l2_items : List[OrderBookL2Item]
586
+ List of order book entries for this symbol
587
+ timestamp : int
588
+ Timestamp of the order book snapshot
589
+
590
+ Returns
591
+ -------
592
+ OrderBook
593
+ Initialized instance
594
+ """
595
+ # Filter and sort bids (descending by price)
596
+ bids = [e for e in order_book_l2_items if e.side == OrderSide.BID]
597
+ bids.sort(key=lambda x: Decimal(x.price), reverse=True)
598
+
599
+ # Filter and sort asks (ascending by price)
600
+ asks = [e for e in order_book_l2_items if e.side == OrderSide.ASK]
601
+ asks.sort(key=lambda x: Decimal(x.price))
602
+
603
+ return cls(symbol=symbol, bids=bids, asks=asks, timestamp=timestamp)
604
+
605
+ @classmethod
606
+ def from_response(
607
+ cls, response: OrderBookL2Response, symbol: Optional[str] = None
608
+ ) -> Dict[str, "OrderBook"]:
609
+ """
610
+ Create OrderBook instance(s) from response data.
611
+
612
+ Parameters
613
+ ----------
614
+ response : OrderBookL2Response
615
+ Parsed response from the API
616
+ symbol : Optional[str]
617
+ If provided, only return order book for this symbol
618
+
619
+ Returns
620
+ -------
621
+ Dict[str, OrderBook]
622
+ Dictionary mapping symbols to their respective order books
623
+ """
624
+ if not response.value:
625
+ return {}
626
+
627
+ # If specific symbol requested, filter first
628
+ items = response.value
629
+ if symbol:
630
+ items = [item for item in items if item.symbol == symbol]
631
+ if not items:
632
+ return {}
633
+
634
+ # Group entries by symbol
635
+ items_sorted = sorted(items, key=attrgetter("symbol"))
636
+ grouped = groupby(items_sorted, key=attrgetter("symbol"))
637
+
638
+ # Create order books for each symbol
639
+ order_books = {}
640
+ for sym, entries in grouped:
641
+ entry_list = list(entries)
642
+ if entry_list: # Only create order book if there are entries
643
+ order_books[sym] = cls.from_order_book_l2_items(
644
+ sym, entry_list, response.timestamp
645
+ )
646
+
647
+ return order_books
648
+
649
+
650
+ # Price Checkpoint History Models
651
+ class PriceCheckpoint(BaseModel):
652
+ """Price checkpoint data model."""
653
+
654
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
655
+
656
+ epoch_id: int
657
+ tx_ordinal: int
658
+ index_price_hash: str
659
+ symbol: str
660
+ index_price: str
661
+ mark_price: str
662
+ time: str
663
+ ema: Optional[str] = None
664
+ price_ordinal: int
665
+ created_at: datetime
666
+
667
+ @field_validator("created_at", mode="before")
668
+ @classmethod
669
+ def parse_datetime_field(cls, value):
670
+ return parse_datetime_string(value)
671
+
672
+
673
+ class PriceCheckpointHistoryResponse(BaseModel):
674
+ """Response model for price checkpoint history endpoint."""
675
+
676
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
677
+
678
+ next_epoch: Optional[int] = None
679
+ next_tx_ordinal: Optional[int] = None
680
+ next_ordinal: Optional[int] = None
681
+ value: List[PriceCheckpoint]
682
+ success: bool
683
+ timestamp: int