ddx-python 1.0.4__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 (106) hide show
  1. ddx/.gitignore +1 -0
  2. ddx/__init__.py +58 -0
  3. ddx/_rust/__init__.pyi +2685 -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 +23 -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 +526 -0
  17. ddx/auditor/README.md +32 -0
  18. ddx/auditor/__init__.py +0 -0
  19. ddx/auditor/auditor_driver.py +1043 -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 +141 -0
  24. ddx/common/logging.py +184 -0
  25. ddx/common/market_aware_account.py +259 -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 +96 -0
  37. ddx/common/transactions/event.py +48 -0
  38. ddx/common/transactions/fee_distribution.py +119 -0
  39. ddx/common/transactions/funding.py +292 -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 +232 -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 +292 -0
  50. ddx/common/transactions/insurance_fund_update.py +138 -0
  51. ddx/common/transactions/insurance_fund_withdraw.py +100 -0
  52. ddx/common/transactions/liquidation.py +353 -0
  53. ddx/common/transactions/partial_fill.py +125 -0
  54. ddx/common/transactions/pnl_realization.py +120 -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 +97 -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 +158 -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 +131 -0
  64. ddx/common/transactions/withdraw.py +90 -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 +270 -0
  69. ddx/models/__init__.py +0 -0
  70. ddx/models/base.py +132 -0
  71. ddx/py.typed +0 -0
  72. ddx/realtime_client/__init__.py +2 -0
  73. ddx/realtime_client/config.py +2 -0
  74. ddx/realtime_client/models/__init__.py +611 -0
  75. ddx/realtime_client/realtime_client.py +646 -0
  76. ddx/rest_client/__init__.py +0 -0
  77. ddx/rest_client/clients/__init__.py +0 -0
  78. ddx/rest_client/clients/base_client.py +60 -0
  79. ddx/rest_client/clients/market_client.py +1243 -0
  80. ddx/rest_client/clients/on_chain_client.py +439 -0
  81. ddx/rest_client/clients/signed_client.py +292 -0
  82. ddx/rest_client/clients/system_client.py +843 -0
  83. ddx/rest_client/clients/trade_client.py +357 -0
  84. ddx/rest_client/constants/__init__.py +0 -0
  85. ddx/rest_client/constants/endpoints.py +66 -0
  86. ddx/rest_client/contracts/__init__.py +0 -0
  87. ddx/rest_client/contracts/checkpoint/__init__.py +560 -0
  88. ddx/rest_client/contracts/ddx/__init__.py +1949 -0
  89. ddx/rest_client/contracts/dummy_token/__init__.py +1014 -0
  90. ddx/rest_client/contracts/i_collateral/__init__.py +1414 -0
  91. ddx/rest_client/contracts/i_stake/__init__.py +696 -0
  92. ddx/rest_client/exceptions/__init__.py +0 -0
  93. ddx/rest_client/exceptions/exceptions.py +32 -0
  94. ddx/rest_client/http/__init__.py +0 -0
  95. ddx/rest_client/http/http_client.py +336 -0
  96. ddx/rest_client/models/__init__.py +0 -0
  97. ddx/rest_client/models/market.py +693 -0
  98. ddx/rest_client/models/signed.py +61 -0
  99. ddx/rest_client/models/system.py +311 -0
  100. ddx/rest_client/models/trade.py +185 -0
  101. ddx/rest_client/utils/__init__.py +0 -0
  102. ddx/rest_client/utils/encryption_utils.py +26 -0
  103. ddx/utils/__init__.py +0 -0
  104. ddx_python-1.0.4.dist-info/METADATA +63 -0
  105. ddx_python-1.0.4.dist-info/RECORD +106 -0
  106. ddx_python-1.0.4.dist-info/WHEEL +5 -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,141 @@
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.transactions.inner.outcome import Outcome
10
+ from ddx._rust.common import TokenSymbol
11
+ from ddx._rust.common.enums import OrderSide, PositionSide, TradeSide
12
+ from ddx._rust.common.state import Position, Strategy, Trader
13
+ from ddx._rust.decimal import Decimal
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ MAX_DDX_PRICE_CHECKPOINT_AGE_IN_TICKS = 40000
18
+ DDX_FEE_DISCOUNT = 0.5
19
+
20
+
21
+ def apply_trade(
22
+ position: Position, amount: Decimal, price: Decimal, side: OrderSide
23
+ ) -> (Position, Decimal):
24
+ logger.debug(
25
+ f"Applying trade of {amount} at {price} on {side} to position {position}"
26
+ )
27
+ if side == OrderSide.Bid:
28
+ if position.side == PositionSide.Long:
29
+ logger.info("Trade side matches position side, increasing position balance")
30
+ return position.increase(price, amount)
31
+ if position.side == PositionSide.Short:
32
+ logger.info(
33
+ "Trade side does not match position side, decreasing position balance"
34
+ )
35
+ if amount > position.balance:
36
+ return position.cross_over(price, amount)
37
+ return position.decrease(price, amount)
38
+ logger.info(
39
+ "Position side not set, setting to Long and increasing position balance"
40
+ )
41
+ position.side = PositionSide.Long
42
+ return position.increase(price, amount)
43
+ else:
44
+ if position.side == PositionSide.Short:
45
+ logger.info("Trade side matches position side, increasing position balance")
46
+ return position.increase(price, amount)
47
+ if position.side == PositionSide.Long:
48
+ logger.info(
49
+ "Trade side does not match position side, decreasing position balance"
50
+ )
51
+ if amount > position.balance:
52
+ return position.cross_over(price, amount)
53
+ return position.decrease(price, amount)
54
+ logger.info(
55
+ "Position side not set, setting to Short and increasing position balance"
56
+ )
57
+ position.side = PositionSide.Short
58
+ return position.increase(price, amount)
59
+
60
+
61
+ @define
62
+ class FillContext:
63
+ """
64
+ Defines a FillContext.
65
+ """
66
+
67
+ outcome: Outcome
68
+ realized_pnl: Optional[Decimal] = field(init=False)
69
+
70
+ def apply_fill(
71
+ self,
72
+ position: Optional[Position],
73
+ side: OrderSide,
74
+ trade_side: TradeSide,
75
+ fill_amount: Decimal,
76
+ fill_price: Decimal,
77
+ ) -> Position:
78
+ if position is None:
79
+ position = Position(
80
+ PositionSide.Long if side == OrderSide.Bid else PositionSide.Short,
81
+ Decimal("0"),
82
+ Decimal("0"),
83
+ )
84
+ logger.info(f"New {position.side} position")
85
+ old_balance = position.balance
86
+
87
+ fee = trade_side.trading_fee(fill_amount, fill_price)
88
+ updated_position, realized_pnl = apply_trade(
89
+ position, fill_amount, fill_price, side
90
+ )
91
+
92
+ self.realized_pnl = realized_pnl
93
+ logger.info(f"Realized pnl: {self.realized_pnl}")
94
+ logger.info(f"Fee: {fee}")
95
+
96
+ # Note that, again, we're never reading the fee from the txlog, and instead
97
+ # we calulate it from the fill amount and price and set it in the outcome.
98
+ self.outcome.fee = fee
99
+
100
+ return updated_position
101
+
102
+ def apply_ddx_fee_and_mutate_trader(
103
+ self,
104
+ trader: Trader,
105
+ ddx_price: Decimal,
106
+ ) -> bool:
107
+ if self.outcome.fee == Decimal("0"):
108
+ logger.info("Base fee of 0, no fees to pay")
109
+ return False
110
+ fee_in_ddx = (self.outcome.fee / ddx_price) * (Decimal("1") - DDX_FEE_DISCOUNT)
111
+ if fee_in_ddx.recorded_amount() == Decimal("0"):
112
+ logger.info(
113
+ "Fee in DDX is 0 after conversion and discount, no fees to pay in DDX"
114
+ )
115
+ return False
116
+ if trader.avail_ddx_balance < fee_in_ddx:
117
+ # TODO 3825: this should be caught by the sequencer
118
+ logger.warn("Not enough DDX to pay fee")
119
+ return False
120
+ old_balance = trader.avail_ddx_balance
121
+ trader.avail_ddx_balance = (old_balance - fee_in_ddx).recorded_amount()
122
+ self.outcome.fee = (old_balance - trader.avail_ddx_balance).recorded_amount()
123
+ self.outcome.pay_fee_in_ddx = True
124
+ return True
125
+
126
+ def realize_trade_and_mutate_strategy(self, strategy: Strategy):
127
+ if strategy.frozen:
128
+ raise Exception("Cannot realize pnl from a frozen strategy")
129
+ strategy.set_avail_collateral(
130
+ TokenSymbol.USDC,
131
+ strategy.avail_collateral[TokenSymbol.USDC] + self.realized_pnl,
132
+ )
133
+ if not self.outcome.pay_fee_in_ddx and self.outcome.fee > Decimal("0"):
134
+ old_balance = strategy.avail_collateral[TokenSymbol.USDC]
135
+ strategy.set_avail_collateral(
136
+ TokenSymbol.USDC,
137
+ strategy.avail_collateral[TokenSymbol.USDC] - self.outcome.fee,
138
+ )
139
+ self.outcome.fee = (
140
+ old_balance - strategy.avail_collateral[TokenSymbol.USDC]
141
+ ).recorded_amount()
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,259 @@
1
+ """
2
+ MarketAwareAccount module.
3
+ """
4
+
5
+ import copy
6
+ import logging
7
+
8
+ from attrs import define
9
+ from ddx.common.transactions.price_checkpoint import PriceCheckpoint
10
+ from ddx._rust.common import ProductSymbol, TokenSymbol
11
+ from ddx._rust.common.enums import OrderSide, PositionSide
12
+ from ddx._rust.common.state import Position, Strategy
13
+ from ddx._rust.decimal import Decimal
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ MMR_FRACTION = Decimal("0.15")
18
+
19
+
20
+ # Designed to mimic ddxenclave::StrategyMetrics
21
+ @define
22
+ class MarketAwareAccount:
23
+ avail_collateral: Decimal
24
+ max_leverage: int
25
+ positions: dict[ProductSymbol, tuple[Position, Decimal]]
26
+
27
+ # must be initialize with a nonempty strategy
28
+ def __init__(
29
+ self,
30
+ strategy: Strategy,
31
+ positions: dict[ProductSymbol, tuple[Position, Decimal]],
32
+ ):
33
+ return self.__attrs_init__(
34
+ strategy.avail_collateral[TokenSymbol.USDC],
35
+ strategy.max_leverage,
36
+ positions,
37
+ )
38
+
39
+ @property
40
+ def notional_value(self):
41
+ notional = Decimal("0")
42
+ for symbol, (position, mark_price) in self.positions.items():
43
+ notional += position.balance * mark_price
44
+ logger.debug(f"Notional value: {notional}")
45
+ return notional
46
+
47
+ @property
48
+ def unrealized_pnl(self):
49
+ unrealized_pnl = Decimal("0")
50
+ for symbol, (position, mark_price) in self.positions.items():
51
+ unrealized_pnl += position.unrealized_pnl(mark_price)
52
+ logger.debug(f"Unrealized pnl: {unrealized_pnl}")
53
+ return unrealized_pnl
54
+
55
+ @property
56
+ def total_value(self):
57
+ res = self.avail_collateral + self.unrealized_pnl
58
+ logger.debug(f"Total value: {res}")
59
+ return res
60
+
61
+ @property
62
+ def margin_fraction(self):
63
+ total_value = self.total_value
64
+ notional_value = self.notional_value
65
+ if notional_value == Decimal("0"):
66
+ if total_value < Decimal("0"):
67
+ return Decimal("-1_000_000")
68
+ return Decimal("1_000_000")
69
+
70
+ mf = total_value / notional_value
71
+ logger.debug(f"Margin fraction: {mf}")
72
+ return mf
73
+
74
+ @property
75
+ def maintenance_margin_fraction(self):
76
+ mmf = MMR_FRACTION / self.max_leverage
77
+ logger.debug(f"Maintenance margin fraction: {mmf}")
78
+ return mmf
79
+
80
+ @property
81
+ def maximum_withdrawal_amount(self):
82
+ res = self.total_value - self.maintenance_margin_fraction * self.notional_value
83
+ logger.debug(f"Max withdrawal amount: {res}")
84
+ return res
85
+
86
+ def maximum_fill_amount_increasing(
87
+ self,
88
+ fee_percentage: Decimal,
89
+ mark_price: Decimal,
90
+ side: OrderSide,
91
+ price: Decimal,
92
+ amount: Decimal,
93
+ ):
94
+ logger.debug(
95
+ f"Max fill amount increasing current position {self.avail_collateral} {self.notional_value}"
96
+ )
97
+ if self.avail_collateral == Decimal("0") and self.notional_value == Decimal(
98
+ "0"
99
+ ):
100
+ return Decimal("0")
101
+
102
+ side = Decimal("1") if side == OrderSide.Bid else Decimal("-1")
103
+ gamma = Decimal("1") / self.max_leverage
104
+ if self.margin_fraction <= gamma:
105
+ derivative_numerator = (
106
+ self.notional_value
107
+ * (side * (mark_price - price) - fee_percentage * price)
108
+ - mark_price * self.total_value
109
+ )
110
+ return min(
111
+ (
112
+ Decimal("0")
113
+ if derivative_numerator < Decimal("0")
114
+ else Decimal("1_000_000")
115
+ ),
116
+ amount,
117
+ )
118
+ else:
119
+ denominator = (
120
+ side * (price - mark_price)
121
+ + fee_percentage * price
122
+ + gamma * mark_price
123
+ )
124
+ return min(
125
+ (
126
+ (self.total_value - gamma * self.notional_value) / denominator
127
+ if denominator > Decimal("0")
128
+ else Decimal("1_000_000")
129
+ ),
130
+ amount,
131
+ )
132
+
133
+ def maximum_fill_amount_decreasing(
134
+ self,
135
+ fee_percentage: Decimal,
136
+ mark_price: Decimal,
137
+ position: Position,
138
+ side: OrderSide,
139
+ price: Decimal,
140
+ amount: Decimal,
141
+ ):
142
+ logger.debug(f"Max fill amount decreasing current position")
143
+ side = Decimal("-1") if side == OrderSide.Bid else Decimal("1")
144
+ gamma = Decimal("1") / self.max_leverage
145
+ if self.margin_fraction <= gamma:
146
+ derivative_numerator = (
147
+ self.notional_value
148
+ * (side * (price - mark_price) - fee_percentage * price)
149
+ + mark_price * self.total_value
150
+ )
151
+ theoretical_fill_amount = (
152
+ Decimal("0")
153
+ if derivative_numerator < Decimal("0")
154
+ else Decimal("1_000_000")
155
+ )
156
+ else:
157
+ denominator = (
158
+ side * (mark_price - price)
159
+ + fee_percentage * price
160
+ - gamma * mark_price
161
+ )
162
+ theoretical_fill_amount = (
163
+ (self.total_value - gamma * self.notional_value) / denominator
164
+ if denominator > Decimal("0")
165
+ else Decimal("1_000_000")
166
+ )
167
+
168
+ return min(
169
+ theoretical_fill_amount,
170
+ amount,
171
+ position.balance,
172
+ )
173
+
174
+ def maximum_fill_amount_cross_over(
175
+ self,
176
+ symbol: ProductSymbol,
177
+ fee_percentage: Decimal,
178
+ mark_price: Decimal,
179
+ position: Position,
180
+ side: OrderSide,
181
+ price: Decimal,
182
+ amount: Decimal,
183
+ ):
184
+ logger.debug(f"Max fill amount crossing over current position")
185
+ decreasing_amount = self.maximum_fill_amount_decreasing(
186
+ fee_percentage, mark_price, position, side, price, amount
187
+ )
188
+
189
+ if decreasing_amount == position.balance:
190
+ # copy account so as to not modify anything
191
+ copy_account = copy.deepcopy(self)
192
+ copy_account.avail_collateral = (
193
+ copy_account.avail_collateral
194
+ + position.avg_pnl(price) * decreasing_amount
195
+ - fee_percentage * price * decreasing_amount
196
+ ).recorded_amount()
197
+ del copy_account.positions[symbol]
198
+
199
+ increasing_amount = copy_account.maximum_fill_amount_increasing(
200
+ fee_percentage, mark_price, side, price, amount - decreasing_amount
201
+ )
202
+ return decreasing_amount + increasing_amount
203
+ else:
204
+ return decreasing_amount
205
+
206
+ def maximum_fill_amount(
207
+ self,
208
+ symbol: ProductSymbol,
209
+ fee_percentage: Decimal,
210
+ mark_price: Decimal,
211
+ side: OrderSide,
212
+ price: Decimal,
213
+ amount: Decimal,
214
+ min_order_size: Decimal,
215
+ ):
216
+ logger.debug(f"Calculating max fill amount")
217
+ if symbol in self.positions:
218
+ logger.debug(f"Position already exists")
219
+ p = self.positions[symbol][0]
220
+
221
+ if (
222
+ (p.side == PositionSide.Long and side == OrderSide.Bid)
223
+ or (p.side == PositionSide.Short and side == OrderSide.Ask)
224
+ or p.side == PositionSide.Empty
225
+ ):
226
+ fill_amount = self.maximum_fill_amount_increasing(
227
+ fee_percentage, mark_price, side, price, amount
228
+ )
229
+ else:
230
+ fill_amount = self.maximum_fill_amount_cross_over(
231
+ symbol, fee_percentage, mark_price, p, side, price, amount
232
+ )
233
+ else:
234
+ logger.debug(f"Position does not exist, auto increase")
235
+ fill_amount = self.maximum_fill_amount_increasing(
236
+ fee_percentage,
237
+ mark_price,
238
+ side,
239
+ price,
240
+ amount,
241
+ )
242
+ return (
243
+ fill_amount
244
+ if min_order_size == Decimal("0")
245
+ else fill_amount - (fill_amount % min_order_size)
246
+ )
247
+
248
+ def assess_solvency(self):
249
+ res = 0 if self.margin_fraction < self.maintenance_margin_fraction else 1
250
+ logger.info(
251
+ f"solvent? {bool(res)}; margin fraction is {self.margin_fraction} and maintenance margin fraction is {self.maintenance_margin_fraction}"
252
+ )
253
+ return res
254
+
255
+ def sorted_positions_by_unrealized_pnl(self):
256
+ return sorted(
257
+ self.positions.items(),
258
+ key=lambda item: item[1][0].unrealized_pnl(item[1][1]),
259
+ )