hyperquant 1.55__tar.gz → 1.56__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 (46) hide show
  1. {hyperquant-1.55 → hyperquant-1.56}/.gitignore +5 -0
  2. {hyperquant-1.55 → hyperquant-1.56}/PKG-INFO +1 -1
  3. {hyperquant-1.55 → hyperquant-1.56}/pyproject.toml +1 -1
  4. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/polymarket.py +396 -186
  5. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/polymarket.py +9 -19
  6. {hyperquant-1.55 → hyperquant-1.56}/uv.lock +1 -1
  7. {hyperquant-1.55 → hyperquant-1.56}/README.md +0 -0
  8. {hyperquant-1.55 → hyperquant-1.56}/requirements-dev.lock +0 -0
  9. {hyperquant-1.55 → hyperquant-1.56}/requirements.lock +0 -0
  10. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/__init__.py +0 -0
  11. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/auth.py +0 -0
  12. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/bitget.py +0 -0
  13. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/bitmart.py +0 -0
  14. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/coinw.py +0 -0
  15. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/deepcoin.py +0 -0
  16. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/edgex.py +0 -0
  17. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/hyperliquid.py +0 -0
  18. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/lbank.py +0 -0
  19. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
  20. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/lib/hpstore.py +0 -0
  21. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/lib/hyper_types.py +0 -0
  22. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/lib/polymarket/ctfAbi.py +0 -0
  23. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/lib/polymarket/safeAbi.py +0 -0
  24. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/lib/util.py +0 -0
  25. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/lighter.py +0 -0
  26. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/apexpro.py +0 -0
  27. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/bitget.py +0 -0
  28. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/bitmart.py +0 -0
  29. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/coinw.py +0 -0
  30. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/deepcoin.py +0 -0
  31. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/edgex.py +0 -0
  32. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/hyperliquid.py +0 -0
  33. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/lbank.py +0 -0
  34. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/lighter.py +0 -0
  35. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/ourbit.py +0 -0
  36. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/ourbit.py +0 -0
  37. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/ws.py +0 -0
  38. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/core.py +0 -0
  39. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/datavison/_util.py +0 -0
  40. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/datavison/binance.py +0 -0
  41. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/datavison/coinglass.py +0 -0
  42. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/datavison/okx.py +0 -0
  43. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/db.py +0 -0
  44. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/draw.py +0 -0
  45. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/logkit.py +0 -0
  46. {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/notikit.py +0 -0
@@ -28,6 +28,7 @@ wheels/
28
28
  env/
29
29
  venv/
30
30
  ENV/
31
+ apis.json
31
32
 
32
33
  # IDE
33
34
  .idea/
@@ -38,3 +39,7 @@ ENV/
38
39
  # OS
39
40
  .DS_Store
40
41
  Thumbs.db
42
+
43
+ # Local runtime artifacts
44
+ *.log
45
+ polymarket_orders.log
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperquant
3
- Version: 1.55
3
+ Version: 1.56
4
4
  Summary: A minimal yet hyper-efficient backtesting framework for quantitative trading
5
5
  Project-URL: Homepage, https://github.com/yourusername/hyperquant
6
6
  Project-URL: Issues, https://github.com/yourusername/hyperquant/issues
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hyperquant"
3
- version = "1.55"
3
+ version = "1.56"
4
4
  description = "A minimal yet hyper-efficient backtesting framework for quantitative trading"
5
5
  authors = [
6
6
  { name = "MissinA", email = "1421329142@qq.com" }
@@ -14,186 +14,413 @@ if TYPE_CHECKING:
14
14
 
15
15
 
16
16
  class Position(DataStore):
17
- """Position DataStore keyed by Polymarket token id."""
17
+ """Position store fed only by User WS trade events."""
18
18
 
19
19
  _KEYS = ["asset"]
20
20
 
21
21
  def _init(self) -> None:
22
- # 缓存LIVE订单已计入的size_matched: {order_id: size_matched}
23
- self._live_cache: dict[str, float] = {}
22
+ self._owner_key: str | None = None
23
+ self._dedup_seen: dict[str, float] = {}
24
+ self._dedup_ttl_secs = 15 * 60
25
+ self._dedup_max_entries = 50_000
26
+ self._market_state: dict[str, dict[str, Any]] = {}
27
+ self._market_assets: dict[str, dict[str, str | None]] = {}
24
28
 
29
+ def set_owner_key(self, api_key: str | None) -> None:
30
+ owner = str(api_key).strip().lower() if api_key else ""
31
+ self._owner_key = owner or None
25
32
 
26
33
  def sorted(
27
34
  self, query: Item | None = None, limit: int | None = None
28
35
  ) -> dict[str, list[Item]]:
29
- """按ts降序排列,按outcome分组"""
30
36
  if query is None:
31
37
  query = {}
32
38
  result: dict[str, list[Item]] = {}
33
39
  for item in self:
34
40
  if all(k in item and query[k] == item[k] for k in query):
35
41
  outcome = item.get("outcome") or "unknown"
36
- if outcome not in result:
37
- result[outcome] = []
38
- result[outcome].append(item)
42
+ result.setdefault(outcome, []).append(item)
39
43
  for outcome in result:
40
- result[outcome].sort(key=lambda x: (x.get("eventSlug") or '0'), reverse=True)
44
+ result[outcome].sort(key=lambda x: float(x.get("ts") or 0.0), reverse=True)
41
45
  if limit:
42
46
  result[outcome] = result[outcome][:limit]
43
47
  return result
44
48
 
45
49
  def _on_response(self, msg: list[Item]) -> None:
46
- if msg:
47
- self._clear()
48
- for rec in msg:
49
- rec["ts"] = 0
50
- self._update(msg)
50
+ # Inventory is user-trade driven only (do not overwrite via REST snapshots).
51
+ _ = msg
52
+ return
51
53
 
52
- def on_trade(self, trade: Item) -> None:
53
- status = str(trade.get("status") or "").upper()
54
- if status not in {"MATCHED"}:
55
- return
54
+ @staticmethod
55
+ def _to_float(value: Any) -> float | None:
56
+ try:
57
+ return float(value)
58
+ except (TypeError, ValueError):
59
+ return None
56
60
 
57
- asset_id = trade.get("asset_id")
58
- outcome = trade.get("outcome")
59
- side = str(trade.get("side") or "").upper()
60
- size_raw = trade.get("size")
61
- price_raw = trade.get("price")
61
+ @staticmethod
62
+ def _status_bucket(status: str) -> str | None:
63
+ if status in {"MATCHED", "MINED", "CONFIRMED"}:
64
+ return "SUCCESS"
65
+ if status == "FAILED":
66
+ return "FAILED"
67
+ return None
68
+
69
+ @staticmethod
70
+ def _normalize_owner(owner: Any) -> str:
71
+ return str(owner or "").strip().lower()
72
+
73
+ @staticmethod
74
+ def _dedup_bucket(status_bucket: str) -> str:
75
+ return "SUCCESS" if status_bucket == "SUCCESS" else "FAILED"
76
+
77
+ def _remember_dedup(self, key: str) -> bool:
78
+ now = time.time()
79
+ cutoff = now - self._dedup_ttl_secs
80
+ if self._dedup_seen:
81
+ stale = [k for k, ts in self._dedup_seen.items() if ts < cutoff]
82
+ for k in stale:
83
+ self._dedup_seen.pop(k, None)
84
+
85
+ if key in self._dedup_seen:
86
+ return False
87
+
88
+ self._dedup_seen[key] = now
89
+ if len(self._dedup_seen) > self._dedup_max_entries:
90
+ oldest_key = min(self._dedup_seen, key=self._dedup_seen.get)
91
+ self._dedup_seen.pop(oldest_key, None)
92
+ return True
93
+
94
+ def _infer_side_key(
95
+ self,
96
+ *,
97
+ market: str,
98
+ asset_id: str,
99
+ outcome: str | None,
100
+ outcome_index: Any,
101
+ ) -> str | None:
102
+ assets = self._market_assets.setdefault(market, {"YES": None, "NO": None})
103
+
104
+ mapped_side: str | None = None
105
+ if asset_id:
106
+ if assets.get("YES") == asset_id:
107
+ mapped_side = "YES"
108
+ elif assets.get("NO") == asset_id:
109
+ mapped_side = "NO"
110
+
111
+ # Prefer outcome text for side inference; outcome_index is fallback only.
112
+ text_side: str | None
113
+ text = str(outcome or "").strip().upper()
114
+ if text in {"YES", "UP", "TRUE"}:
115
+ text_side = "YES"
116
+ elif text in {"NO", "DOWN", "FALSE"}:
117
+ text_side = "NO"
118
+ else:
119
+ text_side = None
120
+
121
+ idx_side: str | None = None
122
+ if outcome_index is not None:
123
+ try:
124
+ idx = int(outcome_index)
125
+ if idx == 0:
126
+ idx_side = "YES"
127
+ elif idx == 1:
128
+ idx_side = "NO"
129
+ except (TypeError, ValueError):
130
+ idx_side = None
131
+
132
+ explicit_side = text_side if text_side is not None else idx_side
133
+
134
+ # Correct a previously wrong binding once explicit side appears.
135
+ if mapped_side and explicit_side and mapped_side != explicit_side:
136
+ dst_asset = assets.get(explicit_side)
137
+ if dst_asset in (None, asset_id):
138
+ assets[mapped_side] = None
139
+ assets[explicit_side] = asset_id
140
+ self._migrate_side_state(market=market, from_side=mapped_side, to_side=explicit_side)
141
+ mapped_side = explicit_side
142
+ else:
143
+ # Conflicting mapping (both legs already occupied by different assets): skip.
144
+ return None
145
+
146
+ if mapped_side:
147
+ return mapped_side
62
148
 
149
+ # Unknown outcome/index: do not guess side to avoid poisoning inventory.
150
+ if explicit_side is None:
151
+ return None
152
+
153
+ bound = assets.get(explicit_side)
154
+ if bound not in (None, asset_id):
155
+ return None
156
+ assets[explicit_side] = asset_id
157
+ return explicit_side
63
158
 
64
- if not asset_id or not outcome or side not in {"BUY", "SELL"}:
159
+ def _migrate_side_state(self, *, market: str, from_side: str, to_side: str) -> None:
160
+ if from_side == to_side:
161
+ return
162
+ state = self._market_state.get(market)
163
+ if not state:
65
164
  return
66
165
 
67
- try:
68
- size = float(size_raw)
69
- except (TypeError, ValueError):
166
+ from_qty_key = "yes_qty" if from_side == "YES" else "no_qty"
167
+ from_avg_key = "yes_avg_cost" if from_side == "YES" else "no_avg_cost"
168
+ to_qty_key = "yes_qty" if to_side == "YES" else "no_qty"
169
+ to_avg_key = "yes_avg_cost" if to_side == "YES" else "no_avg_cost"
170
+
171
+ from_qty = float(state.get(from_qty_key) or 0.0)
172
+ from_avg = float(state.get(from_avg_key) or 0.0)
173
+ to_qty = float(state.get(to_qty_key) or 0.0)
174
+
175
+ # Safe migration only when destination leg has no existing inventory.
176
+ if to_qty > 1e-12:
70
177
  return
71
- try:
72
- price = float(price_raw)
73
- except (TypeError, ValueError):
74
- price = None
75
-
76
-
77
-
78
- key = {"asset": asset_id, "outcome": outcome}
79
- existing = self.get(key) or {}
80
-
81
- cur_size = float(existing.get("size") or 0.0)
82
- cur_total_bought = float(existing.get("totalBought") or 0.0)
83
- cur_avg_price = float(existing.get("avgPrice") or 0.0)
84
- cur_cost = cur_size * cur_avg_price
85
-
86
- if side == "BUY":
87
- new_size = cur_size + size
88
- total_bought = cur_total_bought + size
89
- # 未拿到成交价时使用当前均价兜底,避免均价被拉低
90
- effective_price = price if price is not None else cur_avg_price
91
- new_cost = cur_cost + size * effective_price
92
- else: # SELL
93
- new_size = cur_size - size
94
- total_bought = cur_total_bought
95
- # 卖出按照当前均价释放成本
96
- new_cost = cur_cost - min(size, cur_size) * cur_avg_price
97
-
98
- if new_size <= 0:
99
- new_size = 0.0
100
- avg_price = 0.0
101
- new_cost = 0.0
102
- else:
103
- avg_price = max(new_cost, 0.0) / new_size
178
+ state[to_qty_key] = from_qty
179
+ state[to_avg_key] = from_avg
180
+ state[from_qty_key] = 0.0
181
+ state[from_avg_key] = 0.0
182
+ self._recompute_derived(state)
104
183
 
105
- rec: dict[str, Any] = {
106
- "asset": asset_id,
107
- "outcome": outcome,
108
- "side": side,
109
- "size": new_size,
110
- "totalBought": total_bought,
111
- "avgPrice": avg_price,
184
+ def _default_state(self) -> dict[str, Any]:
185
+ return {
186
+ "yes_qty": 0.0,
187
+ "no_qty": 0.0,
188
+ "yes_avg_cost": 0.0,
189
+ "no_avg_cost": 0.0,
190
+ "net_diff": 0.0,
191
+ "portfolio_cost": 0.0,
192
+ "yes_asset": None,
193
+ "no_asset": None,
194
+ "yes_outcome": "Yes",
195
+ "no_outcome": "No",
112
196
  }
113
197
 
198
+ def _recompute_derived(self, state: dict[str, Any]) -> None:
199
+ yes_qty = float(state.get("yes_qty") or 0.0)
200
+ no_qty = float(state.get("no_qty") or 0.0)
201
+ yes_avg = float(state.get("yes_avg_cost") or 0.0)
202
+ no_avg = float(state.get("no_avg_cost") or 0.0)
203
+
204
+ state["net_diff"] = yes_qty - no_qty
205
+ state["portfolio_cost"] = (yes_avg + no_avg) if (yes_qty > 0.0 and no_qty > 0.0) else 0.0
206
+
207
+ def _upsert_row(self, row: dict[str, Any]) -> None:
208
+ existing = self.get({"asset": row["asset"]})
114
209
  if existing:
115
- self._update([rec])
210
+ self._update([row])
116
211
  else:
117
- self._insert([rec])
212
+ self._insert([row])
213
+
214
+ def _publish_market_state(self, market: str, state: dict[str, Any]) -> None:
215
+ now_ms = int(time.time() * 1000)
216
+
217
+ common = {
218
+ "market": market,
219
+ "yes_qty": float(state.get("yes_qty") or 0.0),
220
+ "no_qty": float(state.get("no_qty") or 0.0),
221
+ "yes_avg_cost": float(state.get("yes_avg_cost") or 0.0),
222
+ "no_avg_cost": float(state.get("no_avg_cost") or 0.0),
223
+ "net_diff": float(state.get("net_diff") or 0.0),
224
+ "portfolio_cost": float(state.get("portfolio_cost") or 0.0),
225
+ "ts": now_ms,
226
+ "source": "user_ws_trade",
227
+ }
118
228
 
119
- def _on_order(self, order: dict[str, Any]) -> None:
120
- """通过order更新持仓,处理LIVE时部分成交的增量统计"""
121
- # print(order)
122
- # order写入本地尝试后续分析
123
- # with open("polymarket_orders.log", "a") as f:
124
- # f.write(json.dumps(order) + "\n")
125
- order_id = order.get("id")
126
- asset_id = order.get("asset_id")
127
- outcome = order.get("outcome")
128
- side = str(order.get("side") or "").upper()
129
- size_matched = float(order.get("size_matched") or 0)
130
- price = float(order.get("price") or 0)
131
- status = str(order.get("status") or "").upper()
132
-
133
- if not order_id or not asset_id or not outcome or side not in {"BUY", "SELL"}:
229
+ yes_asset = state.get("yes_asset")
230
+ no_asset = state.get("no_asset")
231
+ if yes_asset:
232
+ yes_qty = common["yes_qty"]
233
+ yes_avg = common["yes_avg_cost"]
234
+ self._upsert_row(
235
+ {
236
+ "asset": yes_asset,
237
+ "outcome": state.get("yes_outcome") or "Yes",
238
+ "size": yes_qty,
239
+ "avgPrice": yes_avg,
240
+ "totalAvgPrice": yes_avg,
241
+ **common,
242
+ }
243
+ )
244
+ if no_asset:
245
+ no_qty = common["no_qty"]
246
+ no_avg = common["no_avg_cost"]
247
+ self._upsert_row(
248
+ {
249
+ "asset": no_asset,
250
+ "outcome": state.get("no_outcome") or "No",
251
+ "size": no_qty,
252
+ "avgPrice": no_avg,
253
+ "totalAvgPrice": no_avg,
254
+ **common,
255
+ }
256
+ )
257
+
258
+ def _apply_fill(self, fill: dict[str, Any], status_bucket: str) -> None:
259
+ market = str(fill.get("market") or "")
260
+ if not market:
261
+ market = "__unknown_market__"
262
+
263
+ asset_id = str(fill.get("asset_id") or "")
264
+ if not asset_id:
265
+ return
266
+
267
+ outcome = fill.get("outcome")
268
+ side = str(fill.get("side") or "").upper()
269
+ size = self._to_float(fill.get("size"))
270
+ price = self._to_float(fill.get("price"))
271
+ if side not in {"BUY", "SELL"} or size is None or price is None or size <= 0.0 or price <= 0.0:
134
272
  return
135
273
 
136
- cached = self._live_cache.get(order_id, 0.0)
137
-
138
- if status == "LIVE":
139
- # LIVE时计算增量
140
- delta = size_matched - cached
141
- if delta > 0:
142
- self._live_cache[order_id] = size_matched
143
- self._apply_trade(asset_id, outcome, side, delta, price)
144
- elif status in {"CANCELED", "MATCHED"}:
145
- # 订单完结:计算最终增量 = 最终size_matched - 已计入的cached
146
- delta = size_matched - cached
147
- if delta > 0:
148
- self._apply_trade(asset_id, outcome, side, delta, price)
149
- # 清理缓存
150
- self._live_cache.pop(order_id, None)
151
-
152
- def _apply_trade(self, asset_id: str, outcome: str, side: str, size: float, price: float) -> None:
153
- """应用成交到持仓"""
154
- if size <= 0:
274
+ side_key = self._infer_side_key(
275
+ market=market,
276
+ asset_id=asset_id,
277
+ outcome=str(outcome or ""),
278
+ outcome_index=fill.get("outcome_index"),
279
+ )
280
+ if side_key is None:
155
281
  return
156
282
 
157
- key = {"asset": asset_id, "outcome": outcome}
158
- existing = self.get(key) or {}
159
-
160
- cur_size = float(existing.get("size") or 0.0)
161
- cur_total_bought = float(existing.get("totalBought") or 0.0)
162
- cur_avg_price = float(existing.get("avgPrice") or 0.0)
163
- cur_cost = cur_size * cur_avg_price
164
-
165
- if side == "BUY":
166
- new_size = cur_size + size
167
- total_bought = cur_total_bought + size
168
- effective_price = price if price else cur_avg_price
169
- new_cost = cur_cost + size * effective_price
170
- else: # SELL
171
- new_size = cur_size - size
172
- total_bought = cur_total_bought
173
- new_cost = cur_cost - min(size, cur_size) * cur_avg_price
174
-
175
- if new_size <= 0:
176
- new_size = 0.0
177
- avg_price = 0.0
178
- new_cost = 0.0
283
+ state = self._market_state.setdefault(market, self._default_state())
284
+ assets = self._market_assets.get(market) or {}
285
+ state["yes_asset"] = assets.get("YES")
286
+ state["no_asset"] = assets.get("NO")
287
+ if side_key == "YES":
288
+ state["yes_asset"] = asset_id
289
+ if outcome:
290
+ state["yes_outcome"] = outcome
179
291
  else:
180
- avg_price = max(new_cost, 0.0) / new_size
292
+ state["no_asset"] = asset_id
293
+ if outcome:
294
+ state["no_outcome"] = outcome
295
+
296
+ trade_sign = 1.0 if side == "BUY" else -1.0
297
+ status_sign = -1.0 if status_bucket == "FAILED" else 1.0
298
+ delta = size * trade_sign * status_sign
299
+ if abs(delta) < 1e-12:
300
+ return
181
301
 
182
- rec: dict[str, Any] = {
183
- "asset": asset_id,
184
- "outcome": outcome,
185
- "side": side,
186
- "size": new_size,
187
- "totalBought": total_bought,
188
- "totalAvgPrice": avg_price,
189
- "avgPrice": avg_price,
190
- "ts": int(time.time() * 1000),
191
- }
302
+ qty_key = "yes_qty" if side_key == "YES" else "no_qty"
303
+ avg_key = "yes_avg_cost" if side_key == "YES" else "no_avg_cost"
304
+ old_qty = float(state.get(qty_key) or 0.0)
305
+ old_avg = float(state.get(avg_key) or 0.0)
306
+ new_qty = max(old_qty + delta, 0.0)
307
+ state[qty_key] = new_qty
308
+
309
+ if delta > 0.0 and new_qty > 0.0:
310
+ state[avg_key] = (old_qty * old_avg + delta * price) / new_qty
311
+ elif new_qty < 1e-12:
312
+ state[avg_key] = 0.0
313
+ else:
314
+ state[avg_key] = old_avg
192
315
 
193
- if existing:
194
- self._update([rec])
316
+ self._recompute_derived(state)
317
+ self._publish_market_state(market, state)
318
+
319
+ def _parse_maker_fills(self, trade: Item) -> list[dict[str, Any]]:
320
+ fills: list[dict[str, Any]] = []
321
+ maker_orders = trade.get("maker_orders") or []
322
+ if not isinstance(maker_orders, list):
323
+ return fills
324
+
325
+ trade_price = self._to_float(trade.get("price"))
326
+ trade_size = self._to_float(trade.get("size"))
327
+ trade_side = str(trade.get("side") or "").upper()
328
+
329
+ for maker in maker_orders:
330
+ if not isinstance(maker, dict):
331
+ continue
332
+ owner = self._normalize_owner(maker.get("owner"))
333
+ if self._owner_key and (not owner or owner != self._owner_key):
334
+ continue
335
+
336
+ side = str(maker.get("side") or trade_side or "").upper()
337
+ size = self._to_float(maker.get("matched_amount"))
338
+ if size is None:
339
+ size = self._to_float(maker.get("size"))
340
+ if size is None:
341
+ size = trade_size
342
+ price = self._to_float(maker.get("price"))
343
+ if price is None:
344
+ price = trade_price
345
+ if side not in {"BUY", "SELL"} or size is None or price is None:
346
+ continue
347
+ fills.append(
348
+ {
349
+ "trade_id": trade.get("id"),
350
+ "order_id": maker.get("order_id"),
351
+ "market": trade.get("market"),
352
+ "asset_id": maker.get("asset_id") or trade.get("asset_id"),
353
+ "outcome": maker.get("outcome") or trade.get("outcome"),
354
+ "outcome_index": maker.get("outcome_index"),
355
+ "side": side,
356
+ "size": size,
357
+ "price": price,
358
+ }
359
+ )
360
+ return fills
361
+
362
+ def _parse_taker_fill(self, trade: Item) -> list[dict[str, Any]]:
363
+ owner = self._normalize_owner(trade.get("trade_owner") or trade.get("owner"))
364
+ if self._owner_key and owner and owner != self._owner_key:
365
+ return []
366
+
367
+ side = str(trade.get("side") or "").upper()
368
+ size = self._to_float(trade.get("size"))
369
+ price = self._to_float(trade.get("price"))
370
+ if side not in {"BUY", "SELL"} or size is None or price is None:
371
+ return []
372
+
373
+ return [
374
+ {
375
+ "trade_id": trade.get("id"),
376
+ "order_id": trade.get("taker_order_id") or trade.get("order_id"),
377
+ "market": trade.get("market"),
378
+ "asset_id": trade.get("asset_id"),
379
+ "outcome": trade.get("outcome"),
380
+ "outcome_index": trade.get("outcome_index"),
381
+ "side": side,
382
+ "size": size,
383
+ "price": price,
384
+ }
385
+ ]
386
+
387
+ def on_trade(self, trade: Item) -> None:
388
+ status = str(trade.get("status") or "").upper()
389
+ status_bucket = self._status_bucket(status)
390
+ if status_bucket is None:
391
+ return
392
+
393
+ trader_side = str(trade.get("trader_side") or "")
394
+ maker_orders = trade.get("maker_orders") or []
395
+ has_maker_orders = isinstance(maker_orders, list) and len(maker_orders) > 0
396
+
397
+ if trader_side.upper() == "MAKER" or (not trader_side and has_maker_orders):
398
+ fills = self._parse_maker_fills(trade)
399
+ if not fills:
400
+ return
195
401
  else:
196
- self._insert([rec])
402
+ fills = self._parse_taker_fill(trade)
403
+
404
+ bucket = self._dedup_bucket(status_bucket)
405
+ for fill in fills:
406
+ trade_id = str(fill.get("trade_id") or "")
407
+ order_id = str(fill.get("order_id") or "")
408
+ if trade_id:
409
+ dedup_key = f"tid:{trade_id}:oid:{order_id}:{bucket}"
410
+ else:
411
+ dedup_key = (
412
+ f"oid:{order_id}:{bucket}:"
413
+ f"{fill.get('asset_id')}:{fill.get('side')}:"
414
+ f"{fill.get('price')}:{fill.get('size')}"
415
+ )
416
+ if not self._remember_dedup(dedup_key):
417
+ continue
418
+ self._apply_fill(fill, status_bucket)
419
+
420
+ def _on_order(self, order: dict[str, Any]) -> None:
421
+ # Position is no longer order-driven.
422
+ _ = order
423
+ return
197
424
 
198
425
 
199
426
 
@@ -796,6 +1023,9 @@ class PolymarketDataStore(DataStoreCollection):
796
1023
  self._create("trade", datastore_class=Trade)
797
1024
  self._create("price", datastore_class=Price)
798
1025
 
1026
+ def set_owner_key(self, api_key: str | None) -> None:
1027
+ self.position.set_owner_key(api_key)
1028
+
799
1029
  @property
800
1030
  def book(self) -> Book:
801
1031
  """Order Book DataStore
@@ -853,50 +1083,32 @@ class PolymarketDataStore(DataStoreCollection):
853
1083
  @property
854
1084
  def position(self) -> Position:
855
1085
  """
1086
+ User inventory snapshot keyed by ``asset`` (token id), updated from
1087
+ authenticated User WS ``trade`` events only.
856
1088
 
857
- .. code:: python
858
-
859
- [{
860
- # 🔑 基础信息
861
- "proxyWallet": "0x56687bf447db6ffa42ffe2204a05edaa20f55839", # 代理钱包地址(用于代表用户在链上的交易地址)
862
- "asset": "<string>", # outcome token 资产地址或 symbol
863
- "conditionId": "0xdd22472e552920b8438158ea7238bfadfa4f736aa4cee91a6b86c39ead110917", # 市场条件 ID(event 的唯一标识)
864
-
865
- # 💰 交易与价格信息
866
- "size": 123, # 当前持仓数量(仅在未平仓时存在)
867
- "avgPrice": 123, # 平均买入价(每个 outcome token 的均价)
868
- "curPrice": 123, # 当前市场价格
869
- "initialValue": 123, # 初始建仓总价值(avgPrice × size)
870
- "currentValue": 123, # 当前持仓市值(curPrice × size)
871
-
872
- # 📊 盈亏指标
873
- "cashPnl": 123, # 未实现盈亏(当前浮动盈亏)
874
- "percentPnl": 123, # 未实现盈亏百分比
875
- "realizedPnl": 123, # 已实现盈亏(平仓后的实际收益)
876
- "percentRealizedPnl": 123, # 已实现盈亏百分比(相对成本的收益率)
877
-
878
- # 🧮 累计交易信息
879
- "totalBought": 123, # 累计买入数量(含历史)
880
-
881
- # 新增字段
882
- "totalAvgPrice": 123, # 累计买入均价(含历史)
883
-
884
- # ⚙️ 状态标志
885
- "redeemable": True, # 是否可赎回(True 表示市场已结算且你是赢家,可提取 USDC)
886
- "mergeable": True, # 是否可合并(多笔相同 outcome 可合并为一笔)
887
- "negativeRisk": True, # 是否为负风险组合(风险对冲导致净敞口为负)
888
-
889
- # 🧠 市场元数据
890
- "title": "<string>", # 市场标题(如 “Bitcoin up or down 15m”)
891
- "slug": "<string>", # outcome 唯一 slug(对应前端页面路径的一部分)
892
- "eventSlug": "<string>", # event slug(整个预测事件的唯一路径标识)
893
- "icon": "<string>", # 图标 URL(一般为事件关联资产)
894
- "outcome": "<string>", # 当前持有的 outcome 名称(例如 “Yes” 或 “No”)
895
- "outcomeIndex": 123, # outcome 在该市场中的索引(0 或 1)
896
- "oppositeOutcome": "<string>",# 对立 outcome 名称
897
- "oppositeAsset": "<string>", # 对立 outcome token 地址
898
- "endDate": "<string>", # 市场结束时间(UTC ISO 格式字符串)
899
- }]
1089
+ Notes:
1090
+ - Data source: user WS trade events (MATCHED/MINED/CONFIRMED/FAILED).
1091
+ - Order stream and REST position sync do not drive inventory.
1092
+ - Maker fills are owner-filtered by api key; reconnect duplicates are deduped.
1093
+ - FAILED events reverse prior inventory impact.
1094
+
1095
+ .. code:: json
1096
+ {
1097
+ "asset": "15425820309554596073938947961054885346747932482195187699753650361958599405318", // token id
1098
+ "outcome": "Yes", // 当前asset对应的outcome名称
1099
+ "size": 5.0, // 当前asset仓位数量(对应 yes_qty no_qty)
1100
+ "avgPrice": 0.47, // 当前asset均价
1101
+ "totalAvgPrice": 0.47, // avgPrice 同步,兼容旧字段
1102
+ "market": "0x...", // conditionId / market id
1103
+ "yes_qty": 5.0, // YES腿数量
1104
+ "no_qty": 3.0, // NO腿数量
1105
+ "yes_avg_cost": 0.47, // YES腿VWAP成本
1106
+ "no_avg_cost": 0.49, // NO腿VWAP成本
1107
+ "net_diff": 2.0, // 净敞口 = yes_qty - no_qty
1108
+ "portfolio_cost": 0.96, // 组合成本 = yes_avg_cost + no_avg_cost(双腿都有仓时)
1109
+ "ts": 1772269014393, // 本地更新时间戳(ms)
1110
+ "source": "user_ws_trade" // 数据来源标记
1111
+ }
900
1112
  """
901
1113
 
902
1114
  return self._get("position")
@@ -948,7 +1160,7 @@ class PolymarketDataStore(DataStoreCollection):
948
1160
  }
949
1161
  """
950
1162
 
951
- return self._get("trade")
1163
+ return self._get("mytrade")
952
1164
 
953
1165
  @property
954
1166
  def price(self) -> Price:
@@ -1037,12 +1249,10 @@ class PolymarketDataStore(DataStoreCollection):
1037
1249
  self.book._on_message(m)
1038
1250
  elif msg_type == "order":
1039
1251
  self.orders._on_message(m)
1040
- self.position._on_order(m)
1041
1252
 
1042
1253
  elif msg_type == "trade":
1043
1254
  self.mytrade._on_message(m)
1044
- # self.fill._on_trade(m)
1045
- # self.position.on_trade(m)
1255
+ self.position.on_trade(m)
1046
1256
  elif msg_type == 'orders_matched':
1047
1257
  payload = m.get("payload") or {}
1048
1258
  if not payload:
@@ -1072,4 +1282,4 @@ class PolymarketDataStore(DataStoreCollection):
1072
1282
  continue
1073
1283
  msg_type = str(raw_type).lower()
1074
1284
  if msg_type == "last_trade_price":
1075
- self.trade._on_message(m)
1285
+ self.trade._on_message(m)
@@ -264,6 +264,9 @@ class Polymarket:
264
264
  self._forward_ws: dict[str, Any] | None = None
265
265
 
266
266
  self._ensure_session_entry(private_key=private_key, funder=funder, chain_id=chain_id)
267
+ creds = self._api_creds()
268
+ if creds:
269
+ self.store.set_owner_key(creds.get("api_key"))
267
270
 
268
271
  async def __aenter__(self) -> "Polymarket":
269
272
  if self.auth:
@@ -647,15 +650,8 @@ class Polymarket:
647
650
  if not creds:
648
651
  raise RuntimeError("Polymarket API credentials are required for personal subscriptions")
649
652
 
650
- # 记录 position store 最后更新时间
651
- last_position_update = time.time()
652
-
653
653
  def _handler(message, ws=None):
654
- nonlocal last_position_update
655
654
  self.store.onmessage(message, ws)
656
- # 检测是否是 position 相关消息
657
- if isinstance(message, dict) and message.get('event_type') in ('order', 'trade'):
658
- last_position_update = time.time()
659
655
  if callback:
660
656
  callback(message, ws)
661
657
 
@@ -664,30 +660,23 @@ class Polymarket:
664
660
  api_key = creds.get("api_key")
665
661
  api_secret = creds.get("api_secret")
666
662
  api_passphrase = creds.get("api_passphrase")
663
+ self.store.set_owner_key(api_key)
667
664
  if not api_key or not api_secret or not api_passphrase:
668
665
  raise RuntimeError("Polymarket API key/secret/passphrase missing; call create_or_derive_api_creds")
669
666
 
670
667
  auth = {"apiKey": api_key, "secret": api_secret, "passphrase": api_passphrase}
671
668
  payload = {"markets": list(markets or []), "type": "user", "auth": auth}
672
669
 
673
- # 在开始前用rest_api同步持仓
674
- await self.update('position')
670
+ # Position inventory is user-trade driven; ignore REST position sync.
671
+ _ = rest_position_sync_interval
675
672
 
676
- # 后台任务:3秒无更新则同步持仓
673
+ # 后台任务:定时同步 orders(best-effort)
677
674
  async def _rest_sync_watchdog():
678
- nonlocal last_position_update
679
675
  last_orders_update = time.time()
680
676
  while True:
681
677
  await asyncio.sleep(1)
682
678
  now = time.time()
683
- # position: 6秒无更新则同步
684
- if now - last_position_update > rest_position_sync_interval:
685
- try:
686
- await self.update('position')
687
- last_position_update = now
688
- except Exception:
689
- pass
690
- # orders: 每3秒同步一次
679
+ # orders: 定时同步一次
691
680
  if now - last_orders_update > rest_order_sync_interval:
692
681
  try:
693
682
  await self.update('orders')
@@ -2625,6 +2614,7 @@ class Polymarket:
2625
2614
  if not creds["api_key"] or not creds["api_secret"] or not creds["api_passphrase"]:
2626
2615
  raise RuntimeError("Polymarket API creds response missing key/secret/passphrase")
2627
2616
  session.__dict__["_polymarket_api_creds"] = creds
2617
+ self.store.set_owner_key(creds.get("api_key"))
2628
2618
  apis = session.__dict__.get("_apis")
2629
2619
  if isinstance(apis, dict):
2630
2620
  entry = list(apis.get(API_NAME, []))
@@ -694,7 +694,7 @@ wheels = [
694
694
 
695
695
  [[package]]
696
696
  name = "hyperquant"
697
- version = "1.54"
697
+ version = "1.55"
698
698
  source = { editable = "." }
699
699
  dependencies = [
700
700
  { name = "aiohttp" },
File without changes
File without changes