x10-python-trading-starknet 0.0.2__tar.gz → 0.0.5__tar.gz

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 (50) hide show
  1. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/PKG-INFO +1 -1
  2. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/pyproject.toml +1 -1
  3. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/candles.py +4 -2
  4. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/configuration.py +1 -1
  5. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/markets.py +3 -0
  6. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/order_object.py +2 -2
  7. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/orderbook.py +23 -14
  8. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/positions.py +3 -3
  9. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/simple_client/simple_trading_client.py +71 -46
  10. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/stream_client/perpetual_stream_connection.py +6 -6
  11. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/stream_client/stream_client.py +2 -2
  12. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/trading_client/order_management_module.py +4 -4
  13. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/trading_client/trading_client.py +3 -1
  14. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/transfer_object.py +2 -2
  15. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/user_client/onboarding.py +18 -10
  16. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/user_client/user_client.py +2 -0
  17. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/LICENSE +0 -0
  18. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/README.md +0 -0
  19. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/__init__.py +0 -0
  20. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/config.py +0 -0
  21. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/errors.py +0 -0
  22. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/__init__.py +0 -0
  23. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/accounts.py +0 -0
  24. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/amounts.py +0 -0
  25. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/assets.py +0 -0
  26. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/balances.py +0 -0
  27. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/fees.py +0 -0
  28. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/funding_rates.py +0 -0
  29. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/orderbooks.py +0 -0
  30. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/orders.py +0 -0
  31. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/stream_client/__init__.py +0 -0
  32. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/trades.py +0 -0
  33. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/trading_client/__init__.py +0 -0
  34. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/trading_client/account_module.py +0 -0
  35. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/trading_client/base_module.py +0 -0
  36. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/trading_client/info_module.py +0 -0
  37. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/trading_client/markets_information_module.py +0 -0
  38. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/trading_client/testnet_module.py +0 -0
  39. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/transfers.py +0 -0
  40. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/user_client/__init__.py +0 -0
  41. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/user_client/l1_signing.py +0 -0
  42. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/withdrawal_object.py +0 -0
  43. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/perpetual/withdrawals.py +0 -0
  44. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/utils/__init__.py +0 -0
  45. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/utils/date.py +0 -0
  46. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/utils/http.py +0 -0
  47. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/utils/log.py +0 -0
  48. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/utils/model.py +0 -0
  49. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/utils/nonce.py +0 -0
  50. {x10_python_trading_starknet-0.0.2 → x10_python_trading_starknet-0.0.5}/x10/utils/string.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: x10-python-trading-starknet
3
- Version: 0.0.2
3
+ Version: 0.0.5
4
4
  Summary: Python client for X10 API
5
5
  Home-page: https://github.com/x10xchange/python_sdk
6
6
  Author: X10
@@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api"
5
5
 
6
6
  [tool.poetry]
7
7
  name = "x10-python-trading-starknet"
8
- version = "0.0.2"
8
+ version = "0.0.5"
9
9
  description = "Python client for X10 API"
10
10
  authors = ["X10 <tech@ex10.org>"]
11
11
  repository = "https://github.com/x10xchange/python_sdk"
@@ -1,5 +1,5 @@
1
1
  from decimal import Decimal
2
- from typing import Literal
2
+ from typing import Literal, Optional
3
3
 
4
4
  from pydantic import AliasChoices, Field
5
5
 
@@ -14,5 +14,7 @@ class CandleModel(X10BaseModel):
14
14
  low: Decimal = Field(validation_alias=AliasChoices("low", "l"), serialization_alias="l")
15
15
  high: Decimal = Field(validation_alias=AliasChoices("high", "h"), serialization_alias="h")
16
16
  close: Decimal = Field(validation_alias=AliasChoices("close", "c"), serialization_alias="c")
17
- volume: Decimal = Field(validation_alias=AliasChoices("volume", "v"), serialization_alias="v")
17
+ volume: Optional[Decimal] = Field(
18
+ validation_alias=AliasChoices("volume", "v"), serialization_alias="v", default=None
19
+ )
18
20
  timestamp: int = Field(validation_alias=AliasChoices("timestamp", "T"), serialization_alias="T")
