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,54 @@
1
+ """
2
+ WebsocketMessage module
3
+ """
4
+
5
+ from enum import Enum
6
+ from typing import Dict, Union
7
+
8
+ from attrs import define
9
+
10
+
11
+ class WebsocketMessageType(str, Enum):
12
+ GET = "Get"
13
+ SUBSCRIBE = "Subscribe"
14
+ REQUEST = "Request"
15
+ INFO = "Info"
16
+ SEQUENCED = "Sequenced"
17
+ SAFETY_FAILURE = "SafetyFailure"
18
+
19
+
20
+ class WebsocketEventType(str, Enum):
21
+ PARTIAL = "Partial"
22
+ UPDATE = "Update"
23
+ SNAPSHOT = "Snapshot"
24
+ HEAD = "Head"
25
+ TAIL = "Tail"
26
+
27
+
28
+ @define
29
+ class WebsocketMessage:
30
+ """
31
+ Defines a WebsocketMessage.
32
+ """
33
+
34
+ message_type: Union[WebsocketMessageType, str]
35
+ message_content: str
36
+
37
+ @classmethod
38
+ def decode_value_into_cls(cls, raw_websocket_message: Dict):
39
+ """
40
+ Decode a raw websocket message into class
41
+
42
+ Parameters
43
+ ----------
44
+ raw_websocket_message : Dict
45
+ Raw websocket message
46
+ """
47
+
48
+ return cls(
49
+ raw_websocket_message["t"],
50
+ raw_websocket_message["c"],
51
+ )
52
+
53
+ def repr_json(self):
54
+ return {"t": self.message_type, "c": self.message_content}
ddx/common/__init__.py ADDED
File without changes
@@ -0,0 +1,28 @@
1
+ from attrs import define
2
+ from ddx._rust.common import ProductSymbol
3
+ from ddx._rust.common.requests import SettlementAction
4
+
5
+
6
+ @define
7
+ class EpochParams:
8
+ """
9
+ Defines the epoch parameters
10
+ """
11
+
12
+ epoch_size: int
13
+ price_checkpoint_size: int
14
+ settlement_epoch_length: int
15
+ pnl_realization_period: int
16
+ funding_period: int
17
+ trade_mining_period: int
18
+ expiry_price_leaves_duration: int
19
+
20
+ @property
21
+ def settlement_action_periods(self):
22
+ res = {
23
+ SettlementAction.TradeMining: self.trade_mining_period,
24
+ SettlementAction.PnlRealization: self.pnl_realization_period,
25
+ SettlementAction.FundingDistribution: self.funding_period,
26
+ }
27
+
28
+ return res
@@ -0,0 +1,144 @@
1
+ """
2
+ Fill module.
3
+ """
4
+
5
+ import logging
6
+ from typing import Optional
7
+
8
+ from attrs import define, field
9
+ from ddx.common.item_utils import update_avail_collateral
10
+ from ddx.common.transactions.inner.outcome import Outcome
11
+ from ddx._rust.common import TokenSymbol
12
+ from ddx._rust.common.enums import OrderSide, PositionSide, TradeSide
13
+ from ddx._rust.common.state import Position, Strategy, Trader
14
+ from ddx._rust.decimal import Decimal
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ MAX_DDX_PRICE_CHECKPOINT_AGE_IN_TICKS = 40000
19
+ DDX_FEE_DISCOUNT = 0.5
20
+
21
+
22
+ def apply_trade(
23
+ position: Position, amount: Decimal, price: Decimal, side: OrderSide
24
+ ) -> (Position, Decimal):
25
+ logger.debug(
26
+ f"Applying trade of {amount} at {price} on {side} to position {position}"
27
+ )
28
+ if side == OrderSide.Bid:
29
+ if position.side == PositionSide.Long:
30
+ logger.info("Trade side matches position side, increasing position balance")
31
+ return position.increase(price, amount)
32
+ if position.side == PositionSide.Short:
33
+ logger.info(
34
+ "Trade side does not match position side, decreasing position balance"
35
+ )
36
+ if amount > position.balance:
37
+ return position.cross_over(price, amount)
38
+ return position.decrease(price, amount)
39
+ logger.info(
40
+ "Position side not set, setting to Long and increasing position balance"
41
+ )
42
+ position.side = PositionSide.Long
43
+ return position.increase(price, amount)
44
+ else:
45
+ if position.side == PositionSide.Short:
46
+ logger.info("Trade side matches position side, increasing position balance")
47
+ return position.increase(price, amount)
48
+ if position.side == PositionSide.Long:
49
+ logger.info(
50
+ "Trade side does not match position side, decreasing position balance"
51
+ )
52
+ if amount > position.balance:
53
+ return position.cross_over(price, amount)
54
+ return position.decrease(price, amount)
55
+ logger.info(
56
+ "Position side not set, setting to Short and increasing position balance"
57
+ )
58
+ position.side = PositionSide.Short
59
+ return position.increase(price, amount)
60
+
61
+
62
+ @define
63
+ class FillContext:
64
+ """
65
+ Defines a FillContext.
66
+ """
67
+
68
+ outcome: Outcome
69
+ realized_pnl: Optional[Decimal] = field(init=False)
70
+
71
+ def apply_fill(
72
+ self,
73
+ position: Optional[Position],
74
+ side: OrderSide,
75
+ trade_side: TradeSide,
76
+ fill_amount: Decimal,
77
+ fill_price: Decimal,
78
+ ) -> Position:
79
+ if position is None:
80
+ position = Position(
81
+ PositionSide.Long if side == OrderSide.Bid else PositionSide.Short,
82
+ Decimal("0"),
83
+ Decimal("0"),
84
+ )
85
+ logger.info(f"New {position.side} position")
86
+ old_balance = position.balance
87
+
88
+ fee = trade_side.trading_fee(fill_amount, fill_price)
89
+ updated_position, realized_pnl = apply_trade(
90
+ position, fill_amount, fill_price, side
91
+ )
92
+
93
+ self.realized_pnl = realized_pnl
94
+ logger.info(f"Realized pnl: {self.realized_pnl}")
95
+ logger.info(f"Fee: {fee}")
96
+
97
+ # Note that, again, we're never reading the fee from the txlog, and instead
98
+ # we calulate it from the fill amount and price and set it in the outcome.
99
+ self.outcome.fee = fee
100
+
101
+ return updated_position
102
+
103
+ def apply_ddx_fee_and_mutate_trader(
104
+ self,
105
+ trader: Trader,
106
+ ddx_price: Decimal,
107
+ ) -> bool:
108
+ if self.outcome.fee == Decimal("0"):
109
+ logger.info("Base fee of 0, no fees to pay")
110
+ return False
111
+ fee_in_ddx = (self.outcome.fee / ddx_price) * (Decimal("1") - DDX_FEE_DISCOUNT)
112
+ if fee_in_ddx.recorded_amount() == Decimal("0"):
113
+ logger.info(
114
+ "Fee in DDX is 0 after conversion and discount, no fees to pay in DDX"
115
+ )
116
+ return False
117
+ if trader.avail_ddx_balance < fee_in_ddx:
118
+ # TODO 3825: this should be caught by the sequencer
119
+ logger.warn("Not enough DDX to pay fee")
120
+ return False
121
+ old_balance = trader.avail_ddx_balance
122
+ trader.avail_ddx_balance = (old_balance - fee_in_ddx).recorded_amount()
123
+ self.outcome.fee = (old_balance - trader.avail_ddx_balance).recorded_amount()
124
+ self.outcome.pay_fee_in_ddx = True
125
+ return True
126
+
127
+ def realize_trade_and_mutate_strategy(self, strategy: Strategy):
128
+ if strategy.frozen:
129
+ raise Exception("Cannot realize pnl from a frozen strategy")
130
+ update_avail_collateral(
131
+ strategy,
132
+ TokenSymbol.USDC,
133
+ strategy.avail_collateral[TokenSymbol.USDC] + self.realized_pnl,
134
+ )
135
+ if not self.outcome.pay_fee_in_ddx and self.outcome.fee > Decimal("0"):
136
+ old_balance = strategy.avail_collateral[TokenSymbol.USDC]
137
+ update_avail_collateral(
138
+ strategy,
139
+ TokenSymbol.USDC,
140
+ strategy.avail_collateral[TokenSymbol.USDC] - self.outcome.fee,
141
+ )
142
+ self.outcome.fee = (
143
+ old_balance - strategy.avail_collateral[TokenSymbol.USDC]
144
+ ).recorded_amount()
@@ -0,0 +1,38 @@
1
+ # This is unfortunate but must be done because
2
+ # https://pyo3.rs/main/faq#pyo3get-clones-my-field
3
+
4
+ from ddx._rust.common import TokenSymbol
5
+ from ddx._rust.common.state import InsuranceFundContribution, Strategy
6
+ from ddx._rust.decimal import Decimal
7
+
8
+
9
+ def update_avail_collateral(strategy: Strategy, symbol: TokenSymbol, amount: Decimal):
10
+ strategy.avail_collateral = strategy.update_avail_collateral(
11
+ symbol,
12
+ amount,
13
+ )
14
+
15
+
16
+ def update_locked_collateral(strategy: Strategy, symbol: TokenSymbol, amount: Decimal):
17
+ strategy.locked_collateral = strategy.update_locked_collateral(
18
+ symbol,
19
+ amount,
20
+ )
21
+
22
+
23
+ def update_avail_balance(
24
+ contribution: InsuranceFundContribution, symbol: TokenSymbol, amount: Decimal
25
+ ):
26
+ contribution.avail_balance = contribution.update_avail_balance(
27
+ symbol,
28
+ amount,
29
+ )
30
+
31
+
32
+ def update_locked_balance(
33
+ contribution: InsuranceFundContribution, symbol: TokenSymbol, amount: Decimal
34
+ ):
35
+ contribution.locked_balance = contribution.update_locked_balance(
36
+ symbol,
37
+ amount,
38
+ )
ddx/common/logging.py ADDED
@@ -0,0 +1,184 @@
1
+ import inspect
2
+ import logging
3
+
4
+ from colorama import Back, Fore, Style
5
+ from verboselogs import VerboseLogger
6
+
7
+ logging.setLoggerClass(VerboseLogger)
8
+
9
+ CHECKMARK = f"{Style.BRIGHT}{Back.GREEN}\u2713{Style.NORMAL}{Back.RESET}"
10
+
11
+
12
+ def freeze_logging(func):
13
+ """Decorator to set the logging pathname, filename, and lineno based on the caller of the decorated function."""
14
+
15
+ class CustomLogRecord(logging.LogRecord):
16
+ def __init__(self, *args, **kwargs):
17
+ super().__init__(*args, **kwargs)
18
+ # Capture the stack frame of the caller outside the current module and not from __init__.py
19
+ for f in inspect.stack():
20
+ if (
21
+ f[1] != inspect.getfile(inspect.currentframe())
22
+ and "__init__.py" not in f[1]
23
+ and "utils.py" not in f[1]
24
+ ):
25
+ self.pathname = f[1]
26
+ self.filename = f[1].split("/")[-1]
27
+ self.lineno = f[2]
28
+ break
29
+ else:
30
+ self.pathname = "unknown_path"
31
+ self.lineno = 0
32
+
33
+ def wrapper(*args, **kwargs):
34
+ # Temporarily replace the LogRecord class for the logger
35
+ original_factory = logging.getLogRecordFactory()
36
+ logging.setLogRecordFactory(CustomLogRecord)
37
+
38
+ try:
39
+ return func(*args, **kwargs)
40
+ finally:
41
+ # Restore the original LogRecord class
42
+ logging.setLogRecordFactory(original_factory)
43
+
44
+ return wrapper
45
+
46
+
47
+ def auditor_logger(name: str):
48
+ logger = logging.getLogger(name)
49
+ logger = AuditorAdapter(logger)
50
+ return logger
51
+
52
+
53
+ def local_logger(name: str):
54
+ logger = logging.getLogger(name)
55
+ logger = LocalAdapter(logger)
56
+ return logger
57
+
58
+
59
+ def assert_logger(name: str):
60
+ logger = logging.getLogger(name)
61
+ logger = AssertAdapter(logger)
62
+ return logger
63
+
64
+
65
+ def data_logger(name: str):
66
+ logger = logging.getLogger(name)
67
+ logger = DataAdapter(logger)
68
+ return logger
69
+
70
+
71
+ class DataAdapter(logging.LoggerAdapter):
72
+ """
73
+ Wrap all messages with "data: " and make the message green.
74
+ """
75
+
76
+ def process(self, msg, kwargs):
77
+ return f"{Fore.GREEN}data: {msg}{Style.RESET_ALL}", kwargs
78
+
79
+ @freeze_logging
80
+ def verbose(self, msg, *args, **kwargs):
81
+ self.log(logging.VERBOSE, msg, *args, **kwargs)
82
+
83
+ @freeze_logging
84
+ def notice(self, msg, *args, **kwargs):
85
+ self.log(logging.NOTICE, msg, *args, **kwargs)
86
+
87
+ @freeze_logging
88
+ def success(self, msg, *args, **kwargs):
89
+ self.log(logging.SUCCESS, msg, *args, **kwargs)
90
+
91
+ @freeze_logging
92
+ def spam(self, msg, *args, **kwargs):
93
+ self.log(logging.SPAM, msg, *args, **kwargs)
94
+
95
+ @freeze_logging
96
+ def failure(self, msg, *args, **kwargs):
97
+ self.log(logging.FAILURE, msg, *args, **kwargs)
98
+
99
+
100
+ class AssertAdapter(logging.LoggerAdapter):
101
+ """
102
+ Wrap all messages with "assert: " and make the message yellow.
103
+ """
104
+
105
+ def process(self, msg, kwargs):
106
+ return f"{Fore.YELLOW}assert: {msg}{Style.RESET_ALL}", kwargs
107
+
108
+ @freeze_logging
109
+ def verbose(self, msg, *args, **kwargs):
110
+ self.log(logging.VERBOSE, msg, *args, **kwargs)
111
+
112
+ @freeze_logging
113
+ def notice(self, msg, *args, **kwargs):
114
+ self.log(logging.NOTICE, msg, *args, **kwargs)
115
+
116
+ @freeze_logging
117
+ def success(self, msg, *args, **kwargs):
118
+ self.log(logging.SUCCESS, msg, *args, **kwargs)
119
+
120
+ @freeze_logging
121
+ def spam(self, msg, *args, **kwargs):
122
+ self.log(logging.SPAM, msg, *args, **kwargs)
123
+
124
+ @freeze_logging
125
+ def failure(self, msg, *args, **kwargs):
126
+ self.log(logging.FAILURE, msg, *args, **kwargs)
127
+
128
+
129
+ class AuditorAdapter(logging.LoggerAdapter):
130
+ """
131
+ Wrap all messages with "auditor: " and make the message magenta.
132
+ """
133
+
134
+ def process(self, msg, kwargs):
135
+ return f"{Fore.MAGENTA}auditor: {msg}{Style.RESET_ALL}", kwargs
136
+
137
+ @freeze_logging
138
+ def verbose(self, msg, *args, **kwargs):
139
+ self.log(logging.VERBOSE, msg, *args, **kwargs)
140
+
141
+ @freeze_logging
142
+ def notice(self, msg, *args, **kwargs):
143
+ self.log(logging.NOTICE, msg, *args, **kwargs)
144
+
145
+ @freeze_logging
146
+ def success(self, msg, *args, **kwargs):
147
+ self.log(logging.SUCCESS, msg, *args, **kwargs)
148
+
149
+ @freeze_logging
150
+ def spam(self, msg, *args, **kwargs):
151
+ self.log(logging.SPAM, msg, *args, **kwargs)
152
+
153
+ @freeze_logging
154
+ def failure(self, msg, *args, **kwargs):
155
+ self.log(logging.FAILURE, msg, *args, **kwargs)
156
+
157
+
158
+ class LocalAdapter(logging.LoggerAdapter):
159
+ """
160
+ Wrap all messages with "local: " and make the message blue.
161
+ """
162
+
163
+ def process(self, msg, kwargs):
164
+ return f"{Fore.BLUE}local: {msg}{Style.RESET_ALL}", kwargs
165
+
166
+ @freeze_logging
167
+ def verbose(self, msg, *args, **kwargs):
168
+ self.log(logging.VERBOSE, msg, *args, **kwargs)
169
+
170
+ @freeze_logging
171
+ def notice(self, msg, *args, **kwargs):
172
+ self.log(logging.NOTICE, msg, *args, **kwargs)
173
+
174
+ @freeze_logging
175
+ def success(self, msg, *args, **kwargs):
176
+ self.log(logging.SUCCESS, msg, *args, **kwargs)
177
+
178
+ @freeze_logging
179
+ def spam(self, msg, *args, **kwargs):
180
+ self.log(logging.SPAM, msg, *args, **kwargs)
181
+
182
+ @freeze_logging
183
+ def failure(self, msg, *args, **kwargs):
184
+ self.log(logging.FAILURE, msg, *args, **kwargs)
@@ -0,0 +1,64 @@
1
+ import logging
2
+
3
+ import sexpdata
4
+ from ddx._rust.common.specs import (
5
+ ProductSpecs,
6
+ QuarterlyExpiryFuture,
7
+ SingleNamePerpetual,
8
+ SpecsKind,
9
+ )
10
+ from ddx._rust.common.state.keys import SpecsKey
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class MarketSpecs:
16
+ """
17
+ Defines the MarketSpecs of all symbols
18
+
19
+ Attributes:
20
+ market_specs: SpecsKey <> ProductSpecs
21
+ """
22
+
23
+ def __init__(self, genesis_params: dict):
24
+ self.market_specs = {}
25
+ for spec_key, spec in genesis_params["Genesis"]["specs"].items():
26
+ if spec_key.startswith("SINGLENAMEPERP"):
27
+ spec_kind = SpecsKind.SingleNamePerpetual
28
+ spec_type = SingleNamePerpetual
29
+ elif spec_key.startswith("INDEXFUNDPERP"):
30
+ # TODO: implement this
31
+ # spec_kind = SpecsKind.IndexFundPerpetual
32
+ # spec_type = IndexFundPerpetual
33
+ raise NotImplementedError("IndexFundPerpetual is not implemented")
34
+ elif spec_key.startswith("QUARTERLYEXPIRYFUTURE"):
35
+ spec_kind = SpecsKind.QuarterlyExpiryFuture
36
+ spec_type = QuarterlyExpiryFuture
37
+ else:
38
+ continue
39
+ spec = sexpdata.loads(spec)
40
+ inner = spec_type(
41
+ **{
42
+ str(k)[1:].replace("-", "_"): v
43
+ for k, v in zip(spec[1::2], spec[2::2])
44
+ }
45
+ )
46
+ if isinstance(inner, SingleNamePerpetual):
47
+ product_specs = ProductSpecs.SingleNamePerpetual(inner)
48
+ elif isinstance(inner, QuarterlyExpiryFuture):
49
+ product_specs = ProductSpecs.QuarterlyExpiryFuture(inner)
50
+ else:
51
+ raise NotImplementedError("Unknown product specs type")
52
+ self.market_specs[SpecsKey(spec_kind, spec_key.split("-")[1])] = (
53
+ product_specs
54
+ )
55
+ logger.info(f"Loaded market specs: {self.market_specs}")
56
+
57
+ def __getitem__(self, specs_key: SpecsKey) -> ProductSpecs:
58
+ return self.market_specs[specs_key]
59
+
60
+ def keys(self):
61
+ return self.market_specs.keys()
62
+
63
+ def items(self):
64
+ return self.market_specs.items()
@@ -0,0 +1,19 @@
1
+ from attrs import define, field
2
+ from ddx._rust.decimal import Decimal
3
+
4
+
5
+ @define
6
+ class TradeMiningParams:
7
+ """
8
+ Defines the trade mining parameters determined at the start of a scenario
9
+ """
10
+
11
+ trade_mining_length: int
12
+ trade_mining_reward_per_epoch: Decimal
13
+ trade_mining_maker_reward_percentage: Decimal
14
+ trade_mining_taker_reward_percentage: Decimal = field(init=False)
15
+
16
+ def __attrs_post_init__(self):
17
+ self.trade_mining_taker_reward_percentage = (
18
+ Decimal("1") - self.trade_mining_maker_reward_percentage
19
+ )
@@ -0,0 +1,85 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ from ddx.common.logging import auditor_logger
5
+ from ddx._rust.common import ProductSymbol
6
+ from ddx._rust.common.state import DerivadexSMT, Price
7
+ from ddx._rust.common.state.keys import PriceKey
8
+ from ddx._rust.decimal import Decimal
9
+ from sortedcontainers import SortedKeyList
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def get_prices_for_symbol_and_duration(
15
+ smt: DerivadexSMT,
16
+ symbol: ProductSymbol,
17
+ duration: int,
18
+ ) -> SortedKeyList:
19
+ """
20
+ Get Price leaves from SMT for a given market and a certain duration. This is used
21
+ internally when computing the funding rate since we need to
22
+ obtain all the Price leaves in the state to derive the
23
+ time-weighted average of the premium rate.
24
+
25
+ Parameters
26
+ ----------
27
+ smt: DerivadexSMT
28
+ DerivaDEX Sparse Merkle Tree
29
+ symbol : ProductSymbol
30
+ Market symbol
31
+ duration : int
32
+ Duration of lookback in ticks
33
+ """
34
+ logger.debug(f"Getting price leaves for {symbol} for the last {duration} ticks")
35
+
36
+ price_leaves = smt.all_prices_for_symbol(symbol)
37
+ logger.debug(f"All price leaves for {symbol}: {price_leaves}")
38
+ if not price_leaves:
39
+ return SortedKeyList()
40
+
41
+ sorted_price_leaves = SortedKeyList(key=lambda price: price[1].time_value)
42
+ for key, value in price_leaves:
43
+ sorted_price_leaves.add((key, value))
44
+
45
+ last_time_value = sorted_price_leaves[-1][1].time_value
46
+
47
+ bisect_time_value = max(last_time_value - duration, 0)
48
+ logger.debug(
49
+ f"Retrieving price leaves from within ticks [{bisect_time_value}, {last_time_value}]"
50
+ )
51
+ bisect_index = sorted_price_leaves.bisect_key_left(bisect_time_value)
52
+ logger.debug(f"Bisection index: {bisect_index}")
53
+
54
+ return sorted_price_leaves[bisect_index:]
55
+
56
+
57
+ def get_most_recent_price(
58
+ smt: DerivadexSMT, symbol: ProductSymbol, time_value: int
59
+ ) -> Optional[tuple[PriceKey, Price]]:
60
+ """
61
+ Get the most recent Price leaf for a given market and time value.
62
+
63
+ Parameters
64
+ ----------
65
+ smt: DerivadexSMT
66
+ DerivaDEX Sparse Merkle Tree
67
+ symbol : ProductSymbol
68
+ Market symbol
69
+ time_value : int
70
+ Time value of reference
71
+ """
72
+
73
+ logger.debug(
74
+ f"Getting the most recent price for {symbol} at time value {time_value}"
75
+ )
76
+
77
+ price_leaves = smt.all_prices_for_symbol(symbol)
78
+ logger.debug(f"All price leaves for {symbol}: {price_leaves}")
79
+
80
+ # Find the most recent price leaf at or before time_value
81
+ return max(
82
+ filter(lambda price: price[1].time_value <= time_value, price_leaves),
83
+ key=lambda price: price[1].ordinal,
84
+ default=None,
85
+ )
File without changes