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.
- ddx/.gitignore +1 -0
- ddx/__init__.py +58 -0
- ddx/_rust/__init__.pyi +2009 -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 +21 -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 +541 -0
- ddx/auditor/README.md +32 -0
- ddx/auditor/__init__.py +0 -0
- ddx/auditor/auditor_driver.py +1034 -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 +144 -0
- ddx/common/item_utils.py +38 -0
- ddx/common/logging.py +184 -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 +97 -0
- ddx/common/transactions/event.py +48 -0
- ddx/common/transactions/fee_distribution.py +119 -0
- ddx/common/transactions/funding.py +294 -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 +227 -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 +125 -0
- ddx/common/transactions/insurance_fund_update.py +142 -0
- ddx/common/transactions/insurance_fund_withdraw.py +99 -0
- ddx/common/transactions/liquidation.py +357 -0
- ddx/common/transactions/partial_fill.py +125 -0
- ddx/common/transactions/pnl_realization.py +122 -0
- ddx/common/transactions/post.py +72 -0
- ddx/common/transactions/post_order.py +95 -0
- ddx/common/transactions/price_checkpoint.py +96 -0
- ddx/common/transactions/signer_registered.py +62 -0
- ddx/common/transactions/specs_update.py +61 -0
- ddx/common/transactions/strategy_update.py +156 -0
- ddx/common/transactions/tradable_product_update.py +98 -0
- ddx/common/transactions/trade_mining.py +147 -0
- ddx/common/transactions/trader_update.py +105 -0
- ddx/common/transactions/withdraw.py +91 -0
- ddx/common/transactions/withdraw_ddx.py +74 -0
- ddx/common/utils.py +176 -0
- ddx/config.py +17 -0
- ddx/derivadex_client.py +254 -0
- ddx/py.typed +0 -0
- ddx/realtime_client/__init__.py +2 -0
- ddx/realtime_client/config.py +2 -0
- ddx/realtime_client/logs/pytest.log +0 -0
- ddx/realtime_client/models/__init__.py +683 -0
- ddx/realtime_client/realtime_client.py +567 -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 +1241 -0
- ddx/rest_client/clients/on_chain_client.py +432 -0
- ddx/rest_client/clients/signed_client.py +301 -0
- ddx/rest_client/clients/system_client.py +843 -0
- ddx/rest_client/clients/trade_client.py +335 -0
- ddx/rest_client/constants/__init__.py +0 -0
- ddx/rest_client/constants/endpoints.py +67 -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 +305 -0
- ddx/rest_client/models/__init__.py +0 -0
- ddx/rest_client/models/market.py +683 -0
- ddx/rest_client/models/signed.py +60 -0
- ddx/rest_client/models/system.py +390 -0
- ddx/rest_client/models/trade.py +140 -0
- ddx/rest_client/utils/__init__.py +0 -0
- ddx/rest_client/utils/encryption_utils.py +26 -0
- ddx_python-1.0.5.dist-info/METADATA +63 -0
- ddx_python-1.0.5.dist-info/RECORD +104 -0
- 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()
|
ddx/common/item_utils.py
ADDED
|
@@ -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
|