@@ -74,7 +74,7 @@ MAINNET_CONFIG_LEGACY_SIGNING_DOMAIN = EndpointConfig(
74
74
  STARKNET_TESTNET_CONFIG = EndpointConfig(
75
75
  chain_rpc_url="https://rpc.sepolia.org",
76
76
  api_base_url="https://api.starknet.sepolia.extended.exchange/api/v1",
77
- stream_url="wss://api.starknet.sepolia.extended.exchange/stream.extended.exchange/v1",
77
+ stream_url="wss://starknet.sepolia.extended.exchange/stream.extended.exchange/v1",
78
78
  onboarding_url="https://api.starknet.sepolia.extended.exchange",
79
79
  signing_domain="starknet.sepolia.extended.exchange",
80
80
  collateral_asset_contract="0x0C9165046063B7bCD05C6924Bbe05ed535c140a1",
@@ -76,6 +76,9 @@ class TradingConfigModel(X10BaseModel):
76
76
  else:
77
77
  return Decimal(0)
78
78
 
79
+ def round_price(self, price: Decimal, rounding_direction: str = ROUND_CEILING) -> Decimal:
80
+ return self.price_precision * (price / self.price_precision).to_integral_exact(rounding_direction)
81
+
79
82
 
80
83
  class L2ConfigModel(X10BaseModel):
81
84
  type: str
@@ -38,7 +38,7 @@ def create_order_object(
38
38
  side: OrderSide,
39
39
  starknet_domain: StarknetDomain,
40
40
  post_only: bool = False,
41
- previous_order_id: Optional[str] = None,
41
+ previous_order_external_id: Optional[str] = None,
42
42
  expire_time: Optional[datetime] = None,
43
43
  order_external_id: Optional[str] = None,
44
44
  time_in_force: TimeInForce = TimeInForce.GTT,
@@ -66,7 +66,7 @@ def create_order_object(
66
66
  exact_only=False,
67
67
  expire_time=expire_time,
68
68
  post_only=post_only,
69
- previous_order_external_id=previous_order_id,
69
+ previous_order_external_id=previous_order_external_id,
70
70
  order_external_id=order_external_id,
71
71
  time_in_force=time_in_force,
72
72
  self_trade_protection_level=self_trade_protection_level,
@@ -31,9 +31,10 @@ class OrderBook:
31
31
  async def create(
32
32
  endpoint_config: EndpointConfig,
33
33
  market_name: str,
34
- best_ask_change_callback: Callable[[OrderBookEntry], None] | None = None,
35
- best_bid_change_callback: Callable[[OrderBookEntry], None] | None = None,
34
+ best_ask_change_callback: Callable[[OrderBookEntry | None], None] | None = None,
35
+ best_bid_change_callback: Callable[[OrderBookEntry | None], None] | None = None,
36
36
  start=False,
37
+ depth_1: bool = False,
37
38
  ) -> "OrderBook":
38
39
  ob = OrderBook(
39
40
  endpoint_config,
@@ -49,8 +50,8 @@ class OrderBook:
49
50
  self,
50
51
  endpoint_config: EndpointConfig,
51
52
  market_name: str,
52
- best_ask_change_callback: Callable[[OrderBookEntry], None] | None = None,
53
- best_bid_change_callback: Callable[[OrderBookEntry], None] | None = None,
53
+ best_ask_change_callback: Callable[[OrderBookEntry | None], None] | None = None,
54
+ best_bid_change_callback: Callable[[OrderBookEntry | None], None] | None = None,
54
55
  ) -> None:
55
56
  self.__stream_client = PerpetualStreamClient(api_url=endpoint_config.stream_url)
56
57
  self.__market_name = market_name
@@ -64,7 +65,7 @@ class OrderBook:
64
65
  best_bid_before_update = self.best_bid()
65
66
  for bid in data.bid:
66
67
  if bid.price in self._bid_prices:
67
- existing_bid_entry: OrderBookEntry = self._bid_prices.get(bid.price)
68
+ existing_bid_entry: OrderBookEntry = self._bid_prices[bid.price]
68
69
  existing_bid_entry.amount = existing_bid_entry.amount + bid.qty
69
70
  if existing_bid_entry.amount == 0:
70
71
  del self._bid_prices[bid.price]
@@ -74,14 +75,14 @@ class OrderBook:
74
75
  amount=bid.qty,
75
76
  )
76
77
  now_best_bid = self.best_bid()
77
- if now_best_bid and best_bid_before_update != now_best_bid:
78
+ if best_bid_before_update != now_best_bid:
78
79
  if self.best_bid_change_callback:
79
80
  self.best_bid_change_callback(now_best_bid)
80
81
 
81
82
  best_ask_before_update = self.best_ask()
82
83
  for ask in data.ask:
83
84
  if ask.price in self._ask_prices:
84
- existing_ask_entry: OrderBookEntry = self._ask_prices.get(ask.price)
85
+ existing_ask_entry: OrderBookEntry = self._ask_prices[ask.price]
85
86
  existing_ask_entry.amount = existing_ask_entry.amount + ask.qty
86
87
  if existing_ask_entry.amount == 0:
87
88
  del self._ask_prices[ask.price]
@@ -91,7 +92,7 @@ class OrderBook:
91
92
  amount=ask.qty,
92
93
  )
93
94
  now_best_ask = self.best_ask()
94
- if now_best_ask and best_ask_before_update != now_best_ask:
95
+ if best_ask_before_update != now_best_ask:
95
96
  if self.best_ask_change_callback:
96
97
  self.best_ask_change_callback(now_best_ask)
97
98
 
@@ -111,12 +112,20 @@ class OrderBook:
111
112
  loop = asyncio.get_running_loop()
112
113
 
113
114
  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)
115
+ while True:
116
+ print(f"Connecting to orderbook stream for market: {self.__market_name}")
117
+ async with self.__stream_client.subscribe_to_orderbooks(self.__market_name) as stream:
118
+ async for event in stream:
119
+ if event.type == StreamDataType.SNAPSHOT.value:
120
+ if not event.data:
121
+ continue
122
+ self.init_orderbook(event.data)
123
+ elif event.type == StreamDataType.DELTA.value:
124
+ if not event.data:
125
+ continue
126
+ self.update_orderbook(event.data)
127
+ print("Orderbook stream disconnected, reconnecting...")
128
+ await asyncio.sleep(1)
120
129
 
121
130
  self.__task = loop.create_task(inner())
122
131
  return self.__task
@@ -44,8 +44,8 @@ class PositionHistoryModel(X10BaseModel):
44
44
  leverage: Decimal
45
45
  size: Decimal
46
46
  open_price: Decimal
47
- exit_type: Optional[ExitType]
48
- exit_price: Optional[Decimal]
47
+ exit_type: Optional[ExitType] = None
48
+ exit_price: Optional[Decimal] = None
49
49
  realised_pnl: Decimal
50
50
  created_time: int
51
- closed_time: Optional[int]
51
+ closed_time: Optional[int] = None
@@ -25,12 +25,12 @@ from x10.perpetual.trading_client.order_management_module import OrderManagement
25
25
  from x10.utils.http import WrappedStreamResponse
26
26
 
27
27
 
28
- async def condition_to_awaitable(condition: asyncio.Condition) -> Awaitable:
28
+ def condition_to_awaitable(condition: asyncio.Condition) -> Awaitable:
29
29
  async def __inner():
30
30
  async with condition:
31
31
  await condition.wait()
32
32
 
33
- return await __inner()
33
+ return __inner()
34
34
 
35
35
 
36
36
  class TimedOpenOrderModel(OpenOrderModel):
@@ -74,6 +74,10 @@ class CancelWaiter:
74
74
 
75
75
  class BlockingTradingClient:
76
76
  def __init__(self, endpoint_config: EndpointConfig, account: StarkPerpetualAccount):
77
+ if not asyncio.get_event_loop().is_running():
78
+ raise RuntimeError(
79
+ "BlockingTradingClient must be initialized from an async function, use BlockingTradingClient.create()"
80
+ )
77
81
  self.__endpoint_config = endpoint_config
78
82
  self.__account = account
79
83
  self.__market_module = MarketsInformationModule(endpoint_config, api_key=account.api_key)
@@ -85,14 +89,19 @@ class BlockingTradingClient:
85
89
  PerpetualStreamConnection[WrappedStreamResponse[AccountStreamDataModel]],
86
90
  ] = None
