hyperquant 1.36__tar.gz → 1.38__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.36 → hyperquant-1.38}/PKG-INFO +1 -1
- {hyperquant-1.36 → hyperquant-1.38}/pyproject.toml +1 -1
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/polymarket.py +44 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/polymarket.py +133 -13
- {hyperquant-1.36 → hyperquant-1.38}/uv.lock +1 -1
- {hyperquant-1.36 → hyperquant-1.38}/.gitignore +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/README.md +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/requirements-dev.lock +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/requirements.lock +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/__init__.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/auth.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/bitget.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/bitmart.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/coinw.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/deepcoin.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/edgex.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/hyperliquid.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/lbank.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/lib/hpstore.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/lib/hyper_types.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/lib/util.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/lighter.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/apexpro.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/bitget.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/bitmart.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/coinw.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/deepcoin.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/edgex.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/hyperliquid.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/lbank.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/lighter.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/ourbit.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/ourbit.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/ws.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/core.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/datavison/_util.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/datavison/binance.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/datavison/coinglass.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/datavison/okx.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/db.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/draw.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/logkit.py +0 -0
- {hyperquant-1.36 → hyperquant-1.38}/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.38
|
|
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
|
|
@@ -394,6 +394,36 @@ class _SideBook:
|
|
|
394
394
|
except (TypeError, ValueError):
|
|
395
395
|
return None
|
|
396
396
|
|
|
397
|
+
class Price(DataStore):
|
|
398
|
+
_KEYS = ["s"]
|
|
399
|
+
|
|
400
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
401
|
+
payload = msg.get('payload') or {}
|
|
402
|
+
data = payload.get('data') or {}
|
|
403
|
+
symbol = payload.get('symbol')
|
|
404
|
+
|
|
405
|
+
if not symbol:
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
_next = self.get({'s': symbol}) or {}
|
|
409
|
+
_next_price = _next.get('p')
|
|
410
|
+
last_price = None
|
|
411
|
+
|
|
412
|
+
if data and isinstance(data, list):
|
|
413
|
+
last_price = data[-1].get('value')
|
|
414
|
+
if 'value' in payload:
|
|
415
|
+
last_price = payload.get('value')
|
|
416
|
+
|
|
417
|
+
if last_price is None:
|
|
418
|
+
return
|
|
419
|
+
|
|
420
|
+
record = {'s': symbol, 'p': last_price}
|
|
421
|
+
key = {'s': symbol}
|
|
422
|
+
if self.get(key):
|
|
423
|
+
self._update([record])
|
|
424
|
+
else:
|
|
425
|
+
self._insert([record])
|
|
426
|
+
|
|
397
427
|
|
|
398
428
|
class BBO(DataStore):
|
|
399
429
|
_KEYS = ["s", "S"]
|
|
@@ -612,6 +642,7 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
612
642
|
self._create("mytrade", datastore_class=MyTrade)
|
|
613
643
|
self._create("fill", datastore_class=Fill)
|
|
614
644
|
self._create("trade", datastore_class=Trade)
|
|
645
|
+
self._create("price", datastore_class=Price)
|
|
615
646
|
|
|
616
647
|
@property
|
|
617
648
|
def book(self) -> Book:
|
|
@@ -762,6 +793,13 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
762
793
|
"""
|
|
763
794
|
|
|
764
795
|
return self._get("trade")
|
|
796
|
+
|
|
797
|
+
@property
|
|
798
|
+
def price(self) -> Price:
|
|
799
|
+
"""Price DataStore
|
|
800
|
+
_key: s
|
|
801
|
+
"""
|
|
802
|
+
return self._get("price")
|
|
765
803
|
|
|
766
804
|
@property
|
|
767
805
|
def fill(self) -> Fill:
|
|
@@ -837,6 +875,12 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
837
875
|
# 判定msg是否为list
|
|
838
876
|
lst_msg = msg if isinstance(msg, list) else [msg]
|
|
839
877
|
for m in lst_msg:
|
|
878
|
+
if m == '':
|
|
879
|
+
continue
|
|
880
|
+
topic = m.get("topic") or ""
|
|
881
|
+
if topic in {'crypto_prices_chainlink', 'crypto_prices'}:
|
|
882
|
+
self.price._on_message(m)
|
|
883
|
+
continue
|
|
840
884
|
raw_type = m.get("event_type") or m.get("type")
|
|
841
885
|
if not raw_type:
|
|
842
886
|
continue
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from contextlib import suppress
|
|
5
|
-
from datetime import UTC, datetime
|
|
5
|
+
from datetime import UTC, datetime, timedelta
|
|
6
6
|
from functools import lru_cache
|
|
7
7
|
import os
|
|
8
8
|
from typing import Any, Iterable, Iterator, Literal, Mapping, Sequence
|
|
@@ -12,6 +12,7 @@ import json
|
|
|
12
12
|
import aiohttp
|
|
13
13
|
import pybotters
|
|
14
14
|
import pybotters.ws
|
|
15
|
+
import pytz
|
|
15
16
|
|
|
16
17
|
from .models.polymarket import PolymarketDataStore
|
|
17
18
|
from .auth import Auth
|
|
@@ -22,6 +23,7 @@ GAMMA_EVENTS_API = "https://gamma-api.polymarket.com/events"
|
|
|
22
23
|
DEFAULT_DATA_ENDPOINT = "https://data-api.polymarket.com"
|
|
23
24
|
RTS_DATA_ENDPOINT = "wss://ws-live-data.polymarket.com/"
|
|
24
25
|
DEFAULT_BASE_SLUG = "btc-updown-15m"
|
|
26
|
+
HOURLY_BITCOIN_BASE_SLUG = "bitcoin-up-or-down"
|
|
25
27
|
DEFAULT_INTERVAL = 15 * 60
|
|
26
28
|
DEFAULT_WINDOW = 8
|
|
27
29
|
API_NAME = "polymarket"
|
|
@@ -38,6 +40,7 @@ DEFAULT_POLYGON_RPCS = (
|
|
|
38
40
|
"https://polygon-rpc.com",
|
|
39
41
|
"https://rpc.ankr.com/polygon",
|
|
40
42
|
)
|
|
43
|
+
_EASTERN_TZ = pytz.timezone("US/Eastern")
|
|
41
44
|
|
|
42
45
|
def parse_field(value):
|
|
43
46
|
"""尝试将字符串 JSON 转为对象,否则原样返回"""
|
|
@@ -73,6 +76,21 @@ def _accepting_orders(market: Mapping[str, Any]) -> bool:
|
|
|
73
76
|
return bool(accepting)
|
|
74
77
|
|
|
75
78
|
|
|
79
|
+
def _compose_hourly_slug(base_slug: str, *, now: datetime | None = None) -> str:
|
|
80
|
+
tz_now = now or datetime.now(_EASTERN_TZ)
|
|
81
|
+
if tz_now.tzinfo is None:
|
|
82
|
+
tz_now = _EASTERN_TZ.localize(tz_now)
|
|
83
|
+
else:
|
|
84
|
+
tz_now = tz_now.astimezone(_EASTERN_TZ)
|
|
85
|
+
|
|
86
|
+
tz_now = (tz_now + timedelta(seconds=5)).replace(minute=0, second=0, microsecond=0)
|
|
87
|
+
month_str = tz_now.strftime("%B").lower()
|
|
88
|
+
day = tz_now.day
|
|
89
|
+
hour_12 = tz_now.strftime("%I").lstrip("0") or "0"
|
|
90
|
+
am_pm = tz_now.strftime("%p").lower()
|
|
91
|
+
return f"{base_slug}-{month_str}-{day}-{hour_12}{am_pm}-et"
|
|
92
|
+
|
|
93
|
+
|
|
76
94
|
class Polymarket:
|
|
77
95
|
"""Polymarket CLOB client with REST helpers, stores and WS subscriptions."""
|
|
78
96
|
|
|
@@ -222,6 +240,89 @@ class Polymarket:
|
|
|
222
240
|
orders = results["orders"]
|
|
223
241
|
self.store.orders._on_response(orders)
|
|
224
242
|
|
|
243
|
+
async def sub_rts_prices(
|
|
244
|
+
self,
|
|
245
|
+
symbols: Sequence[str] | str | None = None,
|
|
246
|
+
*,
|
|
247
|
+
source: Literal["chainlink", "binance"] = "chainlink",
|
|
248
|
+
server_filter: bool = False,
|
|
249
|
+
) -> pybotters.ws.WebSocketApp:
|
|
250
|
+
"""Subscribe to Polymarket RTDS prices (Chainlink or Binance sources).
|
|
251
|
+
|
|
252
|
+
Parameters
|
|
253
|
+
----------
|
|
254
|
+
symbols
|
|
255
|
+
Requested symbols (Chainlink prefers ``eth/usd`` format, Binance
|
|
256
|
+
uses ``ethusdt``).
|
|
257
|
+
source
|
|
258
|
+
Either ``"chainlink"`` (default) or ``"binance"``.
|
|
259
|
+
server_filter
|
|
260
|
+
When ``True`` the request payload includes the filter exactly as the
|
|
261
|
+
docs specify (e.g. ``{"symbol":"btc/usd"}``). In practice the
|
|
262
|
+
server sometimes stops streaming after returning the first snapshot
|
|
263
|
+
when filters are present, so the default behaviour is to subscribe
|
|
264
|
+
to the full feed and filter locally.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
if isinstance(symbols, str):
|
|
268
|
+
requested = [symbols]
|
|
269
|
+
elif symbols:
|
|
270
|
+
requested = list(symbols)
|
|
271
|
+
else:
|
|
272
|
+
requested = []
|
|
273
|
+
|
|
274
|
+
target_symbols = {s.lower() for s in requested if s}
|
|
275
|
+
|
|
276
|
+
if source == "chainlink":
|
|
277
|
+
topic = "crypto_prices_chainlink"
|
|
278
|
+
sub_type = "*"
|
|
279
|
+
if server_filter and target_symbols:
|
|
280
|
+
if len(target_symbols) == 1:
|
|
281
|
+
filters = json.dumps({"symbol": next(iter(target_symbols))})
|
|
282
|
+
else:
|
|
283
|
+
filters = json.dumps({"symbols": sorted(target_symbols)})
|
|
284
|
+
else:
|
|
285
|
+
filters = None
|
|
286
|
+
else:
|
|
287
|
+
topic = "crypto_prices"
|
|
288
|
+
sub_type = "update"
|
|
289
|
+
filters = None
|
|
290
|
+
if server_filter and target_symbols:
|
|
291
|
+
filters = ",".join(sorted(target_symbols))
|
|
292
|
+
|
|
293
|
+
subscription: dict[str, Any] = {"topic": topic, "type": sub_type}
|
|
294
|
+
if filters:
|
|
295
|
+
subscription["filters"] = filters
|
|
296
|
+
|
|
297
|
+
payload = {
|
|
298
|
+
"action": "subscribe",
|
|
299
|
+
"subscriptions": [subscription],
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
def callback(msg, ws):
|
|
303
|
+
if not msg:
|
|
304
|
+
return
|
|
305
|
+
try:
|
|
306
|
+
data = json.loads(msg)
|
|
307
|
+
except json.JSONDecodeError:
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
payload = data.get("payload") or {}
|
|
311
|
+
symbol = str(payload.get("symbol") or "").lower()
|
|
312
|
+
if (not server_filter) and target_symbols and symbol and symbol not in target_symbols:
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
self.store.onmessage(data, ws)
|
|
316
|
+
|
|
317
|
+
wsapp = self.client.ws_connect(
|
|
318
|
+
RTS_DATA_ENDPOINT,
|
|
319
|
+
hdlr_str=callback,
|
|
320
|
+
heartbeat=5,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
await wsapp._event.wait()
|
|
324
|
+
await wsapp.current_ws.send_json(payload)
|
|
325
|
+
return wsapp
|
|
225
326
|
|
|
226
327
|
|
|
227
328
|
async def sub_books(
|
|
@@ -899,6 +1000,7 @@ class Polymarket:
|
|
|
899
1000
|
if isinstance(fee_resp, dict):
|
|
900
1001
|
return int(fee_resp.get("base_fee", 0))
|
|
901
1002
|
return int(fee_resp or 0)
|
|
1003
|
+
|
|
902
1004
|
|
|
903
1005
|
async def _signed_request_via_session(
|
|
904
1006
|
self, method: str, path: str, body: Mapping[str, Any] | list[Any] | None
|
|
@@ -1171,6 +1273,33 @@ class Polymarket:
|
|
|
1171
1273
|
https://docs.polymarket.com/api-reference/markets/get-market-by-id
|
|
1172
1274
|
"""
|
|
1173
1275
|
|
|
1276
|
+
async def _try_slug(slug: str | None) -> tuple[str, dict, dict] | None:
|
|
1277
|
+
if not slug:
|
|
1278
|
+
return None
|
|
1279
|
+
event = await self._fetch_event(slug)
|
|
1280
|
+
if not event:
|
|
1281
|
+
return None
|
|
1282
|
+
|
|
1283
|
+
event = {k: parse_field(v) for k, v in event.items()}
|
|
1284
|
+
for market in event.get("markets", []):
|
|
1285
|
+
if not _accepting_orders(market):
|
|
1286
|
+
continue
|
|
1287
|
+
market = {k: parse_field(v) for k, v in market.items()}
|
|
1288
|
+
return slug, event, market
|
|
1289
|
+
return None
|
|
1290
|
+
|
|
1291
|
+
if base_slug == HOURLY_BITCOIN_BASE_SLUG:
|
|
1292
|
+
hourly_slug = _compose_hourly_slug(base_slug)
|
|
1293
|
+
hourly_match = await _try_slug(hourly_slug)
|
|
1294
|
+
if hourly_match:
|
|
1295
|
+
return hourly_match
|
|
1296
|
+
|
|
1297
|
+
# 1小时市场等特殊 slug(比如 bitcoin-up-or-down-november-18-10am-et)
|
|
1298
|
+
# 直接传入完整 slug 即可,不再拼接时间戳
|
|
1299
|
+
direct_match = await _try_slug(base_slug)
|
|
1300
|
+
if direct_match:
|
|
1301
|
+
return direct_match
|
|
1302
|
+
|
|
1174
1303
|
now_ts = int(datetime.now(UTC).timestamp())
|
|
1175
1304
|
base_ts = (now_ts // interval) * interval
|
|
1176
1305
|
|
|
@@ -1179,18 +1308,9 @@ class Polymarket:
|
|
|
1179
1308
|
if ts < 0:
|
|
1180
1309
|
continue
|
|
1181
1310
|
slug = f"{base_slug}-{ts}"
|
|
1182
|
-
|
|
1183
|
-
if
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
event = {k: parse_field(v) for k, v in event.items()}
|
|
1189
|
-
|
|
1190
|
-
for market in event.get("markets", []):
|
|
1191
|
-
if _accepting_orders(market):
|
|
1192
|
-
market = {k: parse_field(v) for k, v in market.items()}
|
|
1193
|
-
return slug, event, market
|
|
1311
|
+
result = await _try_slug(slug)
|
|
1312
|
+
if result:
|
|
1313
|
+
return result
|
|
1194
1314
|
|
|
1195
1315
|
raise RuntimeError(
|
|
1196
1316
|
f"未在 {base_slug} 的 +/-{window} 个区间内找到可交易的市场"
|
|
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
|