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.
- ddx/.gitignore +1 -0
- ddx/__init__.py +58 -0
- ddx/_rust/__init__.pyi +2685 -0
- ddx/_rust/common/__init__.pyi +17 -0
- ddx/_rust/common/accounting.pyi +6 -0
- ddx/_rust/common/enums.pyi +3 -0
- ddx/_rust/common/requests/__init__.pyi +23 -0
- ddx/_rust/common/requests/intents.pyi +19 -0
- ddx/_rust/common/specs.pyi +17 -0
- ddx/_rust/common/state/__init__.pyi +41 -0
- ddx/_rust/common/state/keys.pyi +29 -0
- ddx/_rust/common/transactions.pyi +7 -0
- ddx/_rust/decimal.pyi +3 -0
- ddx/_rust/h256.pyi +3 -0
- ddx/_rust.abi3.so +0 -0
- ddx/app_config/ethereum/addresses.json +526 -0
- ddx/auditor/README.md +32 -0
- ddx/auditor/__init__.py +0 -0
- ddx/auditor/auditor_driver.py +1043 -0
- ddx/auditor/websocket_message.py +54 -0
- ddx/common/__init__.py +0 -0
- ddx/common/epoch_params.py +28 -0
- ddx/common/fill_context.py +141 -0
- ddx/common/logging.py +184 -0
- ddx/common/market_aware_account.py +259 -0
- ddx/common/market_specs.py +64 -0
- ddx/common/trade_mining_params.py +19 -0
- ddx/common/transaction_utils.py +85 -0
- ddx/common/transactions/__init__.py +0 -0
- ddx/common/transactions/advance_epoch.py +91 -0
- ddx/common/transactions/advance_settlement_epoch.py +63 -0
- ddx/common/transactions/all_price_checkpoints.py +84 -0
- ddx/common/transactions/cancel.py +76 -0
- ddx/common/transactions/cancel_all.py +88 -0
- ddx/common/transactions/complete_fill.py +103 -0
- ddx/common/transactions/disaster_recovery.py +96 -0
- ddx/common/transactions/event.py +48 -0
- ddx/common/transactions/fee_distribution.py +119 -0
- ddx/common/transactions/funding.py +292 -0
- ddx/common/transactions/futures_expiry.py +123 -0
- ddx/common/transactions/genesis.py +108 -0
- ddx/common/transactions/inner/__init__.py +0 -0
- ddx/common/transactions/inner/adl_outcome.py +25 -0
- ddx/common/transactions/inner/fill.py +232 -0
- ddx/common/transactions/inner/liquidated_position.py +41 -0
- ddx/common/transactions/inner/liquidation_entry.py +41 -0
- ddx/common/transactions/inner/liquidation_fill.py +118 -0
- ddx/common/transactions/inner/outcome.py +32 -0
- ddx/common/transactions/inner/trade_fill.py +292 -0
- ddx/common/transactions/insurance_fund_update.py +138 -0
- ddx/common/transactions/insurance_fund_withdraw.py +100 -0
- ddx/common/transactions/liquidation.py +353 -0
- ddx/common/transactions/partial_fill.py +125 -0
- ddx/common/transactions/pnl_realization.py +120 -0
- ddx/common/transactions/post.py +72 -0
- ddx/common/transactions/post_order.py +95 -0
- ddx/common/transactions/price_checkpoint.py +97 -0
- ddx/common/transactions/signer_registered.py +62 -0
- ddx/common/transactions/specs_update.py +61 -0
- ddx/common/transactions/strategy_update.py +158 -0
- ddx/common/transactions/tradable_product_update.py +98 -0
- ddx/common/transactions/trade_mining.py +147 -0
- ddx/common/transactions/trader_update.py +131 -0
- ddx/common/transactions/withdraw.py +90 -0
- ddx/common/transactions/withdraw_ddx.py +74 -0
- ddx/common/utils.py +176 -0
- ddx/config.py +17 -0
- ddx/derivadex_client.py +270 -0
- ddx/models/__init__.py +0 -0
- ddx/models/base.py +132 -0
- ddx/py.typed +0 -0
- ddx/realtime_client/__init__.py +2 -0
- ddx/realtime_client/config.py +2 -0
- ddx/realtime_client/models/__init__.py +611 -0
- ddx/realtime_client/realtime_client.py +646 -0
- ddx/rest_client/__init__.py +0 -0
- ddx/rest_client/clients/__init__.py +0 -0
- ddx/rest_client/clients/base_client.py +60 -0
- ddx/rest_client/clients/market_client.py +1243 -0
- ddx/rest_client/clients/on_chain_client.py +439 -0
- ddx/rest_client/clients/signed_client.py +292 -0
- ddx/rest_client/clients/system_client.py +843 -0
- ddx/rest_client/clients/trade_client.py +357 -0
- ddx/rest_client/constants/__init__.py +0 -0
- ddx/rest_client/constants/endpoints.py +66 -0
- ddx/rest_client/contracts/__init__.py +0 -0
- ddx/rest_client/contracts/checkpoint/__init__.py +560 -0
- ddx/rest_client/contracts/ddx/__init__.py +1949 -0
- ddx/rest_client/contracts/dummy_token/__init__.py +1014 -0
- ddx/rest_client/contracts/i_collateral/__init__.py +1414 -0
- ddx/rest_client/contracts/i_stake/__init__.py +696 -0
- ddx/rest_client/exceptions/__init__.py +0 -0
- ddx/rest_client/exceptions/exceptions.py +32 -0
- ddx/rest_client/http/__init__.py +0 -0
- ddx/rest_client/http/http_client.py +336 -0
- ddx/rest_client/models/__init__.py +0 -0
- ddx/rest_client/models/market.py +693 -0
- ddx/rest_client/models/signed.py +61 -0
- ddx/rest_client/models/system.py +311 -0
- ddx/rest_client/models/trade.py +185 -0
- ddx/rest_client/utils/__init__.py +0 -0
- ddx/rest_client/utils/encryption_utils.py +26 -0
- ddx/utils/__init__.py +0 -0
- ddx_python-1.0.4.dist-info/METADATA +63 -0
- ddx_python-1.0.4.dist-info/RECORD +106 -0
- 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
|
+
)
|