87
91
  self.__order_waiters: Dict[str, OrderWaiter] = {}
88
- self.__cancel_waiters: Dict[int, CancelWaiter] = {}
89
- self.__orders_task: Union[None, asyncio.Task] = None
90
- self.__stream_lock = asyncio.Lock()
92
+ self.__cancel_waiters: Dict[str, CancelWaiter] = {}
93
+ self.__stream_task = asyncio.create_task(self.___order_stream())
94
+
95
+ @staticmethod
96
+ async def create(endpoint_config: EndpointConfig, account: StarkPerpetualAccount) -> "BlockingTradingClient":
97
+ client = BlockingTradingClient(endpoint_config, account)
98
+ await client.__stream_client.subscribe_to_account_updates(account.api_key)
99
+ return client
91
100
 
92
- async def handle_cancel(self, order_id: int):
93
- if order_id not in self.__cancel_waiters:
101
+ async def __handle_cancel(self, order_external_id: str):
102
+ if order_external_id not in self.__cancel_waiters:
94
103
  return
95
- cancel_waiter = self.__cancel_waiters.get(order_id)
104
+ cancel_waiter = self.__cancel_waiters.get(order_external_id)
96
105
  if not cancel_waiter:
97
106
  return
98
107
  if cancel_waiter.condition:
