x10-python-trading-starknet 0.0.1__py3-none-any.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.
- x10/__init__.py +0 -0
- x10/config.py +13 -0
- x10/errors.py +2 -0
- x10/perpetual/__init__.py +0 -0
- x10/perpetual/accounts.py +86 -0
- x10/perpetual/amounts.py +55 -0
- x10/perpetual/assets.py +71 -0
- x10/perpetual/balances.py +15 -0
- x10/perpetual/candles.py +18 -0
- x10/perpetual/configuration.py +86 -0
- x10/perpetual/fees.py +16 -0
- x10/perpetual/funding_rates.py +11 -0
- x10/perpetual/markets.py +125 -0
- x10/perpetual/order_object.py +201 -0
- x10/perpetual/orderbook.py +211 -0
- x10/perpetual/orderbooks.py +17 -0
- x10/perpetual/orders.py +179 -0
- x10/perpetual/positions.py +51 -0
- x10/perpetual/simple_client/simple_trading_client.py +216 -0
- x10/perpetual/stream_client/__init__.py +3 -0
- x10/perpetual/stream_client/perpetual_stream_connection.py +108 -0
- x10/perpetual/stream_client/stream_client.py +83 -0
- x10/perpetual/trades.py +38 -0
- x10/perpetual/trading_client/__init__.py +3 -0
- x10/perpetual/trading_client/account_module.py +209 -0
- x10/perpetual/trading_client/base_module.py +58 -0
- x10/perpetual/trading_client/info_module.py +13 -0
- x10/perpetual/trading_client/markets_information_module.py +76 -0
- x10/perpetual/trading_client/order_management_module.py +81 -0
- x10/perpetual/trading_client/testnet_module.py +68 -0
- x10/perpetual/trading_client/trading_client.py +123 -0
- x10/perpetual/transfer_object.py +80 -0
- x10/perpetual/transfers.py +39 -0
- x10/perpetual/user_client/__init__.py +0 -0
- x10/perpetual/user_client/l1_signing.py +0 -0
- x10/perpetual/user_client/onboarding.py +231 -0
- x10/perpetual/user_client/user_client.py +199 -0
- x10/perpetual/withdrawal_object.py +29 -0
- x10/perpetual/withdrawals.py +29 -0
- x10/utils/__init__.py +2 -0
- x10/utils/date.py +11 -0
- x10/utils/http.py +239 -0
- x10/utils/log.py +5 -0
- x10/utils/model.py +66 -0
- x10/utils/nonce.py +11 -0
- x10/utils/string.py +10 -0
- x10_python_trading_starknet-0.0.1.dist-info/LICENSE +21 -0
- x10_python_trading_starknet-0.0.1.dist-info/METADATA +413 -0
- x10_python_trading_starknet-0.0.1.dist-info/RECORD +50 -0
- x10_python_trading_starknet-0.0.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import Callable, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from fast_stark_crypto import get_order_msg_hash
|
|
7
|
+
|
|
8
|
+
from x10.perpetual.accounts import StarkPerpetualAccount
|
|
9
|
+
from x10.perpetual.amounts import (
|
|
10
|
+
ROUNDING_BUY_CONTEXT,
|
|
11
|
+
ROUNDING_FEE_CONTEXT,
|
|
12
|
+
ROUNDING_SELL_CONTEXT,
|
|
13
|
+
HumanReadableAmount,
|
|
14
|
+
StarkAmount,
|
|
15
|
+
)
|
|
16
|
+
from x10.perpetual.configuration import StarknetDomain
|
|
17
|
+
from x10.perpetual.fees import DEFAULT_FEES, TradingFeeModel
|
|
18
|
+
from x10.perpetual.markets import MarketModel
|
|
19
|
+
from x10.perpetual.orders import (
|
|
20
|
+
OrderSide,
|
|
21
|
+
OrderType,
|
|
22
|
+
PerpetualOrderModel,
|
|
23
|
+
SelfTradeProtectionLevel,
|
|
24
|
+
SettlementSignatureModel,
|
|
25
|
+
StarkDebuggingOrderAmountsModel,
|
|
26
|
+
StarkSettlementModel,
|
|
27
|
+
TimeInForce,
|
|
28
|
+
)
|
|
29
|
+
from x10.utils import generate_nonce
|
|
30
|
+
from x10.utils.date import to_epoch_millis, utc_now
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def create_order_object(
|
|
34
|
+
account: StarkPerpetualAccount,
|
|
35
|
+
market: MarketModel,
|
|
36
|
+
amount_of_synthetic: Decimal,
|
|
37
|
+
price: Decimal,
|
|
38
|
+
side: OrderSide,
|
|
39
|
+
starknet_domain: StarknetDomain,
|
|
40
|
+
post_only: bool = False,
|
|
41
|
+
previous_order_id: Optional[str] = None,
|
|
42
|
+
expire_time: Optional[datetime] = None,
|
|
43
|
+
order_external_id: Optional[str] = None,
|
|
44
|
+
time_in_force: TimeInForce = TimeInForce.GTT,
|
|
45
|
+
self_trade_protection_level: SelfTradeProtectionLevel = SelfTradeProtectionLevel.ACCOUNT,
|
|
46
|
+
nonce: Optional[int] = None,
|
|
47
|
+
) -> PerpetualOrderModel:
|
|
48
|
+
"""
|
|
49
|
+
Creates an order object to be placed on the exchange using the `place_order` method.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
if expire_time is None:
|
|
53
|
+
expire_time = utc_now() + timedelta(hours=1)
|
|
54
|
+
|
|
55
|
+
fees = account.trading_fee.get(market.name, DEFAULT_FEES)
|
|
56
|
+
|
|
57
|
+
return __create_order_object(
|
|
58
|
+
market=market,
|
|
59
|
+
synthetic_amount=amount_of_synthetic,
|
|
60
|
+
price=price,
|
|
61
|
+
side=side,
|
|
62
|
+
collateral_position_id=account.vault,
|
|
63
|
+
fees=fees,
|
|
64
|
+
signer=account.sign,
|
|
65
|
+
public_key=account.public_key,
|
|
66
|
+
exact_only=False,
|
|
67
|
+
expire_time=expire_time,
|
|
68
|
+
post_only=post_only,
|
|
69
|
+
previous_order_external_id=previous_order_id,
|
|
70
|
+
order_external_id=order_external_id,
|
|
71
|
+
time_in_force=time_in_force,
|
|
72
|
+
self_trade_protection_level=self_trade_protection_level,
|
|
73
|
+
starknet_domain=starknet_domain,
|
|
74
|
+
nonce=nonce,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def __create_order_object(
|
|
79
|
+
market: MarketModel,
|
|
80
|
+
synthetic_amount: Decimal,
|
|
81
|
+
price: Decimal,
|
|
82
|
+
side: OrderSide,
|
|
83
|
+
collateral_position_id: int,
|
|
84
|
+
fees: TradingFeeModel,
|
|
85
|
+
signer: Callable[[int], Tuple[int, int]],
|
|
86
|
+
public_key: int,
|
|
87
|
+
starknet_domain: StarknetDomain,
|
|
88
|
+
exact_only: bool = False,
|
|
89
|
+
expire_time: Optional[datetime] = None,
|
|
90
|
+
post_only: bool = False,
|
|
91
|
+
previous_order_external_id: Optional[str] = None,
|
|
92
|
+
order_external_id: Optional[str] = None,
|
|
93
|
+
time_in_force: TimeInForce = TimeInForce.GTT,
|
|
94
|
+
self_trade_protection_level: SelfTradeProtectionLevel = SelfTradeProtectionLevel.ACCOUNT,
|
|
95
|
+
nonce: Optional[int] = None,
|
|
96
|
+
) -> PerpetualOrderModel:
|
|
97
|
+
if exact_only:
|
|
98
|
+
raise NotImplementedError("`exact_only` option is not supported yet")
|
|
99
|
+
|
|
100
|
+
if expire_time is None:
|
|
101
|
+
raise ValueError("`expire_time` must be provided")
|
|
102
|
+
if nonce is None:
|
|
103
|
+
nonce = generate_nonce()
|
|
104
|
+
is_buying_synthetic = side == OrderSide.BUY
|
|
105
|
+
rounding_context = ROUNDING_BUY_CONTEXT if is_buying_synthetic else ROUNDING_SELL_CONTEXT
|
|
106
|
+
|
|
107
|
+
collateral_amount_human = HumanReadableAmount(synthetic_amount * price, market.collateral_asset)
|
|
108
|
+
synthetic_amount_human = HumanReadableAmount(synthetic_amount, market.synthetic_asset)
|
|
109
|
+
fee_amount_human = HumanReadableAmount(
|
|
110
|
+
fees.taker_fee_rate * collateral_amount_human.value,
|
|
111
|
+
market.collateral_asset,
|
|
112
|
+
)
|
|
113
|
+
fee_rate = fees.taker_fee_rate
|
|
114
|
+
|
|
115
|
+
stark_collateral_amount: StarkAmount = collateral_amount_human.to_stark_amount(rounding_context=rounding_context)
|
|
116
|
+
stark_synthetic_amount: StarkAmount = synthetic_amount_human.to_stark_amount(rounding_context=rounding_context)
|
|
117
|
+
stark_fee_amount: StarkAmount = fee_amount_human.to_stark_amount(rounding_context=ROUNDING_FEE_CONTEXT)
|
|
118
|
+
|
|
119
|
+
if is_buying_synthetic:
|
|
120
|
+
stark_collateral_amount = stark_collateral_amount.negate()
|
|
121
|
+
else:
|
|
122
|
+
stark_synthetic_amount = stark_synthetic_amount.negate()
|
|
123
|
+
|
|
124
|
+
debugging_amounts = StarkDebuggingOrderAmountsModel(
|
|
125
|
+
collateral_amount=Decimal(stark_collateral_amount.value),
|
|
126
|
+
fee_amount=Decimal(stark_fee_amount.value),
|
|
127
|
+
synthetic_amount=Decimal(stark_synthetic_amount.value),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
order_hash = hash_order(
|
|
131
|
+
amount_synthetic=stark_synthetic_amount,
|
|
132
|
+
amount_collateral=stark_collateral_amount,
|
|
133
|
+
max_fee=stark_fee_amount,
|
|
134
|
+
nonce=nonce,
|
|
135
|
+
position_id=collateral_position_id,
|
|
136
|
+
expiration_timestamp=expire_time,
|
|
137
|
+
public_key=public_key,
|
|
138
|
+
starknet_domain=starknet_domain,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
(order_signature_r, order_signature_s) = signer(order_hash)
|
|
142
|
+
settlement = StarkSettlementModel(
|
|
143
|
+
signature=SettlementSignatureModel(r=order_signature_r, s=order_signature_s),
|
|
144
|
+
stark_key=public_key,
|
|
145
|
+
collateral_position=Decimal(collateral_position_id),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
order_id = str(order_hash) if order_external_id is None else order_external_id
|
|
149
|
+
order = PerpetualOrderModel(
|
|
150
|
+
id=order_id,
|
|
151
|
+
market=market.name,
|
|
152
|
+
type=OrderType.LIMIT,
|
|
153
|
+
side=side,
|
|
154
|
+
qty=synthetic_amount_human.value,
|
|
155
|
+
price=price,
|
|
156
|
+
post_only=post_only,
|
|
157
|
+
time_in_force=time_in_force,
|
|
158
|
+
expiry_epoch_millis=to_epoch_millis(expire_time),
|
|
159
|
+
fee=fee_rate,
|
|
160
|
+
self_trade_protection_level=self_trade_protection_level,
|
|
161
|
+
nonce=Decimal(nonce),
|
|
162
|
+
cancel_id=previous_order_external_id,
|
|
163
|
+
settlement=settlement,
|
|
164
|
+
debugging_amounts=debugging_amounts,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return order
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def hash_order(
|
|
171
|
+
amount_synthetic: StarkAmount,
|
|
172
|
+
amount_collateral: StarkAmount,
|
|
173
|
+
max_fee: StarkAmount,
|
|
174
|
+
nonce: int,
|
|
175
|
+
position_id: int,
|
|
176
|
+
expiration_timestamp: datetime,
|
|
177
|
+
public_key: int,
|
|
178
|
+
starknet_domain: StarknetDomain,
|
|
179
|
+
) -> int:
|
|
180
|
+
synthetic_asset = amount_synthetic.asset
|
|
181
|
+
collateral_asset = amount_collateral.asset
|
|
182
|
+
|
|
183
|
+
expire_time_with_buffer = expiration_timestamp + timedelta(days=14)
|
|
184
|
+
expire_time_as_seconds = math.ceil(expire_time_with_buffer.timestamp())
|
|
185
|
+
|
|
186
|
+
return get_order_msg_hash(
|
|
187
|
+
position_id=position_id,
|
|
188
|
+
base_asset_id=int(synthetic_asset.settlement_external_id, 16),
|
|
189
|
+
base_amount=amount_synthetic.value,
|
|
190
|
+
quote_asset_id=int(collateral_asset.settlement_external_id, 16),
|
|
191
|
+
quote_amount=amount_collateral.value,
|
|
192
|
+
fee_amount=max_fee.value,
|
|
193
|
+
fee_asset_id=int(collateral_asset.settlement_external_id, 16),
|
|
194
|
+
expiration=expire_time_as_seconds,
|
|
195
|
+
salt=nonce,
|
|
196
|
+
user_public_key=public_key,
|
|
197
|
+
domain_name=starknet_domain.name,
|
|
198
|
+
domain_version=starknet_domain.version,
|
|
199
|
+
domain_chain_id=starknet_domain.chain_id,
|
|
200
|
+
domain_revision=starknet_domain.revision,
|
|
201
|
+
)
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import dataclasses
|
|
3
|
+
import decimal
|
|
4
|
+
from typing import Callable, Iterable, Tuple
|
|
5
|
+
|
|
6
|
+
from sortedcontainers import SortedDict # type: ignore[import-untyped]
|
|
7
|
+
|
|
8
|
+
from x10.perpetual.configuration import EndpointConfig
|
|
9
|
+
from x10.perpetual.orderbooks import OrderbookUpdateModel
|
|
10
|
+
from x10.perpetual.stream_client.stream_client import PerpetualStreamClient
|
|
11
|
+
from x10.utils.http import StreamDataType
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclasses.dataclass
|
|
15
|
+
class OrderBookEntry:
|
|
16
|
+
price: decimal.Decimal
|
|
17
|
+
amount: decimal.Decimal
|
|
18
|
+
|
|
19
|
+
def __repr__(self) -> str:
|
|
20
|
+
return f"OrderBookEntry(price={self.price}, amount={self.amount})"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclasses.dataclass
|
|
24
|
+
class ImpactDetails:
|
|
25
|
+
price: decimal.Decimal
|
|
26
|
+
amount: decimal.Decimal
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class OrderBook:
|
|
30
|
+
@staticmethod
|
|
31
|
+
async def create(
|
|
32
|
+
endpoint_config: EndpointConfig,
|
|
33
|
+
market_name: str,
|
|
34
|
+
best_ask_change_callback: Callable[[OrderBookEntry], None] | None = None,
|
|
35
|
+
best_bid_change_callback: Callable[[OrderBookEntry], None] | None = None,
|
|
36
|
+
start=False,
|
|
37
|
+
) -> "OrderBook":
|
|
38
|
+
ob = OrderBook(
|
|
39
|
+
endpoint_config,
|
|
40
|
+
market_name,
|
|
41
|
+
best_ask_change_callback,
|
|
42
|
+
best_bid_change_callback,
|
|
43
|
+
)
|
|
44
|
+
if start:
|
|
45
|
+
await ob.start_orderbook()
|
|
46
|
+
return ob
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
endpoint_config: EndpointConfig,
|
|
51
|
+
market_name: str,
|
|
52
|
+
best_ask_change_callback: Callable[[OrderBookEntry], None] | None = None,
|
|
53
|
+
best_bid_change_callback: Callable[[OrderBookEntry], None] | None = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
self.__stream_client = PerpetualStreamClient(api_url=endpoint_config.stream_url)
|
|
56
|
+
self.__market_name = market_name
|
|
57
|
+
self.__task: asyncio.Task | None = None
|
|
58
|
+
self._bid_prices: SortedDict[decimal.Decimal, OrderBookEntry] = SortedDict()
|
|
59
|
+
self._ask_prices: SortedDict[decimal.Decimal, OrderBookEntry] = SortedDict()
|
|
60
|
+
self.best_ask_change_callback = best_ask_change_callback
|
|
61
|
+
self.best_bid_change_callback = best_bid_change_callback
|
|
62
|
+
|
|
63
|
+
def update_orderbook(self, data: OrderbookUpdateModel):
|
|
64
|
+
best_bid_before_update = self.best_bid()
|
|
65
|
+
for bid in data.bid:
|
|
66
|
+
if bid.price in self._bid_prices:
|
|
67
|
+
existing_bid_entry: OrderBookEntry = self._bid_prices.get(bid.price)
|
|
68
|
+
existing_bid_entry.amount = existing_bid_entry.amount + bid.qty
|
|
69
|
+
if existing_bid_entry.amount == 0:
|
|
70
|
+
del self._bid_prices[bid.price]
|
|
71
|
+
else:
|
|
72
|
+
self._bid_prices[bid.price] = OrderBookEntry(
|
|
73
|
+
price=bid.price,
|
|
74
|
+
amount=bid.qty,
|
|
75
|
+
)
|
|
76
|
+
now_best_bid = self.best_bid()
|
|
77
|
+
if now_best_bid and best_bid_before_update != now_best_bid:
|
|
78
|
+
if self.best_bid_change_callback:
|
|
79
|
+
self.best_bid_change_callback(now_best_bid)
|
|
80
|
+
|
|
81
|
+
best_ask_before_update = self.best_ask()
|
|
82
|
+
for ask in data.ask:
|
|
83
|
+
if ask.price in self._ask_prices:
|
|
84
|
+
existing_ask_entry: OrderBookEntry = self._ask_prices.get(ask.price)
|
|
85
|
+
existing_ask_entry.amount = existing_ask_entry.amount + ask.qty
|
|
86
|
+
if existing_ask_entry.amount == 0:
|
|
87
|
+
del self._ask_prices[ask.price]
|
|
88
|
+
else:
|
|
89
|
+
self._ask_prices[ask.price] = OrderBookEntry(
|
|
90
|
+
price=ask.price,
|
|
91
|
+
amount=ask.qty,
|
|
92
|
+
)
|
|
93
|
+
now_best_ask = self.best_ask()
|
|
94
|
+
if now_best_ask and best_ask_before_update != now_best_ask:
|
|
95
|
+
if self.best_ask_change_callback:
|
|
96
|
+
self.best_ask_change_callback(now_best_ask)
|
|
97
|
+
|
|
98
|
+
def init_orderbook(self, data: OrderbookUpdateModel):
|
|
99
|
+
for bid in data.bid:
|
|
100
|
+
self._bid_prices[bid.price] = OrderBookEntry(
|
|
101
|
+
price=bid.price,
|
|
102
|
+
amount=bid.qty,
|
|
103
|
+
)
|
|
104
|
+
for ask in data.ask:
|
|
105
|
+
self._ask_prices[ask.price] = OrderBookEntry(
|
|
106
|
+
price=ask.price,
|
|
107
|
+
amount=ask.qty,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
async def start_orderbook(self) -> asyncio.Task:
|
|
111
|
+
loop = asyncio.get_running_loop()
|
|
112
|
+
|
|
113
|
+
async def inner():
|
|
114
|
+
async with self.__stream_client.subscribe_to_orderbooks(self.__market_name) as stream:
|
|
115
|
+
async for event in stream:
|
|
116
|
+
if event.type == StreamDataType.SNAPSHOT.value:
|
|
117
|
+
self.init_orderbook(event.data)
|
|
118
|
+
elif event.type == StreamDataType.DELTA.value:
|
|
119
|
+
self.update_orderbook(event.data)
|
|
120
|
+
|
|
121
|
+
self.__task = loop.create_task(inner())
|
|
122
|
+
return self.__task
|
|
123
|
+
|
|
124
|
+
def stop_orderbook(self):
|
|
125
|
+
if self.__task:
|
|
126
|
+
self.__task.cancel()
|
|
127
|
+
self.__task = None
|
|
128
|
+
|
|
129
|
+
def best_bid(self) -> OrderBookEntry | None:
|
|
130
|
+
try:
|
|
131
|
+
entry = self._bid_prices.peekitem(-1)
|
|
132
|
+
return entry[1]
|
|
133
|
+
except IndexError:
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
def best_ask(self) -> OrderBookEntry | None:
|
|
137
|
+
try:
|
|
138
|
+
entry = self._ask_prices.peekitem(0)
|
|
139
|
+
return entry[1]
|
|
140
|
+
except IndexError:
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
def __price_impact_notional(
|
|
144
|
+
self, notional: decimal.Decimal, levels: Iterable[Tuple[decimal.Decimal, OrderBookEntry]]
|
|
145
|
+
):
|
|
146
|
+
remaining_to_spend = notional
|
|
147
|
+
total_amount = decimal.Decimal(0)
|
|
148
|
+
weighted_sum = decimal.Decimal(0)
|
|
149
|
+
for price, entry in levels:
|
|
150
|
+
available_at_price = entry.amount
|
|
151
|
+
amount_to_purchase = min(remaining_to_spend / price, available_at_price)
|
|
152
|
+
if remaining_to_spend <= 0:
|
|
153
|
+
break
|
|
154
|
+
if available_at_price <= 0:
|
|
155
|
+
continue
|
|
156
|
+
take = amount_to_purchase
|
|
157
|
+
spent = take * price
|
|
158
|
+
weighted_sum += take * price
|
|
159
|
+
total_amount += take
|
|
160
|
+
remaining_to_spend -= spent
|
|
161
|
+
|
|
162
|
+
if remaining_to_spend > 0:
|
|
163
|
+
return None
|
|
164
|
+
average_price = weighted_sum / total_amount
|
|
165
|
+
return ImpactDetails(price=average_price, amount=total_amount)
|
|
166
|
+
|
|
167
|
+
def __price_impact_qty(self, qty: decimal.Decimal, levels: Iterable[Tuple[decimal.Decimal, OrderBookEntry]]):
|
|
168
|
+
remaining_qty = qty
|
|
169
|
+
total_amount = decimal.Decimal(0)
|
|
170
|
+
total_spent = decimal.Decimal(0)
|
|
171
|
+
for price, entry in levels:
|
|
172
|
+
available_at_price = entry.amount
|
|
173
|
+
take = min(remaining_qty, available_at_price)
|
|
174
|
+
if remaining_qty <= 0:
|
|
175
|
+
break
|
|
176
|
+
if available_at_price <= 0:
|
|
177
|
+
continue
|
|
178
|
+
total_spent += take * price
|
|
179
|
+
total_amount += take
|
|
180
|
+
remaining_qty -= take
|
|
181
|
+
|
|
182
|
+
if remaining_qty > 0:
|
|
183
|
+
return None
|
|
184
|
+
average_price = total_spent / total_amount
|
|
185
|
+
return ImpactDetails(price=average_price, amount=total_amount)
|
|
186
|
+
|
|
187
|
+
def calculate_price_impact_notional(self, notional: decimal.Decimal, side: str) -> ImpactDetails | None:
|
|
188
|
+
if notional <= 0:
|
|
189
|
+
return None
|
|
190
|
+
if side == "SELL":
|
|
191
|
+
if not self._bid_prices:
|
|
192
|
+
return None
|
|
193
|
+
return self.__price_impact_notional(notional, reversed(self._bid_prices.items()))
|
|
194
|
+
elif side == "BUY":
|
|
195
|
+
if not self._ask_prices:
|
|
196
|
+
return None
|
|
197
|
+
return self.__price_impact_notional(notional, self._ask_prices.items())
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
def calculate_price_impact_qty(self, qty: decimal.Decimal, side: str) -> ImpactDetails | None:
|
|
201
|
+
if qty <= 0:
|
|
202
|
+
return None
|
|
203
|
+
if side == "SELL":
|
|
204
|
+
if not self._bid_prices:
|
|
205
|
+
return None
|
|
206
|
+
return self.__price_impact_qty(qty, reversed(self._bid_prices.items()))
|
|
207
|
+
elif side == "BUY":
|
|
208
|
+
if not self._ask_prices:
|
|
209
|
+
return None
|
|
210
|
+
return self.__price_impact_qty(qty, self._ask_prices.items())
|
|
211
|
+
return None
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from pydantic import AliasChoices, Field
|
|
5
|
+
|
|
6
|
+
from x10.utils.model import X10BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OrderbookQuantityModel(X10BaseModel):
|
|
10
|
+
qty: Decimal = Field(validation_alias=AliasChoices("qty", "q"), serialization_alias="q")
|
|
11
|
+
price: Decimal = Field(validation_alias=AliasChoices("price", "p"), serialization_alias="p")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OrderbookUpdateModel(X10BaseModel):
|
|
15
|
+
market: str = Field(validation_alias=AliasChoices("market", "m"), serialization_alias="m")
|
|
16
|
+
bid: List[OrderbookQuantityModel] = Field(validation_alias=AliasChoices("bid", "b"), serialization_alias="b")
|
|
17
|
+
ask: List[OrderbookQuantityModel] = Field(validation_alias=AliasChoices("ask", "a"), serialization_alias="a")
|
x10/perpetual/orders.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from x10.utils.model import HexValue, SettlementSignatureModel, X10BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TimeInForce(Enum):
|
|
9
|
+
GTT = "GTT"
|
|
10
|
+
IOC = "IOC"
|
|
11
|
+
FOK = "FOK"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OrderSide(Enum):
|
|
15
|
+
BUY = "BUY"
|
|
16
|
+
SELL = "SELL"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OrderType(Enum):
|
|
20
|
+
LIMIT = "LIMIT"
|
|
21
|
+
CONDITIONAL = "CONDITIONAL"
|
|
22
|
+
MARKET = "MARKET"
|
|
23
|
+
TPSL = "TPSL"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OrderTpslType(Enum):
|
|
27
|
+
ORDER = "ORDER"
|
|
28
|
+
POSITION = "POSITION"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class OrderStatus(Enum):
|
|
32
|
+
# Technical status
|
|
33
|
+
UNKNOWN = "UNKNOWN"
|
|
34
|
+
|
|
35
|
+
NEW = "NEW"
|
|
36
|
+
UNTRIGGERED = "UNTRIGGERED"
|
|
37
|
+
PARTIALLY_FILLED = "PARTIALLY_FILLED"
|
|
38
|
+
FILLED = "FILLED"
|
|
39
|
+
CANCELLED = "CANCELLED"
|
|
40
|
+
EXPIRED = "EXPIRED"
|
|
41
|
+
REJECTED = "REJECTED"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class OrderStatusReason(Enum):
|
|
45
|
+
# Technical status
|
|
46
|
+
UNKNOWN = "UNKNOWN"
|
|
47
|
+
|
|
48
|
+
NONE = "NONE"
|
|
49
|
+
UNKNOWN_MARKET = "UNKNOWN_MARKET"
|
|
50
|
+
DISABLED_MARKET = "DISABLED_MARKET"
|
|
51
|
+
NOT_ENOUGH_FUNDS = "NOT_ENOUGH_FUNDS"
|
|
52
|
+
NO_LIQUIDITY = "NO_LIQUIDITY"
|
|
53
|
+
INVALID_FEE = "INVALID_FEE"
|
|
54
|
+
INVALID_QTY = "INVALID_QTY"
|
|
55
|
+
INVALID_PRICE = "INVALID_PRICE"
|
|
56
|
+
INVALID_VALUE = "INVALID_VALUE"
|
|
57
|
+
UNKNOWN_ACCOUNT = "UNKNOWN_ACCOUNT"
|
|
58
|
+
SELF_TRADE_PROTECTION = "SELF_TRADE_PROTECTION"
|
|
59
|
+
POST_ONLY_FAILED = "POST_ONLY_FAILED"
|
|
60
|
+
REDUCE_ONLY_FAILED = "REDUCE_ONLY_FAILED"
|
|
61
|
+
INVALID_EXPIRE_TIME = "INVALID_EXPIRE_TIME"
|
|
62
|
+
POSITION_TPSL_CONFLICT = "POSITION_TPSL_CONFLICT"
|
|
63
|
+
INVALID_LEVERAGE = "INVALID_LEVERAGE"
|
|
64
|
+
PREV_ORDER_NOT_FOUND = "PREV_ORDER_NOT_FOUND"
|
|
65
|
+
PREV_ORDER_TRIGGERED = "PREV_ORDER_TRIGGERED"
|
|
66
|
+
TPSL_OTHER_SIDE_FILLED = "TPSL_OTHER_SIDE_FILLED"
|
|
67
|
+
PREV_ORDER_CONFLICT = "PREV_ORDER_CONFLICT"
|
|
68
|
+
ORDER_REPLACED = "ORDER_REPLACED"
|
|
69
|
+
POST_ONLY_MODE = "POST_ONLY_MODE"
|
|
70
|
+
REDUCE_ONLY_MODE = "REDUCE_ONLY_MODE"
|
|
71
|
+
TRADING_OFF_MODE = "TRADING_OFF_MODE"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class OrderTriggerPriceType(Enum):
|
|
75
|
+
# Technical status
|
|
76
|
+
UNKNOWN = "UNKNOWN"
|
|
77
|
+
|
|
78
|
+
MARK = "MARK"
|
|
79
|
+
INDEX = "INDEX"
|
|
80
|
+
LAST = "LAST"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class OrderTriggerDirection(Enum):
|
|
84
|
+
# Technical status
|
|
85
|
+
UNKNOWN = "UNKNOWN"
|
|
86
|
+
|
|
87
|
+
UP = "UP"
|
|
88
|
+
DOWN = "DOWN"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class OrderPriceType(Enum):
|
|
92
|
+
# Technical status
|
|
93
|
+
UNKNOWN = "UNKNOWN"
|
|
94
|
+
|
|
95
|
+
MARKET = "MARKET"
|
|
96
|
+
LIMIT = "LIMIT"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class SelfTradeProtectionLevel(Enum):
|
|
100
|
+
DISABLED = "DISABLED"
|
|
101
|
+
ACCOUNT = "ACCOUNT"
|
|
102
|
+
CLIENT = "CLIENT"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class StarkSettlementModel(X10BaseModel):
|
|
106
|
+
signature: SettlementSignatureModel
|
|
107
|
+
stark_key: HexValue
|
|
108
|
+
collateral_position: Decimal
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class StarkDebuggingOrderAmountsModel(X10BaseModel):
|
|
112
|
+
collateral_amount: Decimal
|
|
113
|
+
fee_amount: Decimal
|
|
114
|
+
synthetic_amount: Decimal
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class CreateOrderConditionalTriggerModel(X10BaseModel):
|
|
118
|
+
trigger_price: Decimal
|
|
119
|
+
trigger_price_type: OrderTriggerPriceType
|
|
120
|
+
direction: OrderTriggerDirection
|
|
121
|
+
execution_price_type: OrderPriceType
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class CreateOrderTpslTriggerModel(X10BaseModel):
|
|
125
|
+
trigger_price: Decimal
|
|
126
|
+
trigger_price_type: OrderTriggerPriceType
|
|
127
|
+
price: Decimal
|
|
128
|
+
price_type: OrderPriceType
|
|
129
|
+
settlement: StarkSettlementModel
|
|
130
|
+
debugging_amounts: Optional[StarkDebuggingOrderAmountsModel] = None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class PerpetualOrderModel(X10BaseModel):
|
|
134
|
+
id: str
|
|
135
|
+
market: str
|
|
136
|
+
type: OrderType
|
|
137
|
+
side: OrderSide
|
|
138
|
+
qty: Decimal
|
|
139
|
+
price: Decimal
|
|
140
|
+
reduce_only: bool = False
|
|
141
|
+
post_only: bool = False
|
|
142
|
+
time_in_force: TimeInForce
|
|
143
|
+
expiry_epoch_millis: int
|
|
144
|
+
fee: Decimal
|
|
145
|
+
nonce: Decimal
|
|
146
|
+
self_trade_protection_level: SelfTradeProtectionLevel
|
|
147
|
+
cancel_id: Optional[str] = None
|
|
148
|
+
settlement: Optional[StarkSettlementModel] = None
|
|
149
|
+
trigger: Optional[CreateOrderConditionalTriggerModel] = None
|
|
150
|
+
tp_sl_type: Optional[OrderTpslType] = None
|
|
151
|
+
take_profit: Optional[CreateOrderTpslTriggerModel] = None
|
|
152
|
+
stop_loss: Optional[CreateOrderTpslTriggerModel] = None
|
|
153
|
+
debugging_amounts: Optional[StarkDebuggingOrderAmountsModel] = None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class PlacedOrderModel(X10BaseModel):
|
|
157
|
+
id: int
|
|
158
|
+
external_id: str
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class OpenOrderModel(X10BaseModel):
|
|
162
|
+
id: int
|
|
163
|
+
account_id: int
|
|
164
|
+
external_id: str
|
|
165
|
+
market: str
|
|
166
|
+
type: OrderType
|
|
167
|
+
side: OrderSide
|
|
168
|
+
status: OrderStatus
|
|
169
|
+
status_reason: Optional[OrderStatusReason] = None
|
|
170
|
+
price: Decimal
|
|
171
|
+
average_price: Optional[Decimal] = None
|
|
172
|
+
qty: Decimal
|
|
173
|
+
filled_qty: Optional[Decimal] = None
|
|
174
|
+
reduce_only: bool
|
|
175
|
+
post_only: bool
|
|
176
|
+
payed_fee: Optional[Decimal] = None
|
|
177
|
+
created_time: int
|
|
178
|
+
updated_time: int
|
|
179
|
+
expiry_time: Optional[int] = None
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from x10.utils.model import X10BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ExitType(Enum):
|
|
9
|
+
TRADE = "TRADE"
|
|
10
|
+
LIQUIDATION = "LIQUIDATION"
|
|
11
|
+
ADL = "ADL"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PositionSide(Enum):
|
|
15
|
+
LONG = "LONG"
|
|
16
|
+
SHORT = "SHORT"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PositionModel(X10BaseModel):
|
|
20
|
+
id: int
|
|
21
|
+
account_id: int
|
|
22
|
+
market: str
|
|
23
|
+
side: PositionSide
|
|
24
|
+
leverage: Decimal
|
|
25
|
+
size: Decimal
|
|
26
|
+
value: Decimal
|
|
27
|
+
open_price: Decimal
|
|
28
|
+
mark_price: Decimal
|
|
29
|
+
liquidation_price: Optional[Decimal] = None
|
|
30
|
+
unrealised_pnl: Decimal
|
|
31
|
+
realised_pnl: Decimal
|
|
32
|
+
tp_price: Optional[Decimal] = None
|
|
33
|
+
sl_price: Optional[Decimal] = None
|
|
34
|
+
adl: Optional[int] = None
|
|
35
|
+
created_at: int
|
|
36
|
+
updated_at: int
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PositionHistoryModel(X10BaseModel):
|
|
40
|
+
id: int
|
|
41
|
+
account_id: int
|
|
42
|
+
market: str
|
|
43
|
+
side: PositionSide
|
|
44
|
+
leverage: Decimal
|
|
45
|
+
size: Decimal
|
|
46
|
+
open_price: Decimal
|
|
47
|
+
exit_type: Optional[ExitType]
|
|
48
|
+
exit_price: Optional[Decimal]
|
|
49
|
+
realised_pnl: Decimal
|
|
50
|
+
created_time: int
|
|
51
|
+
closed_time: Optional[int]
|