hyperquant 1.48__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.

Potentially problematic release.


This version of hyperquant might be problematic. Click here for more details.

Files changed (42) hide show
  1. hyperquant/__init__.py +8 -0
  2. hyperquant/broker/auth.py +972 -0
  3. hyperquant/broker/bitget.py +311 -0
  4. hyperquant/broker/bitmart.py +720 -0
  5. hyperquant/broker/coinw.py +487 -0
  6. hyperquant/broker/deepcoin.py +651 -0
  7. hyperquant/broker/edgex.py +500 -0
  8. hyperquant/broker/hyperliquid.py +570 -0
  9. hyperquant/broker/lbank.py +661 -0
  10. hyperquant/broker/lib/edgex_sign.py +455 -0
  11. hyperquant/broker/lib/hpstore.py +252 -0
  12. hyperquant/broker/lib/hyper_types.py +48 -0
  13. hyperquant/broker/lib/polymarket/ctfAbi.py +721 -0
  14. hyperquant/broker/lib/polymarket/safeAbi.py +1138 -0
  15. hyperquant/broker/lib/util.py +22 -0
  16. hyperquant/broker/lighter.py +679 -0
  17. hyperquant/broker/models/apexpro.py +150 -0
  18. hyperquant/broker/models/bitget.py +359 -0
  19. hyperquant/broker/models/bitmart.py +635 -0
  20. hyperquant/broker/models/coinw.py +724 -0
  21. hyperquant/broker/models/deepcoin.py +809 -0
  22. hyperquant/broker/models/edgex.py +1053 -0
  23. hyperquant/broker/models/hyperliquid.py +284 -0
  24. hyperquant/broker/models/lbank.py +557 -0
  25. hyperquant/broker/models/lighter.py +868 -0
  26. hyperquant/broker/models/ourbit.py +1155 -0
  27. hyperquant/broker/models/polymarket.py +1071 -0
  28. hyperquant/broker/ourbit.py +550 -0
  29. hyperquant/broker/polymarket.py +2399 -0
  30. hyperquant/broker/ws.py +132 -0
  31. hyperquant/core.py +513 -0
  32. hyperquant/datavison/_util.py +18 -0
  33. hyperquant/datavison/binance.py +111 -0
  34. hyperquant/datavison/coinglass.py +237 -0
  35. hyperquant/datavison/okx.py +177 -0
  36. hyperquant/db.py +191 -0
  37. hyperquant/draw.py +1200 -0
  38. hyperquant/logkit.py +205 -0
  39. hyperquant/notikit.py +124 -0
  40. hyperquant-1.48.dist-info/METADATA +32 -0
  41. hyperquant-1.48.dist-info/RECORD +42 -0
  42. hyperquant-1.48.dist-info/WHEEL +4 -0