@@ -100,57 +109,61 @@ class BlockingTradingClient:
100
109
  cancel_waiter.end_nanos = time.time_ns()
101
110
  cancel_waiter.condition.notify_all()
102
111
 
103
- async def handle_update(self, order: OpenOrderModel):
104
- if order.external_id not in self.__order_waiters:
105
- return
106
- order_waiter = self.__order_waiters.get(order.external_id)
107
- if not order_waiter:
108
- return
109
- if order_waiter.condition:
110
- async with order_waiter.condition:
111
- order_waiter.open_order = TimedOpenOrderModel(
112
- start_nanos=order_waiter.start_nanos,
113
- end_nanos=time.time_ns(),
114
- open_order=order,
115
- )
116
- order_waiter.condition.notify_all()
117
-
118
- async def handle_order(self, order: OpenOrderModel):
112
+ async def __handle_update(self, order: OpenOrderModel):
113
+ if order.status == OrderStatus.NEW.value:
114
+ if order.external_id not in self.__order_waiters:
115
+ return
116
+ order_waiter = self.__order_waiters.get(order.external_id)
117
+ if not order_waiter:
118
+ return
119
+ if order_waiter.condition:
120
+ async with order_waiter.condition:
121
+ order_waiter.open_order = TimedOpenOrderModel(
122
+ start_nanos=order_waiter.start_nanos,
123
+ end_nanos=time.time_ns(),
124
+ open_order=order,
125
+ )
126
+ order_waiter.condition.notify_all()
127
+
128
+ async def __handle_order(self, order: OpenOrderModel):
119
129
  if order.status == OrderStatus.CANCELLED.value:
120
- await self.handle_cancel(order.id)
130
+ await self.__handle_cancel(order.external_id)
121
131
  else:
122
- await self.handle_update(order)
132
+ await self.__handle_update(order)
123
133
 
124
134
  async def ___order_stream(self):
135
+ self.__account_stream = await self.__stream_client.subscribe_to_account_updates(self.__account.api_key)
125
136
  async for event in self.__account_stream:
126
137
  if not (event.data and event.data.orders):
127
138
  continue
128
139
  for order in event.data.orders:
129
- await self.handle_order(order)
140
+ await self.__handle_order(order)
141
+ print("Order stream closed, reconnecting...")
142
+ await self.___order_stream()
130
143
 
131
- async def cancel_order(self, order_id: int) -> TimedCancel:
144
+ async def cancel_order(self, order_external_id: str) -> TimedCancel:
132
145
  awaitable: Awaitable
133
- if order_id in self.__cancel_waiters:
134
- awaitable = condition_to_awaitable(self.__cancel_waiters[order_id].condition)
146
+ if order_external_id in self.__cancel_waiters:
147
+ awaitable = condition_to_awaitable(self.__cancel_waiters[order_external_id].condition)
135
148
  else:
