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.
- {hyperquant-1.55 → hyperquant-1.56}/.gitignore +5 -0
- {hyperquant-1.55 → hyperquant-1.56}/PKG-INFO +1 -1
- {hyperquant-1.55 → hyperquant-1.56}/pyproject.toml +1 -1
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/polymarket.py +396 -186
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/polymarket.py +9 -19
- {hyperquant-1.55 → hyperquant-1.56}/uv.lock +1 -1
- {hyperquant-1.55 → hyperquant-1.56}/README.md +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/requirements-dev.lock +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/requirements.lock +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/__init__.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/auth.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/bitget.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/bitmart.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/coinw.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/deepcoin.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/edgex.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/hyperliquid.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/lbank.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/lib/hpstore.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/lib/hyper_types.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/lib/polymarket/ctfAbi.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/lib/polymarket/safeAbi.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/lib/util.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/lighter.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/apexpro.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/bitget.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/bitmart.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/coinw.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/deepcoin.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/edgex.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/hyperliquid.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/lbank.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/lighter.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/models/ourbit.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/ourbit.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/broker/ws.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/core.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/datavison/_util.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/datavison/binance.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/datavison/coinglass.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/datavison/okx.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/db.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/draw.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/logkit.py +0 -0
- {hyperquant-1.55 → hyperquant-1.56}/src/hyperquant/notikit.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hyperquant
|
|
3
|
-
Version: 1.
|
|
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
|
|
@@ -14,186 +14,413 @@ if TYPE_CHECKING:
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class Position(DataStore):
|
|
17
|
-
"""Position
|
|
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
|
-
|
|
23
|
-
self.
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
"
|
|
111
|
-
"
|
|
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([
|
|
210
|
+
self._update([row])
|
|
116
211
|
else:
|
|
117
|
-
self._insert([
|
|
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
|
-
|
|
120
|
-
""
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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.
|
|
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
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
"
|
|
867
|
-
"
|
|
868
|
-
"
|
|
869
|
-
"
|
|
870
|
-
"
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
"
|
|
874
|
-
"
|
|
875
|
-
"
|
|
876
|
-
"
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
-
#
|
|
674
|
-
|
|
670
|
+
# Position inventory is user-trade driven; ignore REST position sync.
|
|
671
|
+
_ = rest_position_sync_interval
|
|
675
672
|
|
|
676
|
-
#
|
|
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
|
-
#
|
|
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, []))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|