@@ -0,0 +1,22 @@
1
+ from decimal import ROUND_HALF_UP, Decimal
2
+
3
+
4
+ def fmt_value(price: float, tick: float) -> str:
5
+ tick_dec = Decimal(str(tick))
6
+ price_dec = Decimal(str(price))
7
+ return str(
8
+ (price_dec / tick_dec).quantize(Decimal("1"), rounding=ROUND_HALF_UP) * tick_dec
9
+ )
10
+
11
+
12
+ def place_to_step(place: int) -> float:
13
+ """
14
+ 把 pricePlace / volumePlace 转换成 tick_size / lot_size
15
+
16
+ Args:
17
+ place (int): 小数位数,例如 pricePlace=1, volumePlace=2
18
+
19
+ Returns:
20
+ float: 步长 (step),例如 0.1, 0.01
21
+ """
22
+ return 10 ** (-place)
@@ -0,0 +1,679 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import json
6
+ from decimal import Decimal, ROUND_DOWN, ROUND_HALF_UP
7
+ from typing import Any, Literal, Sequence
8
+
9
+ import pybotters
10
+
11
+ from lighter.api.account_api import AccountApi
12
+ from lighter.api.order_api import OrderApi
13
+ from lighter.api.candlestick_api import CandlestickApi
14
+ from lighter.api_client import ApiClient
15
+ from lighter.configuration import Configuration
16
+ from lighter.signer_client import SignerClient
17
+
18
+ from .models.lighter import LighterDataStore, _maybe_to_dict
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class Lighter:
24
+ """Lighter exchange client (REST + WebSocket) built on top of the official SDK."""
25
+
26
+ def __init__(
27
+ self,
28
+ client: pybotters.Client,
29
+ *,
30
+ configuration: Configuration | None = None,
31
+ l1_address: str | None = None,
32
+ secret: str | None = None,
33
+ api_key_index: int = 3,
34
+ api_client: ApiClient | None = None,
35
+ order_api: OrderApi | None = None,
36
+ candlestick_api: CandlestickApi | None = None,
37
+ account_api: AccountApi | None = None,
38
+ ws_url: str | None = None,
39
+ ) -> None:
40
+ self.client = client
41
+ self.store = LighterDataStore()
42
+ self.l1_address = l1_address
43
+ self.account_index: int | None = None
44
+ self.secret:str = secret
45
+ self.api_key_index = api_key_index
46
+
47
+ self.configuration = configuration or Configuration.get_default()
48
+ self._api_client = api_client or ApiClient(configuration=self.configuration)
49
+ self._owns_api_client = api_client is None
50
+
51
+ self.order_api = order_api or OrderApi(self._api_client)
52
+ self.candlestick_api = candlestick_api or CandlestickApi(self._api_client)
53
+ self.account_api = account_api or AccountApi(self._api_client)
54
+ self.signer: SignerClient = None
55
+
56
+ base_host = self.configuration.host.rstrip("/")
57
+ default_ws_url = f"{base_host.replace('https://', 'wss://')}/stream"
58
+ self.ws_url = ws_url or default_ws_url
59
+ self.id_to_symbol: dict[str, str] = {}
60
+
61
+ async def __aenter__(self) -> "Lighter":
62
+ await self.update("detail")
63
+
64
+ # 设置id_to_symbol映射
65
+ for detail in self.store.detail.find():
66
+ market_id = detail.get("market_id")
67
+ symbol = detail.get("symbol")
68
+ if market_id is not None and symbol is not None:
69
+ self.id_to_symbol[str(market_id)] = symbol
70
+
71
+ self.store.set_id_to_symbol(self.id_to_symbol)
72
+
73
+ # 尝试自动设置account_index
74
+ if self.l1_address is not None:
75
+ subact = await self.account_api.accounts_by_l1_address(
76
+ l1_address=self.l1_address
77
+ )
78
+ self.account_index = subact.sub_accounts[0].index
79
+
80
+ if self.secret:
81
+
82
+ self.signer = SignerClient(
83
+ url=self.configuration.host,
84
+ private_key=self.secret,
85
+ account_index=self.account_index if self.account_index is not None else -1,
86
+ api_key_index=self.api_key_index,
87
+ )
88
+
89
+ return self
90
+
91
+ async def __aexit__(self, exc_type, exc, tb) -> None:
92
+ await self.aclose()
93
+
94
+ async def aclose(self) -> None:
95
+ if self._owns_api_client:
96
+ await self._api_client.close()
97
+
98
+ @property
99
+ def auth(self):
100
+ if not self.signer:
101
+ raise RuntimeError("SignerClient is required for auth token generation")
102
+ auth, err = self.signer.create_auth_token_with_expiry(SignerClient.DEFAULT_10_MIN_AUTH_EXPIRY)
103
+ if err is not None:
104
+ raise Exception(err)
105
+ return auth
106
+
107
+ def get_contract_id(self, symbol: str) -> str | None:
108
+ """Helper that resolves a symbol to its `market_id`."""
109
+ detail = self.store.detail.get({"symbol": symbol}) or self.store.detail.get({"market_id": symbol})
110
+ if not detail:
111
+ return None
112
+ market_id = detail.get("market_id")
113
+ if market_id is None and detail.get("order_book_index") is not None:
114
+ market_id = detail["order_book_index"]
115
+ return str(market_id) if market_id is not None else None
116
+
117
+ def _get_detail_entry(self, symbol: str | None = None, market_index: int | None = None) -> dict[str, Any] | None:
118
+ if symbol:
119
+ entry = self.store.detail.get({"symbol": symbol})
120
+ if entry:
121
+ return entry
122
+
123
+ if market_index is not None:
124
+ entries = self.store.detail.find({"market_id": market_index})
125
+ if entries:
126
+ return entries[0]
127
+
128
+ return None
129
+
130
+ async def update(
131
+ self,
132
+ update_type: Literal[
133
+ "detail",
134
+ "orders",
135
+ "history_order",
136
+ "history_orders",
137
+ "account",
138
+ "positions",
139
+ "all",
140
+ ] = "all",
141
+ *,
142
+ symbol: str | None = None,
143
+ limit: int = 50,
144
+ ) -> None:
145
+ """Refresh cached data via Lighter REST endpoints."""
146
+
147
+ tasks: list[tuple[str, Any]] = []
148
+
149
+ include_detail = update_type in {"detail", "all"}
150
+ include_orders = update_type in {"orders", "all"}
151
+ include_history = update_type in {"history_order", "history_orders", "all"}
152
+ include_account = update_type in {"account", "positions", "all"}
153
+ account_index = self.account_index
154
+
155
+
156
+ if include_detail:
157
+ # Use raw HTTP to avoid strict SDK model validation issues (e.g., status 'inactive').
158
+ url = f"{self.configuration.host.rstrip('/')}/api/v1/orderBooks"
159
+ tasks.append(("detail", self.client.get(url)))
160
+
161
+ if include_orders:
162
+ if account_index is None or symbol is None:
163
+ if update_type == "orders":
164
+ raise ValueError("account_index and symbol are required to update orders")
165
+ else:
166
+ cid = self.get_contract_id(symbol)
167
+ tasks.append(
168
+ (
169
+ "orders",
170
+ self.order_api.account_active_orders(
171
+ account_index=account_index,
172
+ market_id=int(cid),
173
+ auth=self.auth
174
+ ),
175
+ )
176
+ )
177
+
178
+ if include_history:
179
+ if account_index is None:
180
+ raise ValueError("account_index is required to update history orders")
181
+ else:
182
+ tasks.append(
183
+ (
184
+ "history_orders",
185
+ self.order_api.account_inactive_orders(
186
+ account_index=account_index,
187
+ limit=limit,
188
+ auth=self.auth
189
+ ),
190
+ )
191
+ )
192
+
193
+ if include_account:
194
+ if account_index is None:
195
+ if update_type in {"account", "positions"}:
196
+ raise ValueError("account_index is required to update account data")
197
+ else:
198
+ tasks.append(
199
+ (
200
+ "account",
201
+ self.account_api.account(
202
+ by="index",
203
+ value=str(account_index),
204
+ ),
205
+ )
206
+ )
207
+
208
+ if not tasks:
209
+ logger.debug("No REST requests enqueued for Lighter update_type=%s", update_type)
210
+ return
211
+
212
+ results: dict[str, Any] = {}
213
+ for key, coroutine in tasks:
214
+ try:
215
+ resp = await coroutine
216
+ if key == "detail":
217
+ # Parse JSON body for detail endpoint
218
+ results[key] = await resp.json()
219
+ else:
220
+ results[key] = resp
221
+ except Exception:
222
+ logger.exception("Lighter REST request %s failed", key)
223
+ raise
224
+
225
+ if "detail" in results:
226
+ self.store.detail._onresponse(results["detail"])
227
+
228
+ if "orders" in results:
229
+ self.store.orders._onresponse(results["orders"])
230
+
231
+ if "history_orders" in results:
232
+ self.store.orders._onresponse(results["history_orders"])
233
+
234
+ if "account" in results:
235
+ account_payload = results["account"]
236
+ self.store.accounts._onresponse(account_payload)
237
+ self.store.positions._onresponse(account_payload)
238
+
239
+ async def sub_orderbook(
240
+ self,
241
+ symbols: Sequence[str] | str,
242
+ *,
243
+ account_ids: Sequence[int] | int | None = None,
244
+ depth_limit: int | None = None,
245
+ ) -> pybotters.ws.WebSocketApp:
246
+ """Subscribe to order book (and optional account) websocket streams by symbol."""
247
+
248
+ if isinstance(symbols, str):
249
+ symbol_list = [symbols]
250
+ else:
251
+ symbol_list = list(symbols)
252
+
253
+ if not symbol_list and not account_ids:
254
+ raise ValueError("At least one symbol or account_id must be provided")
255
+
256
+ needs_detail = any(self.get_contract_id(sym) is None for sym in symbol_list)
257
+ if needs_detail and symbol_list:
258
+ try:
259
+ await self.update("detail")
260
+ except Exception:
261
+ logger.exception("Failed to refresh Lighter market metadata for symbol resolution")
262
+ raise
263
+
264
+ order_book_ids: list[str] = []
265
+ for sym in symbol_list:
266
+ market_id = self.get_contract_id(sym)
267
+ if market_id is None:
268
+ if sym.isdigit():
269
+ market_id = sym
270
+ else:
271
+ raise ValueError(f"Unknown symbol: {sym}")
272
+ market_id_str = str(market_id)
273
+ order_book_ids.append(market_id_str)
274
+ self.store.book.id_to_symbol[market_id_str] = sym
275
+
276
+ account_id_list: list[str] = []
277
+ if account_ids is not None:
278
+ if isinstance(account_ids, int):
279
+ account_id_list = [str(account_ids)]
280
+ else:
281
+ account_id_list = [str(aid) for aid in account_ids]
282
+
283
+ if not order_book_ids and not account_id_list:
284
+ raise ValueError("No valid symbols or account_ids resolved for subscription")
285
+
286
+ if depth_limit is not None:
287
+ self.store.book.limit = depth_limit
288
+
289
+ order_book_channels = [f"order_book/{mid}" for mid in order_book_ids]
290
+ account_channels = [f"account_all/{aid}" for aid in account_id_list]
291
+
292
+ send_payload = [
293
+ {"type": "subscribe", "channel": channel} for channel in order_book_channels + account_channels
294
+ ]
295
+
296
+ ws_app = self.client.ws_connect(
297
+ self.ws_url,
298
+ send_json=send_payload,
299
+ hdlr_json=self.store.onmessage,
300
+ )
301
+
302
+ await ws_app._event.wait()
303
+ return ws_app
304
+
305
+
306
+ async def sub_orders(
307
+ self,
308
+ account_ids: Sequence[int] | int = None,
309
+ ) -> pybotters.ws.WebSocketApp:
310
+ """Subscribe to order updates via Account All Orders stream.
311
+
312
+ Channel per docs: "account_all_orders/{ACCOUNT_ID}" (requires auth).
313
+ Response carries an "orders" mapping of market_id -> [Order].
314
+ """
315
+ if account_ids:
316
+ if isinstance(account_ids, int):
317
+ account_id_list = [str(account_ids)]
318
+ else:
319
+ account_id_list = [str(aid) for aid in account_ids]
320
+ else:
321
+ account_id_list = [self.account_index]
322
+
323
+ channels = [f"account_all_orders/{aid}" for aid in account_id_list]
324
+ send_payload = [
325
+ {"type": "subscribe", "channel": channel, "auth": self.auth}
326
+ for channel in channels
327
+ ]
328
+
329
+ ws_app = self.client.ws_connect(
330
+ self.ws_url,
331
+ send_json=send_payload,
332
+ hdlr_json=self.store.onmessage,
333
+ )
334
+ await ws_app._event.wait()
335
+ return ws_app
336
+
337
+
338
+
339
+ async def sub_kline(
340
+ self,
341
+ symbols: Sequence[str] | str,
342
+ *,
343
+ resolutions: Sequence[str] | str,
344
+ ) -> pybotters.ws.WebSocketApp:
345
+ """Subscribe to trade streams and aggregate into klines in the store.
346
+
347
+ - symbols: list of symbols (e.g., ["BTC-USD"]) or a single symbol; may also be numeric market_ids.
348
+ - resolutions: list like ["1m", "5m"] or a single resolution; added to kline store for aggregation.
349
+ """
350
+
351
+ # Normalize inputs
352
+ symbol_list = [symbols] if isinstance(symbols, str) else list(symbols)
353
+ res_list = [resolutions] if isinstance(resolutions, str) else list(resolutions)
354
+
355
+ if not symbol_list:
356
+ raise ValueError("At least one symbol must be provided")
357
+ if not res_list:
358
+ raise ValueError("At least one resolution must be provided")
359
+
360
+ # Ensure market metadata for symbol->market_id resolution
361
+ needs_detail = any(self.get_contract_id(sym) is None and not str(sym).isdigit() for sym in symbol_list)
362
+ if needs_detail:
363
+ try:
364
+ await self.update("detail")
365
+ except Exception:
366
+ logger.exception("Failed to refresh Lighter market metadata for kline subscription")
367
+ raise
368
+
369
+ # Resolve market ids and populate id->symbol mapping for klines store
370
+ trade_market_ids: list[str] = []
371
+ for sym in symbol_list:
372
+ market_id = self.get_contract_id(sym)
373
+ if market_id is None:
374
+ if str(sym).isdigit():
375
+ market_id = str(sym)
376
+ symbol_for_map = str(sym)
377
+ else:
378
+ raise ValueError(f"Unknown symbol: {sym}")
379
+ else:
380
+ symbol_for_map = sym
381
+ market_id_str = str(market_id)
382
+ trade_market_ids.append(market_id_str)
383
+ # ensure klines store can resolve symbol from market id
384
+ self.store.klines.id_to_symbol[market_id_str] = symbol_for_map
385
+
386
+ # Register resolutions into kline store aggregation list
387
+ for r in res_list:
388
+ if r not in self.store.klines._res_list:
389
+ self.store.klines._res_list.append(r)
390
+
391
+ # Build subscribe payload for trade channels
392
+ channels = [f"trade/{mid}" for mid in trade_market_ids]
393
+ send_payload = [{"type": "subscribe", "channel": ch} for ch in channels]
394
+
395
+ ws_app = self.client.ws_connect(
396
+ self.ws_url,
397
+ send_json=send_payload,
398
+ hdlr_json=self.store.onmessage,
399
+ )
400
+
401
+ await ws_app._event.wait()
402
+ return ws_app
403
+
404
+ async def place_order(
405
+ self,
406
+ symbol: str,
407
+ *,
408
+ base_amount: float,
409
+ price: float,
410
+ is_ask: bool,
411
+ order_type: Literal[
412
+ "limit",
413
+ "market",
414
+ "stop-loss",
415
+ "stop-loss-limit",
416
+ "take-profit",
417
+ "take-profit-limit",
418
+ "twap",
419
+ ] = "limit",
420
+ time_in_force: Literal["ioc", "gtc", "post_only"] = "gtc",
421
+ reduce_only: bool = False,
422
+ trigger_price: float | None = None,
423
+ order_expiry: int | None = None,
424
+ nonce: int | None = None,
425
+ api_key_index: int | None = None,
426
+ client_order_index: int = 0,
427
+ ) -> dict[str, Any]:
428
+ """Submit an order through the signer client using human-readable inputs."""
429
+
430
+ if self.signer is None:
431
+ raise RuntimeError("SignerClient is required for placing orders")
432
+
433
+ market_index = self.get_contract_id(symbol)
434
+ if market_index is None:
435
+ raise ValueError(f"Unknown symbol: {symbol}")
436
+ market_index = int(market_index)
437
+
438
+ detail = self._get_detail_entry(symbol=symbol, market_index=market_index)
439
+ if detail is None:
440
+ await self.update("detail")
441
+ detail = self._get_detail_entry(symbol=symbol, market_index=market_index)
442
+ if detail is None:
443
+ raise ValueError(f"Market metadata unavailable for symbol: {symbol}")
444
+
445
+ order_type_map = {
446
+ "limit": self.signer.ORDER_TYPE_LIMIT,
447
+ "market": self.signer.ORDER_TYPE_MARKET,
448
+ "stop-loss": self.signer.ORDER_TYPE_STOP_LOSS,
449
+ "stop-loss-limit": self.signer.ORDER_TYPE_STOP_LOSS_LIMIT,
450
+ "take-profit": self.signer.ORDER_TYPE_TAKE_PROFIT,
451
+ "take-profit-limit": self.signer.ORDER_TYPE_TAKE_PROFIT_LIMIT,
452
+ "twap": self.signer.ORDER_TYPE_TWAP,
453
+ }
454
+ tif_map = {
455
+ "ioc": self.signer.ORDER_TIME_IN_FORCE_IMMEDIATE_OR_CANCEL,
456
+ "gtc": self.signer.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
457
+ "post_only": self.signer.ORDER_TIME_IN_FORCE_POST_ONLY,
458
+ }
459
+
460
+ try:
461
+ order_type_code = order_type_map[order_type]
462
+ except KeyError as exc:
463
+ raise ValueError(f"Unsupported order_type: {order_type}") from exc
464
+
465
+ try:
466
+ tif_code = tif_map[time_in_force]
467
+ except KeyError as exc:
468
+ raise ValueError(f"Unsupported time_in_force: {time_in_force}") from exc
469
+
470
+ # Per WS/API docs, OrderExpiry can be 0 with ExpiredAt computed by signer.
471
+ # Use caller-provided value if given; otherwise default to 0 to avoid
472
+ # "OrderExpiry is invalid" errors on some markets.
473
+ expiry = order_expiry if order_expiry is not None else 0
474
+ nonce_value = nonce if nonce is not None else -1
475
+ api_key_idx = api_key_index if api_key_index is not None else self.api_key_index
476
+
477
+ # ----- Precision and min constraints handling -----
478
+ # Prefer explicitly supported decimals. Avoid using quote decimals to infer size.
479
+ price_decimals = (
480
+ detail.get("supported_price_decimals")
481
+ or detail.get("price_decimals")
482
+ or 0
483
+ )
484
+ size_decimals = (
485
+ detail.get("supported_size_decimals")
486
+ or detail.get("size_decimals")
487
+ or 0
488
+ )
489
+
490
+ # Optional constraints provided by the API
491
+ # Strings like "10.000000" may be returned – normalize via Decimal for accuracy
492
+ def _to_decimal(v, default: str | int = 0):
493
+ try:
494
+ if v is None or v == "":
495
+ return Decimal(str(default))
496
+ return Decimal(str(v))
497
+ except Exception:
498
+ return Decimal(str(default))
499
+
500
+ min_base_amount = _to_decimal(detail.get("min_base_amount"), 0)
501
+ min_quote_amount = _to_decimal(detail.get("min_quote_amount"), 0)
502
+ order_quote_limit = _to_decimal(detail.get("order_quote_limit"), 0)
503
+
504
+ # Use Decimal for precise arithmetic and quantization
505
+ d_price = Decimal(str(price))
506
+ d_size = Decimal(str(base_amount))
507
+ quant_price = Decimal(1) / (Decimal(10) ** int(price_decimals)) if int(price_decimals) > 0 else Decimal(1)
508
+ quant_size = Decimal(1) / (Decimal(10) ** int(size_decimals)) if int(size_decimals) > 0 else Decimal(1)
509
+
510
+ # Round price/size to allowed decimals (half up to the nearest tick)
511
+ d_price = d_price.quantize(quant_price, rounding=ROUND_HALF_UP)
512
+ d_size = d_size.quantize(quant_size, rounding=ROUND_HALF_UP)
513
+
514
+ # Ensure minimum notional and minimum base constraints
515
+ # If violating, adjust size upward to the smallest valid amount respecting size tick
516
+ if min_quote_amount > 0:
517
+ notional = d_price * d_size
518
+ if notional < min_quote_amount:
519
+ # required size to reach min notional
520
+ required = (min_quote_amount / d_price).quantize(quant_size, rounding=ROUND_HALF_UP)
521
+ if required > d_size:
522
+ d_size = required
523
+ if min_base_amount > 0 and d_size < min_base_amount:
524
+ d_size = min_base_amount.quantize(quant_size, rounding=ROUND_HALF_UP)
525
+
526
+ # Respect optional maximum notional limit if provided (>0)
527
+ if order_quote_limit and order_quote_limit > 0:
528
+ notional = d_price * d_size
529
+ if notional > order_quote_limit:
530
+ # Reduce size down to the maximum allowed notional (floor to tick)
531
+ max_size = (order_quote_limit / d_price).quantize(quant_size, rounding=ROUND_DOWN)
532
+ if max_size <= 0:
533
+ raise ValueError("order would exceed order_quote_limit and cannot be reduced to a positive size")
534
+ d_size = max_size
535
+
536
+ # Convert to integer representation expected by signer
537
+ price_scale = 10 ** int(price_decimals)
538
+ size_scale = 10 ** int(size_decimals)
539
+
540
+ price_int = int((d_price * price_scale).to_integral_value(rounding=ROUND_HALF_UP))
541
+ base_amount_int = int((d_size * size_scale).to_integral_value(rounding=ROUND_HALF_UP))
542
+
543
+ trigger_price_int = (
544
+ int((Decimal(str(trigger_price)) * price_scale).to_integral_value(rounding=ROUND_HALF_UP))
545
+ if trigger_price is not None
546
+ else self.signer.NIL_TRIGGER_PRICE
547
+ )
548
+
549
+ created_tx, response, error = await self.signer.create_order(
550
+ market_index=market_index,
551
+ client_order_index=client_order_index,
552
+ base_amount=base_amount_int,
553
+ price=price_int,
554
+ is_ask=is_ask,
555
+ order_type=order_type_code,
556
+ time_in_force=tif_code,
557
+ reduce_only=reduce_only,
558
+ trigger_price=trigger_price_int,
559
+ order_expiry=expiry,
560
+ nonce=nonce_value,
561
+ api_key_index=api_key_idx,
562
+ )
563
+
564
+ if error:
565
+ raise RuntimeError(f"Lighter create_order failed: {error}")
566
+ if response is None:
567
+ raise RuntimeError("Lighter create_order returned no response")
568
+
569
+ if hasattr(created_tx, "to_json"):
570
+ request_payload = json.loads(created_tx.to_json())
571
+ else:
572
+ request_payload = str(created_tx)
573
+ response_payload = response.to_dict() if hasattr(response, "to_dict") else _maybe_to_dict(response)
574
+
575
+ # return {
576
+ # "request": request_payload,
577
+ # "response": response_payload,
578
+ # }
579
+ return response_payload
580
+
581
+ async def cancel_order(
582
+ self,
583
+ symbol: str,
584
+ order_index: int,
585
+ *,
586
+ nonce: int | None = None,
587
+ api_key_index: int | None = None,
588
+ ) -> dict[str, Any]:
589
+ """Cancel a single order using the signer client."""
590
+
591
+ market_index = self.get_contract_id(symbol)
592
+ if market_index is None:
593
+ raise ValueError(f"Unknown symbol: {symbol}")
594
+ market_index = int(market_index)
595
+
596
+ if self.signer is None:
597
+ raise RuntimeError("SignerClient is required for cancelling orders")
598
+
599
+ nonce_value = nonce if nonce is not None else -1
600
+ api_key_idx = api_key_index or self.api_key_index
601
+
602
+ cancel_tx, response, error = await self.signer.cancel_order(
603
+ market_index=market_index,
604
+ order_index=order_index,
605
+ nonce=nonce_value,
606
+ api_key_index=api_key_idx,
607
+ )
608
+
609
+ if error:
610
+ raise RuntimeError(f"Lighter cancel_order failed: {error}")
611
+ if response is None:
612
+ raise RuntimeError("Lighter cancel_order returned no response")
613
+
614
+ if hasattr(cancel_tx, "to_json"):
615
+ request_payload = json.loads(cancel_tx.to_json())
616
+ else:
617
+ request_payload = str(cancel_tx)
618
+ response_payload = response.to_dict() if hasattr(response, "to_dict") else _maybe_to_dict(response)
619
+ return {
620
+ "request": request_payload,
621
+ "response": response_payload,
622
+ }
623
+
624
+ async def update_kline(
625
+ self,
626
+ symbol: str,
627
+ *,
628
+ resolution: str,
629
+ start_timestamp: int,
630
+ end_timestamp: int,
631
+ count_back: int,
632
+ set_timestamp_to_end: bool | None = None,
633
+ ) -> list[dict[str, Any]]:
634
+ """Fetch candlesticks and update the Kline store.
635
+
636
+ Parameters
637
+ - symbol: market symbol, e.g. "BTC-USD".
638
+ - resolution: e.g. "1m", "5m", "1h".
639
+ - start_timestamp: epoch milliseconds.
640
+ - end_timestamp: epoch milliseconds.
641
+ - count_back: number of bars to fetch.
642
+ - set_timestamp_to_end: if True, API sets last bar timestamp to the end.
643
+ """
644
+
645
+ market_id = self.get_contract_id(symbol)
646
+ if market_id is None:
647
+ # try to refresh metadata once
648
+ await self.update("detail")
649
+ market_id = self.get_contract_id(symbol)
650
+ if market_id is None:
651
+ raise ValueError(f"Unknown symbol: {symbol}")
652
+
653
+ resp = await self.candlestick_api.candlesticks(
654
+ market_id=int(market_id),
655
+ resolution=resolution,
656
+ start_timestamp=int(start_timestamp),
657
+ end_timestamp=int(end_timestamp),
658
+ count_back=int(count_back),
659
+ set_timestamp_to_end=bool(set_timestamp_to_end) if set_timestamp_to_end is not None else None,
660
+ )
661
+
662
+ # Update store
663
+ self.store.klines._onresponse(resp, symbol=symbol, resolution=resolution)
664
+
665
+ payload = _maybe_to_dict(resp) or {}
666
+ items = payload.get("candlesticks") or []
667
+ # attach symbol/resolution to return
668
+ out: list[dict[str, Any]] = []
669
+ for it in items:
670
+ if hasattr(it, "to_dict"):
671
+ d = it.to_dict()
672
+ elif hasattr(it, "model_dump"):
673
+ d = it.model_dump()
674
+ else:
675
+ d = dict(it) if isinstance(it, dict) else {"value": it}
676
+ d["symbol"] = symbol
677
+ d["resolution"] = resolution
678
+ out.append(d)
679
+ return out