136
- self.__cancel_waiters[order_id] = CancelWaiter(
149
+ self.__cancel_waiters[order_external_id] = CancelWaiter(
137
150
  asyncio.Condition(), start_nanos=time.time_ns(), end_nanos=None
138
151
  )
139
- cancel_task = asyncio.create_task(self.__orders_module.cancel_order(order_id))
152
+ cancel_task = asyncio.create_task(self.__orders_module.cancel_order_by_external_id(order_external_id))
140
153
  awaitable = asyncio.gather(
141
154
  cancel_task,
142
- asyncio.wait_for(condition_to_awaitable(self.__cancel_waiters[order_id].condition), 5),
155
+ asyncio.wait_for(condition_to_awaitable(self.__cancel_waiters[order_external_id].condition), 5),
143
156
  return_exceptions=False,
144
157
  )
145
158
 
146
- cancel_waiter = self.__cancel_waiters[order_id]
159
+ cancel_waiter = self.__cancel_waiters[order_external_id]
147
160
  end_nanos = None
148
161
  if cancel_waiter.end_nanos:
149
162
  end_nanos = cancel_waiter.end_nanos
150
163
  else:
151
164
  await awaitable
152
- end_nanos = self.__cancel_waiters[order_id].end_nanos
153
- del self.__cancel_waiters[order_id]
165
+ end_nanos = self.__cancel_waiters[order_external_id].end_nanos
166
+ del self.__cancel_waiters[order_external_id]
154
167
  end_nanos = cast(int, end_nanos)
155
168
  return TimedCancel(
156
169
  start_nanos=cancel_waiter.start_nanos,
@@ -161,9 +174,26 @@ class BlockingTradingClient:
161
174
  async def get_markets(self) -> Dict[str, MarketModel]:
162
175
  if not self.__markets:
163
176
  markets = await self.__market_module.get_markets()
164
- self.__markets = {m.name: m for m in markets.data}
177
+ market_data = markets.data
178
+ if not market_data:
179
+ raise ValueError("Core market data is empty, check your connection or API key.")
180
+ self.__markets = {m.name: m for m in market_data}
165
181
  return self.__markets
166
182
 
183
+ async def mass_cancel(
184
+ self,
185
+ order_ids: list[int] | None = None,
186
+ external_order_ids: list[str] | None = None,
187
+ markets: list[str] | None = None,
188
+ cancel_all: bool = False,
189
+ ) -> None:
190
+ await self.__orders_module.mass_cancel(
191
+ order_ids=order_ids,
192
+ external_order_ids=external_order_ids,
193
+ markets=markets,
194
+ cancel_all=cancel_all,
195
+ )
196
+
167
197
  async def create_and_place_order(
168
198
  self,
169
199
  market_name: str,
@@ -171,19 +201,13 @@ class BlockingTradingClient:
171
201
  price: Decimal,
172
202
  side: OrderSide,
173
203
  post_only: bool = False,
174
- previous_order_id: str | None = None,
204
+ previous_order_external_id: str | None = None,
205
+ external_id: str | None = None,
175
206
  ) -> TimedOpenOrderModel:
176
207
  market = (await self.get_markets()).get(market_name)
177
208
  if not market:
178
209
  raise ValueError(f"Market '{market_name}' not found.")
179
210
 
180
- if not self.__account_stream:
181
- await self.__stream_lock.acquire()
182
- if not self.__account_stream:
183
- self.__account_stream = await self.__stream_client.subscribe_to_account_updates(self.__account.api_key)
184
- self.__orders_task = asyncio.create_task(self.___order_stream())
185
- self.__stream_lock.release()
186
-
187
211
  order: PerpetualOrderModel = create_order_object(
188
212
  account=self.__account,
189
213
  market=market,
@@ -191,8 +215,9 @@ class BlockingTradingClient:
191
215
  price=price,
192
216
  side=side,
193
217
  post_only=post_only,
194
- previous_order_id=previous_order_id,
218
+ previous_order_external_id=previous_order_external_id,
195
219
  starknet_domain=self.__endpoint_config.starknet_domain,
220
+ order_external_id=external_id,
196
221
  )
197
222
 
198
223
  if order.id in self.__order_waiters:
@@ -43,10 +43,8 @@ class PerpetualStreamConnection(Generic[StreamMsgResponseType]):
43
43
 
44
44
  async def close(self):
45
45
  assert self.__websocket is not None
46
- assert not self.__websocket.closed
47
-
48
- await self.__websocket.close()
49
-
46
+ if not self.__websocket.closed:
47
+ await self.__websocket.close()
50
48
  LOGGER.debug("Stream closed: %s", self.__stream_url)
51
49
 
52
50
  @property
@@ -67,8 +65,10 @@ class PerpetualStreamConnection(Generic[StreamMsgResponseType]):
67
65
 
68
66
  if self.__websocket.closed:
69
67
  raise StopAsyncIteration
70
-
71
- return await self.__receive()
68
+ try:
69
+ return await self.__receive()
70
+ except websockets.ConnectionClosed:
71
+ raise StopAsyncIteration from None
72
72
 
73
73
  async def __receive(self) -> StreamMsgResponseType:
74
74
  assert self.__websocket is not None
@@ -24,12 +24,12 @@ class PerpetualStreamClient:
24
24
 
25
25
  self.__api_url = api_url
26
26
 
27
- def subscribe_to_orderbooks(self, market_name: Optional[str] = None):
27
+ def subscribe_to_orderbooks(self, market_name: Optional[str] = None, depth: int | None = None):
28
28
  """
29
29
  https://api.docs.extended.exchange/#orderbooks-stream
30
30
  """
31
31
 
32
- url = self.__get_url("/orderbooks/<market?>", market=market_name)
32
+ url = self.__get_url("/orderbooks/<market?>" + (f"?depth={depth}" if depth else ""), market=market_name)
33
33
  return self.__connect(url, WrappedStreamResponse[OrderbookUpdateModel])
34
34
 
35
35
  def subscribe_to_public_trades(self, market_name: Optional[str] = None):
@@ -10,10 +10,10 @@ LOGGER = get_logger(__name__)
10
10
 
11
11
 
12
12
  class _MassCancelRequestModel(X10BaseModel):
13
- order_ids: Optional[List[int]]
14
- external_order_ids: Optional[List[str]]
15
- markets: Optional[List[str]]
16
- cancel_all: Optional[bool]
13
+ order_ids: Optional[List[int]] = None
14
+ external_order_ids: Optional[List[str]] = None
15
+ markets: Optional[List[str]] = None
16
+ cancel_all: Optional[bool] = None
17
17
 
18
18
 
19
19
  class OrderManagementModule(BaseModule):
@@ -52,6 +52,7 @@ class PerpetualTradingClient:
52
52
  expire_time: Optional[datetime] = None,
53
53
  time_in_force: TimeInForce = TimeInForce.GTT,
54
54
  self_trade_protection_level: SelfTradeProtectionLevel = SelfTradeProtectionLevel.ACCOUNT,
55
+ external_id: Optional[str] = None,
55
56
  ) -> WrappedApiResponse[PlacedOrderModel]:
56
57
  if not self.__stark_account:
57
58
  raise ValueError("Stark account is not set")
@@ -74,11 +75,12 @@ class PerpetualTradingClient:
74
75
  price=price,
75
76
  side=side,
76
77
  post_only=post_only,
77
- previous_order_id=previous_order_id,
78
+ previous_order_external_id=previous_order_id,
78
79
  expire_time=expire_time,
79
80
  time_in_force=time_in_force,
80
81
  self_trade_protection_level=self_trade_protection_level,
81
82
  starknet_domain=self.__config.starknet_domain,
83
+ order_external_id=external_id,
82
84
  )
