hyperquant 0.5__py3-none-any.whl → 0.7__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.
@@ -0,0 +1,1053 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any, Awaitable, TYPE_CHECKING
5
+
6
+ from aiohttp import ClientResponse
7
+ import aiohttp
8
+ from pybotters.store import DataStore, DataStoreCollection
9
+
10
+ if TYPE_CHECKING:
11
+ from pybotters.typedefs import Item
12
+ from pybotters.ws import ClientWebSocketResponse
13
+
14
+
15
+ class Book(DataStore):
16
+ """Order book data store for the Edgex websocket feed."""
17
+
18
+ _KEYS = ["c", "S", "p"]
19
+
20
+ def _init(self) -> None:
21
+ self._version: int | str | None = None
22
+ self.limit: int | None = None
23
+
24
+ def _on_message(self, msg: dict[str, Any]) -> None:
25
+ content = msg.get("content") or {}
26
+ entries = content.get("data") or []
27
+ data_type = (content.get("dataType") or "").lower()
28
+
29
+ for entry in entries:
30
+ contract_id = entry.get("contractId")
31
+ if contract_id is None:
32
+ continue
33
+
34
+ contract_name = entry.get("contractName")
35
+ end_version = entry.get("endVersion")
36
+ depth_type = (entry.get("depthType") or "").lower()
37
+
38
+ is_snapshot = data_type == "snapshot" or depth_type == "snapshot"
39
+
40
+ if is_snapshot:
41
+ self._handle_snapshot(
42
+ contract_id,
43
+ contract_name,
44
+ entry,
45
+ )
46
+ else:
47
+ self._handle_delta(
48
+ contract_id,
49
+ contract_name,
50
+ entry,
51
+ )
52
+
53
+ if end_version is not None:
54
+ self._version = self._normalize_version(end_version)
55
+
56
+ def _handle_snapshot(
57
+ self,
58
+ contract_id: str,
59
+ contract_name: str | None,
60
+ entry: dict[str, Any],
61
+ ) -> None:
62
+ asks = entry.get("asks") or []
63
+ bids = entry.get("bids") or []
64
+
65
+ self._find_and_delete({"c": contract_id})
66
+
67
+ payload: list[dict[str, Any]] = []
68
+ payload.extend(
69
+ self._build_items(
70
+ contract_id,
71
+ contract_name,
72
+ "a",
73
+ asks,
74
+ )
75
+ )
76
+ payload.extend(
77
+ self._build_items(
78
+ contract_id,
79
+ contract_name,
80
+ "b",
81
+ bids,
82
+ )
83
+ )
84
+
85
+ if payload:
86
+ self._insert(payload)
87
+ self._trim(contract_id, contract_name)
88
+
89
+ def _handle_delta(
90
+ self,
91
+ contract_id: str,
92
+ contract_name: str | None,
93
+ entry: dict[str, Any],
94
+ ) -> None:
95
+ updates: list[dict[str, Any]] = []
96
+ deletes: list[dict[str, Any]] = []
97
+
98
+ asks = entry.get("asks") or []
99
+ bids = entry.get("bids") or []
100
+
101
+ for side, levels in (("a", asks), ("b", bids)):
102
+ for row in levels:
103
+ price, size = self._extract_price_size(row)
104
+ criteria = {"c": contract_id, "S": side, "p": price}
105
+
106
+ if not size or float(size) == 0.0:
107
+ deletes.append(criteria)
108
+ continue
109
+
110
+ updates.append(
111
+ {
112
+ "c": contract_id,
113
+ "S": side,
114
+ "p": price,
115
+ "q": size,
116
+ "s": self._symbol(contract_id, contract_name),
117
+ }
118
+ )
119
+
120
+ if deletes:
121
+ self._delete(deletes)
122
+ if updates:
123
+ self._update(updates)
124
+ self._trim(contract_id, contract_name)
125
+
126
+ def _build_items(
127
+ self,
128
+ contract_id: str,
129
+ contract_name: str | None,
130
+ side: str,
131
+ rows: list[dict[str, Any]],
132
+ ) -> list[dict[str, Any]]:
133
+ items: list[dict[str, Any]] = []
134
+ for row in rows:
135
+ price, size = self._extract_price_size(row)
136
+ if not size or float(size) == 0.0:
137
+ continue
138
+ items.append(
139
+ {
140
+ "c": contract_id,
141
+ "S": side,
142
+ "p": price,
143
+ "q": size,
144
+ "s": self._symbol(contract_id, contract_name),
145
+ }
146
+ )
147
+ return items
148
+
149
+ @staticmethod
150
+ def _normalize_version(value: Any) -> int | str:
151
+ if value is None:
152
+ return value
153
+ try:
154
+ return int(value)
155
+ except (TypeError, ValueError):
156
+ return str(value)
157
+
158
+ @staticmethod
159
+ def _to_str(value: Any) -> str | None:
160
+ if value is None:
161
+ return None
162
+ return str(value)
163
+
164
+ @staticmethod
165
+ def _extract_price_size(row: dict[str, Any]) -> tuple[str, str]:
166
+ return str(row["price"]), str(row["size"])
167
+
168
+ def _trim(self, contract_id: str, contract_name: str | None) -> None:
169
+ if self.limit is None:
170
+ return
171
+
172
+ query: dict[str, Any]
173
+ symbol = self._symbol(contract_id, contract_name)
174
+ if symbol:
175
+ query = {"s": symbol}
176
+ else:
177
+ query = {"c": contract_id}
178
+
179
+ sort_data = self.sorted(query, self.limit)
180
+ asks = sort_data.get("a", [])
181
+ bids = sort_data.get("b", [])
182
+
183
+ self._find_and_delete(query)
184
+
185
+ trimmed = asks + bids
186
+ if trimmed:
187
+ self._insert(trimmed)
188
+
189
+ @staticmethod
190
+ def _symbol(contract_id: str, contract_name: str | None) -> str:
191
+ if contract_name:
192
+ return str(contract_name)
193
+ return str(contract_id)
194
+
195
+ @property
196
+ def version(self) -> int | str | None:
197
+ """返回当前缓存的订单簿版本号。"""
198
+ return self._version
199
+
200
+ def sorted(
201
+ self,
202
+ query: dict[str, Any] | None = None,
203
+ limit: int | None = None,
204
+ ) -> dict[str, list[dict[str, Any]]]:
205
+ """按买卖方向与价格排序后的订单簿视图。"""
206
+ return self._sorted(
207
+ item_key="S",
208
+ item_asc_key="a",
209
+ item_desc_key="b",
210
+ sort_key="p",
211
+ query=query,
212
+ limit=limit,
213
+ )
214
+
215
+
216
+ class Ticker(DataStore):
217
+ """24 小时行情推送数据。"""
218
+
219
+ _KEYS = ["c"]
220
+
221
+ def _on_message(self, msg: dict[str, Any]) -> None:
222
+ content = msg.get("content") or {}
223
+ entries = content.get("data") or []
224
+ data_type = (content.get("dataType") or "").lower()
225
+
226
+ for entry in entries:
227
+ item = self._format(entry)
228
+ if item is None:
229
+ continue
230
+
231
+ criteria = {"c": item["c"]}
232
+ if data_type == "snapshot":
233
+ self._find_and_delete(criteria)
234
+ self._insert([item])
235
+ else:
236
+ self._update([item])
237
+
238
+ def _onresponse(self, data: dict[str, Any]) -> None:
239
+ entries = data.get("data") or []
240
+
241
+ if not isinstance(entries, list):
242
+ entries = [entries]
243
+
244
+ items = []
245
+ for entry in entries:
246
+ item = self._format(entry)
247
+ if item:
248
+ items.append(item)
249
+
250
+ self._clear()
251
+ if items:
252
+ self._insert(items)
253
+
254
+ def _format(self, entry: dict[str, Any]) -> dict[str, Any] | None:
255
+ contract_id = entry.get("contractId")
256
+ if contract_id is None:
257
+ return None
258
+
259
+ item: dict[str, Any] = {"c": str(contract_id)}
260
+
261
+ name = entry.get("contractName")
262
+ if name is not None:
263
+ item["s"] = str(name)
264
+
265
+ fields = [
266
+ "priceChange",
267
+ "priceChangePercent",
268
+ "trades",
269
+ "size",
270
+ "value",
271
+ "high",
272
+ "low",
273
+ "open",
274
+ "close",
275
+ "highTime",
276
+ "lowTime",
277
+ "startTime",
278
+ "endTime",
279
+ "lastPrice",
280
+ "indexPrice",
281
+ "oraclePrice",
282
+ "openInterest",
283
+ "fundingRate",
284
+ "fundingTime",
285
+ "nextFundingTime",
286
+ "bestAskPrice",
287
+ "bestBidPrice",
288
+ ]
289
+
290
+ for key in fields:
291
+ value = entry.get(key)
292
+ if value is not None:
293
+ item[key] = str(value)
294
+
295
+ return item
296
+
297
+
298
+ class Order(DataStore):
299
+ """Order data store combining REST results with trade-event deltas.
300
+
301
+ We only keep fields that are practical for trading book-keeping: identifiers,
302
+ basic order parameters, cumulative fills, high-level status and timestamps.
303
+ Network payloads carry hundreds of fields (``l2`` signatures, TPSL templates,
304
+ liquidation metadata, etc.), but the extra data adds noise and bloats memory
305
+ consumption. This store narrows every entry to a compact schema while still
306
+ supporting diff events from the private websocket feed.
307
+ """
308
+
309
+ _KEYS = ["orderId"]
310
+
311
+ _TERMINAL_STATUSES = {
312
+ "FILLED",
313
+ "CANCELED",
314
+ "CANCELLED",
315
+ "REJECTED",
316
+ "EXPIRED",
317
+ }
318
+
319
+ _ACTIVE_STATUSES = {
320
+ "OPEN",
321
+ "PARTIALLY_FILLED",
322
+ "PENDING",
323
+ "CREATED",
324
+ "ACKNOWLEDGED",
325
+ }
326
+
327
+ _KEEP_FIELDS = (
328
+ "userId",
329
+ "accountId",
330
+ "coinId",
331
+ "contractId",
332
+ "clientOrderId",
333
+ "type",
334
+ "timeInForce",
335
+ "reduceOnly",
336
+ "price",
337
+ "size",
338
+ "cumFillSize",
339
+ "cumFillValue",
340
+ "cumMatchSize",
341
+ "cumMatchValue",
342
+ "cumMatchFee",
343
+ "triggerPrice",
344
+ "triggerPriceType",
345
+ "cancelReason",
346
+ "createdTime",
347
+ "updatedTime",
348
+ "matchSequenceId",
349
+ )
350
+
351
+ _BOOL_FIELDS = {"reduceOnly"}
352
+
353
+ def _on_message(self, msg: dict[str, Any]) -> None:
354
+ content = msg.get("content") or {}
355
+ data = content.get("data") or {}
356
+ orders = data.get("order") or []
357
+
358
+ if not isinstance(orders, list):
359
+ orders = [orders]
360
+
361
+ items = [self._format(order) for order in orders]
362
+ items = [item for item in items if item]
363
+ if not items:
364
+ return
365
+
366
+ event = (content.get("event") or "").lower()
367
+ if event == "snapshot":
368
+ self._clear()
369
+ self._insert(items)
370
+ return
371
+
372
+ for item in items:
373
+ status = str(item.get("status") or "").upper()
374
+ criteria = {"orderId": item["orderId"]}
375
+ existing = self.find(criteria)
376
+
377
+ if status in self._TERMINAL_STATUSES:
378
+ if existing:
379
+ self._update([item])
380
+ else:
381
+ self._insert([item])
382
+ self._find_and_delete(criteria)
383
+ continue
384
+
385
+ if status and status not in self._ACTIVE_STATUSES:
386
+ if existing:
387
+ self._update([item])
388
+ else:
389
+ self._insert([item])
390
+ self._find_and_delete(criteria)
391
+ continue
392
+
393
+ if existing:
394
+ self._update([item])
395
+ else:
396
+ self._insert([item])
397
+
398
+ def _onresponse(self, data: dict[str, Any]) -> None:
399
+ payload = data.get("data")
400
+
401
+ if isinstance(payload, dict):
402
+ orders = payload.get("dataList") or payload.get("orderList") or []
403
+ else:
404
+ orders = payload or []
405
+
406
+ if not isinstance(orders, list):
407
+ orders = [orders]
408
+
409
+ items = [self._format(order) for order in orders]
410
+ items = [item for item in items if item]
411
+
412
+ self._clear()
413
+ if items:
414
+ self._insert(items)
415
+
416
+ @staticmethod
417
+ def _normalize_order_id(value: Any) -> str | None:
418
+ if value is None:
419
+ return None
420
+ return str(value)
421
+
422
+ @staticmethod
423
+ def _normalize_side(value: Any) -> str | None:
424
+ if value is None:
425
+ return None
426
+ if isinstance(value, str):
427
+ return value.lower()
428
+ return str(value)
429
+
430
+ @staticmethod
431
+ def _normalize_status(value: Any) -> str | None:
432
+ if value is None:
433
+ return None
434
+ return str(value).upper()
435
+
436
+ @staticmethod
437
+ def _stringify(value: Any) -> Any:
438
+ if value is None:
439
+ return None
440
+ if isinstance(value, (bool, dict, list)):
441
+ return value
442
+ return str(value)
443
+
444
+ def _format(self, order: dict[str, Any] | None) -> dict[str, Any] | None:
445
+ if not order:
446
+ return None
447
+
448
+ order_id = (
449
+ order.get("orderId")
450
+ or order.get("id")
451
+ or order.get("order_id")
452
+ or order.get("orderID")
453
+ )
454
+
455
+ normalized_id = self._normalize_order_id(order_id)
456
+ if normalized_id is None:
457
+ return None
458
+
459
+ item: dict[str, Any] = {"orderId": normalized_id, "id": normalized_id}
460
+
461
+ side = self._normalize_side(order.get("side"))
462
+ if side is not None:
463
+ item["side"] = side
464
+
465
+ status = self._normalize_status(order.get("status"))
466
+ if status is not None:
467
+ item["status"] = status
468
+
469
+ contract_name = order.get("contractName")
470
+ if contract_name:
471
+ symbol = self._stringify(contract_name)
472
+ item["contractName"] = symbol
473
+ item.setdefault("symbol", symbol)
474
+
475
+ for field in self._KEEP_FIELDS:
476
+ if field in ("side", "status"):
477
+ continue
478
+ value = order.get(field)
479
+ if value is None:
480
+ continue
481
+ if field in self._BOOL_FIELDS:
482
+ item[field] = bool(value)
483
+ else:
484
+ item[field] = self._stringify(value)
485
+
486
+ return item
487
+
488
+
489
+ class Balance(DataStore):
490
+ """Account balance snapshot retaining only the trading-critical fields."""
491
+
492
+ _KEYS = ["accountId", "coinId"]
493
+
494
+
495
+ def _onresponse(self, data: dict[str, Any]) -> None:
496
+ data = data.get('data', {})
497
+ collateral_assets = data.get('collateralAssetModelList') or []
498
+ if collateral_assets:
499
+ self._update(collateral_assets)
500
+
501
+ def _on_message(self, msg: dict[str, Any]) -> None:
502
+ pass
503
+
504
+
505
+ class Position(DataStore):
506
+ """
507
+ Stores per-account open positions in a simplified camelCase schema.
508
+ Only the current open position fields are retained: positionId, contractId, accountId,
509
+ userId, coinId, side, size, value, fee, fundingFee.
510
+ """
511
+
512
+ _KEYS = ["positionId"]
513
+
514
+ @staticmethod
515
+ def _stringify(value: Any) -> Any:
516
+ if value is None:
517
+ return None
518
+ if isinstance(value, (bool, dict, list)):
519
+ return value
520
+ return str(value)
521
+
522
+ def _onresponse(self, data: dict[str, Any]) -> None:
523
+ """
524
+ Handle REST response for getAccountAsset (open positions snapshot).
525
+ Expects data from getAccountAsset (REST), which returns a snapshot of **current open positions**,
526
+ as a list in data["positionList"].
527
+ Each entry is normalized to camelCase schema, only including essential fields for the current open position.
528
+ """
529
+ data = data.get("data", {}) or {}
530
+ positions = data.get("positionList") or []
531
+ if not isinstance(positions, list):
532
+ positions = [positions]
533
+ items = [self._normalize_position(pos) for pos in positions]
534
+ self._clear()
535
+ if items:
536
+ self._update(items)
537
+
538
+ def _on_message(self, msg: dict[str, Any]) -> None:
539
+ data = msg.get("content", {}).get("data", {})
540
+ if not data:
541
+ return
542
+ positions = data.get("position")
543
+ if not positions:
544
+ return
545
+ items = [self._normalize_position(pos) for pos in positions]
546
+ self._clear()
547
+ if items:
548
+ self._update(items)
549
+
550
+ def _normalize_position(self, pos: dict[str, Any]) -> dict[str, Any]:
551
+ # Only keep essential fields for the current open position
552
+ def get(key, *alts):
553
+ for k in (key,) + alts:
554
+ if k in pos and pos[k] is not None:
555
+ return pos[k]
556
+ return None
557
+
558
+ open_size = get("openSize")
559
+ open_value = get("openValue")
560
+ open_fee = get("openFee")
561
+ funding_fee = get("fundingFee")
562
+
563
+ # side: "long" if openSize > 0, "short" if openSize < 0, None if 0
564
+ side = None
565
+ try:
566
+ if open_size is not None:
567
+ fsize = float(open_size)
568
+ if fsize > 0:
569
+ side = "long"
570
+ elif fsize < 0:
571
+ side = "short"
572
+ except Exception:
573
+ side = None
574
+
575
+ size = None
576
+ if open_size is not None:
577
+ try:
578
+ size = str(abs(float(open_size)))
579
+ except Exception:
580
+ size = str(open_size)
581
+ value = None
582
+ if open_value is not None:
583
+ try:
584
+ value = str(abs(float(open_value)))
585
+ except Exception:
586
+ value = str(open_value)
587
+
588
+ item = {
589
+ "positionId": self._stringify(get("positionId", "position_id")),
590
+ "contractId": self._stringify(get("contractId")),
591
+ "accountId": self._stringify(get("accountId")),
592
+ "userId": self._stringify(get("userId")),
593
+ "coinId": self._stringify(get("coinId")),
594
+ "side": side,
595
+ "size": size,
596
+ "value": value,
597
+ "fee": self._stringify(open_fee),
598
+ "fundingFee": self._stringify(funding_fee),
599
+ }
600
+ return item
601
+
602
+
603
+ class CoinMeta(DataStore):
604
+ """Coin metadata (precision, StarkEx info, etc.)."""
605
+
606
+ _KEYS = ["coinId"]
607
+
608
+ def _onresponse(self, data: dict[str, Any]) -> None:
609
+ coins = (data.get("data") or {}).get("coinList") or []
610
+ items: list[dict[str, Any]] = []
611
+
612
+ for coin in coins:
613
+ coin_id = coin.get("coinId")
614
+ if coin_id is None:
615
+ continue
616
+ items.append(
617
+ {
618
+ "coinId": str(coin_id),
619
+ "coinName": coin.get("coinName"),
620
+ "stepSize": coin.get("stepSize"),
621
+ "showStepSize": coin.get("showStepSize"),
622
+ "starkExAssetId": coin.get("starkExAssetId"),
623
+ }
624
+ )
625
+
626
+ self._clear()
627
+ if items:
628
+ self._insert(items)
629
+
630
+
631
+ class ContractMeta(DataStore):
632
+ """Per-contract trading parameters from the metadata endpoint."""
633
+
634
+ _KEYS = ["contractName"]
635
+
636
+ _FIELDS = (
637
+ "contractName",
638
+ "baseCoinId",
639
+ "quoteCoinId",
640
+ "tickSize",
641
+ "stepSize",
642
+ "minOrderSize",
643
+ "maxOrderSize",
644
+ "defaultTakerFeeRate",
645
+ "defaultMakerFeeRate",
646
+ "enableTrade",
647
+ "fundingInterestRate",
648
+ "fundingImpactMarginNotional",
649
+ "fundingRateIntervalMin",
650
+ "starkExSyntheticAssetId",
651
+ "starkExResolution",
652
+ )
653
+
654
+ def _onresponse(self, data: dict[str, Any]) -> None:
655
+ contracts = (data.get("data") or {}).get("contractList") or []
656
+ items: list[dict[str, Any]] = []
657
+
658
+ for contract in contracts:
659
+ contract_id = contract.get("contractId")
660
+ if contract_id is None:
661
+ continue
662
+
663
+ payload = {"contractId": str(contract_id)}
664
+ for key in self._FIELDS:
665
+ payload[key] = contract.get(key)
666
+ payload["riskTierList"] = self._simplify_risk_tiers(
667
+ contract.get("riskTierList")
668
+ )
669
+
670
+ items.append(payload)
671
+
672
+ self._clear()
673
+ if items:
674
+ self._insert(items)
675
+
676
+ @staticmethod
677
+ def _simplify_risk_tiers(risk_tiers: Any) -> list[dict[str, Any]]:
678
+ items: list[dict[str, Any]] = []
679
+ for tier in risk_tiers or []:
680
+ items.append(
681
+ {
682
+ "tier": tier.get("tier"),
683
+ "positionValueUpperBound": tier.get("positionValueUpperBound"),
684
+ "maxLeverage": tier.get("maxLeverage"),
685
+ "maintenanceMarginRate": tier.get("maintenanceMarginRate"),
686
+ "starkExRisk": tier.get("starkExRisk"),
687
+ "starkExUpperBound": tier.get("starkExUpperBound"),
688
+ }
689
+ )
690
+ return items
691
+
692
+
693
+ class AppMeta(DataStore):
694
+ """Global metadata (appName, env, fee account, etc.)."""
695
+
696
+ _KEYS = ["appName"]
697
+
698
+ def _onresponse(self, data: dict[str, Any]) -> None:
699
+ appdata = (data.get("data") or {}).get("global") or {}
700
+ if not appdata:
701
+ self._clear()
702
+ return
703
+ # Convert all values to str where appropriate, but preserve fields as-is (for bool/int etc).
704
+ item = {}
705
+ for k, v in appdata.items():
706
+ if k == "starkExCollateralCoin" and isinstance(v, dict):
707
+ # Flatten the dict into top-level fields with prefix
708
+ for subk, subv in v.items():
709
+ # Compose the flattened key
710
+ prefix = "starkExCollateral"
711
+ # Capitalize first letter of subkey
712
+ if subk and subk[0].islower():
713
+ flatkey = prefix + subk[0].upper() + subk[1:]
714
+ else:
715
+ flatkey = prefix + subk
716
+ item[flatkey] = subv if subv is None or isinstance(subv, (bool, int, float)) else str(subv)
717
+ continue
718
+ # Convert to str except for None; preserve bool/int/float as-is
719
+ if v is None:
720
+ item[k] = v
721
+ elif isinstance(v, (bool, int, float)):
722
+ item[k] = v
723
+ else:
724
+ item[k] = str(v)
725
+ self._clear()
726
+ if item:
727
+ self._insert([item])
728
+
729
+
730
+ class EdgexDataStore(DataStoreCollection):
731
+ """Edgex DataStore collection exposing the order book feed."""
732
+
733
+ def _init(self) -> None:
734
+ self._create("book", datastore_class=Book)
735
+ self._create("ticker", datastore_class=Ticker)
736
+ self._create("orders", datastore_class=Order)
737
+ self._create("balance", datastore_class=Balance)
738
+ # Position store holds per-account open positions in simplified camelCase form
739
+ self._create("position", datastore_class=Position)
740
+ self._create("meta_coin", datastore_class=CoinMeta)
741
+ self._create("detail", datastore_class=ContractMeta)
742
+ self._create("app", datastore_class=AppMeta)
743
+
744
+ @property
745
+ def book(self) -> Book:
746
+ """
747
+ 获取 Edgex 合约订单簿数据流。
748
+
749
+ .. code:: json
750
+
751
+ [
752
+ {
753
+ "c": "10000001", # 合约 ID
754
+ "s": "BTCUSD",
755
+ "S": "a", # 方向 a=卖 b=买
756
+ "p": "117388.2", # 价格
757
+ "q": "12.230", # 数量
758
+ }
759
+ ]
760
+ """
761
+ return self._get("book")
762
+
763
+ @property
764
+ def orders(self) -> Order:
765
+ """
766
+ 账户订单数据流(REST 快照 + 私有 WS 增量)。
767
+
768
+ 存储为**精简 schema**,仅保留实操必需字段。终态订单(FILLED / CANCELED / CANCELLED / REJECTED / EXPIRED)
769
+ 会在写入一次后从本地缓存删除,只保留进行中的订单(OPEN / PARTIALLY_FILLED / PENDING / CREATED / ACKNOWLEDGED)。
770
+
771
+
772
+ 存储示例(本地条目)
773
+ -------------------
774
+ REST 快照:
775
+
776
+ .. code:: json
777
+
778
+ [
779
+ {
780
+ "orderId": "564815695875932430",
781
+ "id": "564815695875932430",
782
+ "contractId": "10000001",
783
+ "contractName": "BTCUSD",
784
+ "symbol": "BTCUSD",
785
+ "side": "buy",
786
+ "status": "OPEN",
787
+ "type": "LIMIT",
788
+ "timeInForce": "GOOD_TIL_CANCEL",
789
+ "reduceOnly": false,
790
+ "price": "97444.5",
791
+ "size": "0.010",
792
+ "cumFillSize": "0.000",
793
+ "cumFillValue": "0",
794
+ "clientOrderId": "553364074986685",
795
+ "createdTime": "1734662555665",
796
+ "updatedTime": "1734662555665"
797
+ }
798
+ ]
799
+
800
+
801
+ """
802
+ return self._get("orders")
803
+
804
+ @property
805
+ def balance(self) -> Balance:
806
+ """
807
+ 获取账户资产余额(REST 快照 + 私有 WS 增量)。
808
+
809
+ .. code:: json
810
+
811
+ [
812
+ {
813
+ userId: "663528067892773124",
814
+ accountId: "663528067938910372",
815
+ coinId: "1000",
816
+ totalEquity: "22.721859",
817
+ totalPositionValueAbs: "0",
818
+ initialMarginRequirement: "0",
819
+ starkExRiskValue: "0",
820
+ pendingWithdrawAmount: "0",
821
+ pendingTransferOutAmount: "0",
822
+ orderFrozenAmount: "3.001126965030794963240623474121093750",
823
+ availableAmount: "19.720732",
824
+ },
825
+ ]
826
+
827
+ """
828
+ return self._get("balance")
829
+
830
+ @property
831
+ def position(self) -> "Position":
832
+ """
833
+ 获取账户当前未平仓持仓(open positions,来自 getAccountAsset)。
834
+
835
+ 本属性提供**当前未平仓持仓**的快照(由 REST ``getAccountAsset`` 提供),每条数据为当前账户的一个持仓(多/空/逐仓/全仓等)。
836
+ 字段为 snake_case,包含持仓数量、均价、强平价、杠杆、保证金率等信息,适合用于持仓管理与风险监控。
837
+
838
+ 数据示例:
839
+
840
+ .. code:: python
841
+
842
+ [
843
+ {
844
+ orderId: "665307878751470244",
845
+ id: "665307878751470244",
846
+ side: "buy",
847
+ status: "OPEN",
848
+ userId: "663528067892773124",
849
+ accountId: "663528067938910372",
850
+ coinId: "1000",
851
+ contractId: "10000003",
852
+ clientOrderId: "32570392453812747",
853
+ type: "LIMIT",
854
+ timeInForce: "GOOD_TIL_CANCEL",
855
+ reduceOnly: False,
856
+ price: "210.00",
857
+ size: "0.3",
858
+ cumFillSize: "0",
859
+ cumFillValue: "0",
860
+ cumMatchSize: "0",
861
+ cumMatchValue: "0",
862
+ cumMatchFee: "0",
863
+ triggerPrice: "0",
864
+ triggerPriceType: "UNKNOWN_PRICE_TYPE",
865
+ cancelReason: "UNKNOWN_ORDER_CANCEL_REASON",
866
+ createdTime: "1758621759117",
867
+ updatedTime: "1758621759122",
868
+ matchSequenceId: "784278904",
869
+ },
870
+ ];
871
+
872
+
873
+ 本属性仅包含**当前持有的未平仓持仓**(由 REST ``getAccountAsset`` 提供)。
874
+ 若需获取**历史已平仓持仓周期**,请调用 ``getPositionTermPage``。
875
+ """
876
+ return self._get("position")
877
+
878
+ @property
879
+ def coins(self) -> CoinMeta:
880
+ """
881
+ 获取币种精度及 StarkEx 资产信息列表。
882
+
883
+ .. code:: json
884
+
885
+ [
886
+ {
887
+ "coinId": "1000",
888
+ "coinName": "USDT",
889
+ "stepSize": "0.000001",
890
+ "showStepSize": "0.0001",
891
+ "starkExAssetId": "0x33bda5c9..."
892
+ }
893
+ ]
894
+ """
895
+ return self._get("meta_coin")
896
+
897
+ @property
898
+ def detail(self) -> ContractMeta:
899
+ """
900
+ 获取合约级别的交易参数。
901
+
902
+ .. code:: json
903
+
904
+ [
905
+ {
906
+ "contractId": "10000001",
907
+ "contractName": "BTCUSD",
908
+ "baseCoinId": "1001",
909
+ "quoteCoinId": "1000",
910
+ "tickSize": "0.1",
911
+ "stepSize": "0.001",
912
+ "minOrderSize": "0.001",
913
+ "maxOrderSize": "50.000",
914
+ "defaultMakerFeeRate": "0.0002",
915
+ "defaultTakerFeeRate": "0.00055",
916
+ "enableTrade": true,
917
+ "fundingInterestRate": "0.0003",
918
+ "fundingImpactMarginNotional": "10",
919
+ "fundingRateIntervalMin": "240",
920
+ "starkExSyntheticAssetId": "0x42544332...",
921
+ "starkExResolution": "0x2540be400",
922
+ "riskTierList": [
923
+ {
924
+ "tier": 1,
925
+ "positionValueUpperBound": "50000",
926
+ "maxLeverage": "100",
927
+ "maintenanceMarginRate": "0.005",
928
+ "starkExRisk": "21474837",
929
+ "starkExUpperBound": "214748364800000000000"
930
+ }
931
+ ]
932
+ }
933
+ ]
934
+ """
935
+ return self._get("detail")
936
+
937
+ @property
938
+ def ticker(self) -> Ticker:
939
+ """
940
+ 获取 24 小时行情推送。
941
+
942
+ .. code:: json
943
+
944
+ [
945
+ {
946
+ "c": "10000001", # 合约 ID
947
+ "s": "BTCUSD", # 合约名称
948
+ "lastPrice": "117400", # 最新价
949
+ "priceChange": "200", # 涨跌额
950
+ "priceChangePercent": "0.0172", # 涨跌幅
951
+ "size": "1250", # 24h 成交量
952
+ "value": "147000000", # 24h 成交额
953
+ "high": "118000", # 24h 最高价
954
+ "low": "116500", # 低价
955
+ "open": "116800", # 开盘价
956
+ "close": "117400", # 收盘价
957
+ "indexPrice": "117350", # 指数价
958
+ "oraclePrice": "117360.12", # 预言机价
959
+ "openInterest": "50000", # 持仓量
960
+ "fundingRate": "0.000234", # 当前资金费率
961
+ "fundingTime": "1758240000000", # 上一次结算时间
962
+ "nextFundingTime": "1758254400000", # 下一次结算时间
963
+ "bestAskPrice": "117410", # 卖一价
964
+ "bestBidPrice": "117400" # 买一价
965
+ }
966
+ ]
967
+ """
968
+ return self._get("ticker")
969
+
970
+ async def initialize(self, *aws: Awaitable["ClientResponse"]) -> None:
971
+ """Populate metadata stores from awaited HTTP responses."""
972
+
973
+ for fut in asyncio.as_completed(aws):
974
+ res = await fut
975
+ data = await res.json()
976
+ if data['code'] != 'SUCCESS':
977
+ raise ValueError(f"Unexpected response code: {data}")
978
+ if res.url.path == "/api/v1/public/meta/getMetaData":
979
+ self._apply_metadata(data)
980
+ elif res.url.path == "/api/v1/private/account/getAccountAsset":
981
+ self.balance._onresponse(data)
982
+ self.position._onresponse(data)
983
+ elif res.url.path == "/api/v1/private/order/getActiveOrderPage":
984
+ self.orders._onresponse(data)
985
+ elif res.url.path == "/api/v1/public/quote/getTicker":
986
+ self.ticker._onresponse(data)
987
+
988
+ def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
989
+ # print(msg)
990
+ channel = (msg.get("channel") or "").lower()
991
+ msg_type = (msg.get("type") or "").lower()
992
+
993
+ if msg_type == "ping" and ws is not None:
994
+ payload = {"type": "pong", "time": msg.get("time")}
995
+ asyncio.create_task(ws.send_json(payload))
996
+ return
997
+
998
+ if msg_type in {"trade-event", "trade_event", "order-event", "order_event"}:
999
+ self.orders._on_message(msg)
1000
+ self.position._on_message(msg)
1001
+
1002
+ if "depth" in channel and msg_type in {"quote-event", "payload"}:
1003
+ self.book._on_message(msg)
1004
+
1005
+ if channel.startswith("ticker") and msg_type in {"payload", "quote-event"}:
1006
+ self.ticker._on_message(msg)
1007
+
1008
+ def _apply_metadata(self, data: dict[str, Any]) -> None:
1009
+ self.app._onresponse(data)
1010
+ self.coins._onresponse(data)
1011
+ self.detail._onresponse(data)
1012
+
1013
+
1014
+ @property
1015
+ def app(self) -> AppMeta:
1016
+ """
1017
+ 获取全局元数据,如 appName、环境、fee 账户等。
1018
+
1019
+ .. code:: python
1020
+
1021
+
1022
+ [
1023
+ {
1024
+ "appName": "edgeX",
1025
+ "appEnv": "mainnet",
1026
+ "appOnlySignOn": "https://pro.edgex.exchange",
1027
+ "feeAccountId": "256105",
1028
+ "feeAccountL2Key": "0x70092acf49d535fbb64d99883abda95dcf9a4fc60f494437a3d76f27db0a0f5",
1029
+ "poolAccountId": "508126509156794507",
1030
+ "poolAccountL2Key": "0x7f2e1e8a572c847086ee93c9b5bbce8b96320aaa69147df1cfca91d5e90bc60",
1031
+ "fastWithdrawAccountId": "508126509156794507",
1032
+ "fastWithdrawAccountL2Key": "0x7f2e1e8a572c847086ee93c9b5bbce8b96320aaa69147df1cfca91d5e90bc60",
1033
+ "fastWithdrawMaxAmount": "100000",
1034
+ "fastWithdrawRegistryAddress": "0xBE9a129909EbCb954bC065536D2bfAfBd170d27A",
1035
+ "starkExChainId": "0x1",
1036
+ "starkExContractAddress": "0xfAaE2946e846133af314d1Df13684c89fA7d83DD",
1037
+ "starkExCollateralCoinId": "1000",
1038
+ "starkExCollateralCoinName": "USD",
1039
+ "starkExCollateralStepSize": "0.000001",
1040
+ "starkExCollateralShowStepSize": "0.0001",
1041
+ "starkExCollateralIconUrl": "https://static.edgex.exchange/icons/coin/USDT.svg",
1042
+ "starkExCollateralStarkExAssetId": "0x2ce625e94458d39dd0bf3b45a843544dd4a14b8169045a3a3d15aa564b936c5",
1043
+ "starkExCollateralStarkExResolution": "0xf4240",
1044
+ "starkExMaxFundingRate": 12000,
1045
+ "starkExOrdersTreeHeight": 64,
1046
+ "starkExPositionsTreeHeight": 64,
1047
+ "starkExFundingValidityPeriod": 86400,
1048
+ "starkExPriceValidityPeriod": 86400,
1049
+ "maintenanceReason": "",
1050
+ }
1051
+ ]
1052
+ """
1053
+ return self._get("app")