83
85
  return await self.__order_management_module.place_order(order)
84
86
 
@@ -55,13 +55,13 @@ def create_transfer_object(
55
55
  domain_version=starknet_domain.version,
56
56
  domain_chain_id=starknet_domain.chain_id,
57
57
  domain_revision=starknet_domain.revision,
58
- collateral_id=1,
58
+ collateral_id=int(config.collateral_asset_on_chain_id, base=16),
59
59
  )
60
60
 
61
61
  (transfer_signature_r, transfer_signature_s) = stark_account.sign(transfer_hash)
62
62
  settlement = StarkTransferSettlement(
63
63
  amount=int(stark_amount),
64
- asset_id=int(config.collateral_asset_id, base=16),
64
+ asset_id=int(config.collateral_asset_on_chain_id, base=16),
65
65
  expiration_timestamp=expiration_timestamp,
66
66
  nonce=nonce,
67
67
  receiver_position_id=to_vault,
@@ -39,6 +39,7 @@ class AccountRegistration:
39
39
  tos_accepted: bool
40
40
  time: datetime
41
41
  action: str
42
+ host: str
42
43
 
43
44
  def __post_init__(self):
44
45
  self.time_string = self.time.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
@@ -52,6 +53,7 @@ class AccountRegistration:
52
53
  "tosAccepted": self.tos_accepted,
53
54
  "time": self.time_string,
54
55
  "action": self.action,
56
+ "host": self.host,
55
57
  }
56
58
  types = {
57
59
  "EIP712Domain": [
@@ -63,6 +65,7 @@ class AccountRegistration:
63
65
  {"name": "tosAccepted", "type": "bool"},
64
66
  {"name": "time", "type": "string"},
65
67
  {"name": "action", "type": "string"},
68
+ {"name": "host", "type": "string"},
66
69
  ],
67
70
  }
68
71
  primary_type = "AccountRegistration"
@@ -81,6 +84,7 @@ class AccountRegistration:
81
84
  "tosAccepted": self.tos_accepted,
82
85
  "time": self.time_string,
83
86
  "action": self.action,
87
+ "host": self.host,
84
88
  }
85
89
 
86
90
 
@@ -127,7 +131,7 @@ class OnboardingPayLoad:
127
131
 
128
132
 
129
133
  def get_registration_struct_to_sign(
130
- account_index: int, address: str, timestamp: datetime, action: str
134
+ account_index: int, address: str, timestamp: datetime, action: str, host: str
131
135
  ) -> AccountRegistration:
132
136
  return AccountRegistration(
133
137
  account_index=account_index,
@@ -135,6 +139,7 @@ def get_registration_struct_to_sign(
135
139
  tos_accepted=True,
136
140
  time=timestamp,
137
141
  action=action,
142
+ host=host,
138
143
  )
139
144
 
140
145
 
@@ -180,6 +185,7 @@ def get_onboarding_payload(
180
185
  account: LocalAccount,
181
186
  signing_domain: str,
182
187
  key_pair: StarkKeyPair,
188
+ host: str,
183
189
  time: datetime | None = None,
184
190
  referral_code: str | None = None,
185
191
  ) -> OnboardingPayLoad:
@@ -187,11 +193,11 @@ def get_onboarding_payload(
187
193
  time = datetime.now(timezone.utc)
188
194
 
189
195
  registration_payload = get_registration_struct_to_sign(
190
- account_index=0, address=account.address, timestamp=time, action=register_action
196
+ account_index=0, address=account.address, timestamp=time, action=register_action, host=host
191
197
  )
192
- l1_signature = account.sign_message(
193
- registration_payload.to_signable_message(signing_domain=signing_domain)
194
- ).signature.hex()
198
+ payload = registration_payload.to_signable_message(signing_domain=signing_domain)
199
+ l1_signature = account.sign_message(payload).signature.hex()
200
+
195
201
  l2_message = pedersen_hash(int(account.address, 16), key_pair.public)
196
202
  l2_r, l2_s = stark_sign(msg_hash=l2_message, private_key=key_pair.private)
197
203
 
@@ -207,16 +213,18 @@ def get_onboarding_payload(
207
213
 
208
214
 
209
215
  def get_sub_account_creation_payload(
210
- account_index: int, l1_address: str, key_pair: StarkKeyPair, description: str, time: datetime | None = None
216
+ account_index: int,
217
+ l1_address: str,
218
+ key_pair: StarkKeyPair,
219
+ description: str,
220
+ host: str,
221
+ time: datetime | None = None,
211
222
  ):
212
223
  if time is None:
213
224
  time = datetime.now(timezone.utc)
214
225
 
215
226
  registration_payload = get_registration_struct_to_sign(
216
- account_index=account_index,
217
- address=l1_address,
218
- timestamp=time,
219
- action=sub_account_action,
227
+ account_index=account_index, address=l1_address, timestamp=time, action=sub_account_action, host=host
220
228
  )
221
229
 
222
230
  l2_message = pedersen_hash(int(l1_address, 16), key_pair.public)
@@ -78,6 +78,7 @@ class UserClient:
78
78
  signing_domain=self.__endpoint_config.signing_domain,
79
79
  key_pair=key_pair,
80
80
  referral_code=referral_code,
81
+ host=self.__endpoint_config.onboarding_url,
81
82
  )
82
83
  url = self._get_url(self.__endpoint_config.onboarding_url, path="/auth/onboard")
83
84
  onboarding_response = await send_post_request(
@@ -111,6 +112,7 @@ class UserClient:
111
112
  l1_address=signing_account.address,
112
113
  key_pair=key_pair,
113
114
  description=description,
115
+ host=self.__endpoint_config.onboarding_url,
114
116
  )
115
117
  headers = {
116
118
  L1_AUTH_SIGNATURE_HEADER: l1_signature.signature.hex(),