hyperquant 1.53__tar.gz → 1.55__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.53 → hyperquant-1.55}/PKG-INFO +1 -1
- {hyperquant-1.53 → hyperquant-1.55}/pyproject.toml +1 -1
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/polymarket.py +401 -73
- {hyperquant-1.53 → hyperquant-1.55}/uv.lock +1 -1
- {hyperquant-1.53 → hyperquant-1.55}/.gitignore +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/README.md +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/requirements-dev.lock +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/requirements.lock +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/__init__.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/auth.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/bitget.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/bitmart.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/coinw.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/deepcoin.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/edgex.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/hyperliquid.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/lbank.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/lib/hpstore.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/lib/hyper_types.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/lib/polymarket/ctfAbi.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/lib/polymarket/safeAbi.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/lib/util.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/lighter.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/apexpro.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/bitget.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/bitmart.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/coinw.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/deepcoin.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/edgex.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/hyperliquid.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/lbank.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/lighter.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/ourbit.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/polymarket.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/ourbit.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/ws.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/core.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/datavison/_util.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/datavison/binance.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/datavison/coinglass.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/datavison/okx.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/db.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/draw.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/logkit.py +0 -0
- {hyperquant-1.53 → hyperquant-1.55}/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.55
|
|
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
|
|
@@ -7,7 +7,8 @@ from datetime import UTC, datetime, timedelta
|
|
|
7
7
|
from functools import lru_cache
|
|
8
8
|
import os
|
|
9
9
|
import time
|
|
10
|
-
from typing import Any, Iterable, Iterator, Literal, Mapping, Sequence
|
|
10
|
+
from typing import Any, Awaitable, Callable, Iterable, Iterator, Literal, Mapping, Sequence
|
|
11
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
11
12
|
|
|
12
13
|
import json
|
|
13
14
|
|
|
@@ -108,7 +109,10 @@ CONDITIONAL_TOKENS_SPLIT_ABI = [
|
|
|
108
109
|
},
|
|
109
110
|
]
|
|
110
111
|
DEFAULT_POLYGON_RPCS = (
|
|
111
|
-
|
|
112
|
+
"https://1rpc.io/matic",
|
|
113
|
+
"https://polygon-bor-rpc.publicnode.com",
|
|
114
|
+
"https://polygon.drpc.org",
|
|
115
|
+
# Keep historical endpoints as fallback for users with API access.
|
|
112
116
|
"https://polygon-rpc.com",
|
|
113
117
|
"https://rpc.ankr.com/polygon",
|
|
114
118
|
)
|
|
@@ -256,6 +260,8 @@ class Polymarket:
|
|
|
256
260
|
self._ws_personal: pybotters.ws.WebSocketApp | None = None
|
|
257
261
|
self.auth = False
|
|
258
262
|
self._tick_size_cache: dict[str, tuple[str, float]] = {}
|
|
263
|
+
self._forward_orders_cfg: dict[str, Any] | None = None
|
|
264
|
+
self._forward_ws: dict[str, Any] | None = None
|
|
259
265
|
|
|
260
266
|
self._ensure_session_entry(private_key=private_key, funder=funder, chain_id=chain_id)
|
|
261
267
|
|
|
@@ -273,6 +279,7 @@ class Polymarket:
|
|
|
273
279
|
await self._ws_public.current_ws.close()
|
|
274
280
|
self._ws_public = None
|
|
275
281
|
self._ws_public_ready.clear()
|
|
282
|
+
await self._close_forward_ws()
|
|
276
283
|
|
|
277
284
|
async def prewarm_connections(
|
|
278
285
|
self,
|
|
@@ -293,6 +300,109 @@ class Polymarket:
|
|
|
293
300
|
except Exception:
|
|
294
301
|
continue
|
|
295
302
|
|
|
303
|
+
async def mount_forward_orders(
|
|
304
|
+
self,
|
|
305
|
+
*,
|
|
306
|
+
mode: Literal["rest", "ws"],
|
|
307
|
+
url: str,
|
|
308
|
+
region: str = "afr",
|
|
309
|
+
session: str = "orders",
|
|
310
|
+
ws_timeout: float = 30.0,
|
|
311
|
+
batch_mode: Literal["step", "batch"] = "step",
|
|
312
|
+
concurrency: int = 5,
|
|
313
|
+
callback: Callable[[dict[str, Any]], Awaitable[None] | None] | None = None,
|
|
314
|
+
) -> None:
|
|
315
|
+
"""Mount forward execution for place_orders/cancel_orders."""
|
|
316
|
+
cfg = {
|
|
317
|
+
"mode": mode,
|
|
318
|
+
"url": url,
|
|
319
|
+
"region": region,
|
|
320
|
+
"session": session,
|
|
321
|
+
"ws_timeout": float(ws_timeout),
|
|
322
|
+
"batch_mode": batch_mode,
|
|
323
|
+
"concurrency": int(concurrency),
|
|
324
|
+
"callback": callback,
|
|
325
|
+
}
|
|
326
|
+
if mode == "rest":
|
|
327
|
+
cfg["url"] = self._normalize_forward_batch_url(url)
|
|
328
|
+
await self._close_forward_ws()
|
|
329
|
+
else:
|
|
330
|
+
cfg["url"] = self._normalize_forward_ws_url(url)
|
|
331
|
+
await self._ensure_forward_ws_connected(cfg)
|
|
332
|
+
self._forward_orders_cfg = cfg
|
|
333
|
+
|
|
334
|
+
async def unmount_forward_orders(self) -> None:
|
|
335
|
+
"""Disable forward execution and fallback to direct CLOB calls."""
|
|
336
|
+
await self._close_forward_ws()
|
|
337
|
+
self._forward_orders_cfg = None
|
|
338
|
+
|
|
339
|
+
async def _close_forward_ws(self) -> None:
|
|
340
|
+
state = self._forward_ws
|
|
341
|
+
self._forward_ws = None
|
|
342
|
+
if not state:
|
|
343
|
+
return
|
|
344
|
+
task: asyncio.Task | None = state.get("listener")
|
|
345
|
+
ws: aiohttp.ClientWebSocketResponse | None = state.get("ws")
|
|
346
|
+
if task:
|
|
347
|
+
task.cancel()
|
|
348
|
+
try:
|
|
349
|
+
await task
|
|
350
|
+
except asyncio.CancelledError:
|
|
351
|
+
pass
|
|
352
|
+
except Exception:
|
|
353
|
+
pass
|
|
354
|
+
if ws and not ws.closed:
|
|
355
|
+
with suppress(Exception):
|
|
356
|
+
await ws.close()
|
|
357
|
+
|
|
358
|
+
async def _forward_ws_listener(self, ws: aiohttp.ClientWebSocketResponse, cfg: Mapping[str, Any]) -> None:
|
|
359
|
+
callback = cfg.get("callback")
|
|
360
|
+
while True:
|
|
361
|
+
msg = await ws.receive()
|
|
362
|
+
if msg.type == aiohttp.WSMsgType.TEXT:
|
|
363
|
+
try:
|
|
364
|
+
data = json.loads(msg.data)
|
|
365
|
+
except Exception:
|
|
366
|
+
continue
|
|
367
|
+
if callback:
|
|
368
|
+
try:
|
|
369
|
+
ret = callback(data)
|
|
370
|
+
if asyncio.iscoroutine(ret):
|
|
371
|
+
await ret
|
|
372
|
+
except Exception as exc:
|
|
373
|
+
self.logger.warning("forward ws callback error: %s", exc)
|
|
374
|
+
continue
|
|
375
|
+
if msg.type in {aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED}:
|
|
376
|
+
break
|
|
377
|
+
if msg.type == aiohttp.WSMsgType.ERROR:
|
|
378
|
+
break
|
|
379
|
+
|
|
380
|
+
async def _ensure_forward_ws_connected(self, cfg: Mapping[str, Any] | None = None) -> None:
|
|
381
|
+
current = cfg or self._forward_orders_cfg
|
|
382
|
+
if not current or current.get("mode") != "ws":
|
|
383
|
+
return
|
|
384
|
+
state = self._forward_ws
|
|
385
|
+
if state:
|
|
386
|
+
ws: aiohttp.ClientWebSocketResponse | None = state.get("ws")
|
|
387
|
+
task: asyncio.Task | None = state.get("listener")
|
|
388
|
+
if ws and not ws.closed and task and not task.done():
|
|
389
|
+
return
|
|
390
|
+
await self._close_forward_ws()
|
|
391
|
+
|
|
392
|
+
session: aiohttp.ClientSession = getattr(self.client, "_session", None)
|
|
393
|
+
if session is None:
|
|
394
|
+
raise RuntimeError("pybotters client session missing")
|
|
395
|
+
|
|
396
|
+
ws_url = current["url"]
|
|
397
|
+
sep = "&" if ("?" in ws_url) else "?"
|
|
398
|
+
ws_url = f"{ws_url}{sep}region={current.get('region', 'afr')}&session={current.get('session', 'orders')}"
|
|
399
|
+
ws = await session.ws_connect(ws_url, heartbeat=30)
|
|
400
|
+
# Consume hello once to align message stream.
|
|
401
|
+
with suppress(Exception):
|
|
402
|
+
await asyncio.wait_for(ws.receive(), timeout=float(current.get("ws_timeout", 30.0)))
|
|
403
|
+
listener = asyncio.create_task(self._forward_ws_listener(ws, current))
|
|
404
|
+
self._forward_ws = {"ws": ws, "listener": listener}
|
|
405
|
+
|
|
296
406
|
# ------------------------------------------------------------------
|
|
297
407
|
# Store helpers
|
|
298
408
|
|
|
@@ -1355,6 +1465,101 @@ class Polymarket:
|
|
|
1355
1465
|
except Exception:
|
|
1356
1466
|
return await resp.text()
|
|
1357
1467
|
|
|
1468
|
+
def _normalize_forward_batch_url(self, forward_url: str) -> str:
|
|
1469
|
+
parts = urlsplit(forward_url)
|
|
1470
|
+
if not parts.scheme or not parts.netloc:
|
|
1471
|
+
raise ValueError("forward_url must be a valid absolute URL")
|
|
1472
|
+
path = (parts.path or "").rstrip("/")
|
|
1473
|
+
if path.endswith("/forward-batch") or path.endswith("/forward-batch-step"):
|
|
1474
|
+
target_path = path
|
|
1475
|
+
elif path.endswith("/forward"):
|
|
1476
|
+
target_path = f"{path[:-len('/forward')]}/forward-batch-step"
|
|
1477
|
+
else:
|
|
1478
|
+
target_path = f"{path}/forward-batch-step" if path else "/forward-batch-step"
|
|
1479
|
+
return urlunsplit((parts.scheme, parts.netloc, target_path, parts.query, parts.fragment))
|
|
1480
|
+
|
|
1481
|
+
def _normalize_forward_ws_url(self, forward_url: str) -> str:
|
|
1482
|
+
parts = urlsplit(forward_url)
|
|
1483
|
+
if parts.scheme not in {"ws", "wss"} or not parts.netloc:
|
|
1484
|
+
raise ValueError("forward ws url must be a valid ws:// or wss:// URL")
|
|
1485
|
+
path = (parts.path or "").rstrip("/")
|
|
1486
|
+
if not path.endswith("/ws-forward"):
|
|
1487
|
+
path = f"{path}/ws-forward" if path else "/ws-forward"
|
|
1488
|
+
return urlunsplit((parts.scheme, parts.netloc, path, parts.query, parts.fragment))
|
|
1489
|
+
|
|
1490
|
+
def _build_l2_signed_headers(
|
|
1491
|
+
self,
|
|
1492
|
+
*,
|
|
1493
|
+
signer_addr: str,
|
|
1494
|
+
api_key: str,
|
|
1495
|
+
api_secret: str,
|
|
1496
|
+
api_passphrase: str,
|
|
1497
|
+
method: str,
|
|
1498
|
+
path: str,
|
|
1499
|
+
payload: Any,
|
|
1500
|
+
) -> dict[str, str]:
|
|
1501
|
+
import base64
|
|
1502
|
+
import hashlib
|
|
1503
|
+
import hmac
|
|
1504
|
+
|
|
1505
|
+
ts = int(time.time())
|
|
1506
|
+
serialized = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
|
|
1507
|
+
msg = f"{ts}{method.upper()}{path}{serialized}"
|
|
1508
|
+
secret_bytes = base64.urlsafe_b64decode(api_secret)
|
|
1509
|
+
sig = hmac.new(secret_bytes, msg.encode("utf-8"), hashlib.sha256).digest()
|
|
1510
|
+
sign_b64 = base64.urlsafe_b64encode(sig).decode("utf-8")
|
|
1511
|
+
return {
|
|
1512
|
+
"POLY_ADDRESS": signer_addr,
|
|
1513
|
+
"POLY_SIGNATURE": sign_b64,
|
|
1514
|
+
"POLY_TIMESTAMP": str(ts),
|
|
1515
|
+
"POLY_API_KEY": api_key,
|
|
1516
|
+
"POLY_PASSPHRASE": api_passphrase,
|
|
1517
|
+
"Content-Type": "application/json",
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
async def _submit_forward_requests(self, requests_payload: Sequence[Mapping[str, Any]]) -> Any:
|
|
1521
|
+
cfg = self._forward_orders_cfg
|
|
1522
|
+
if not cfg:
|
|
1523
|
+
raise RuntimeError("forward is not mounted")
|
|
1524
|
+
session: aiohttp.ClientSession = getattr(self.client, "_session", None)
|
|
1525
|
+
if session is None:
|
|
1526
|
+
raise RuntimeError("pybotters client session missing")
|
|
1527
|
+
|
|
1528
|
+
if cfg["mode"] == "rest":
|
|
1529
|
+
endpoint = cfg["url"]
|
|
1530
|
+
async with session.post(endpoint, json={"requests": list(requests_payload)}) as resp:
|
|
1531
|
+
if resp.status >= 400:
|
|
1532
|
+
text = await resp.text()
|
|
1533
|
+
raise RuntimeError(f"Forward batch failed: {resp.status} {text}")
|
|
1534
|
+
try:
|
|
1535
|
+
return await resp.json()
|
|
1536
|
+
except Exception:
|
|
1537
|
+
return await resp.text()
|
|
1538
|
+
|
|
1539
|
+
await self._ensure_forward_ws_connected(cfg)
|
|
1540
|
+
state = self._forward_ws
|
|
1541
|
+
ws: aiohttp.ClientWebSocketResponse | None = state.get("ws") if state else None
|
|
1542
|
+
if ws is None or ws.closed:
|
|
1543
|
+
raise RuntimeError("forward ws not connected")
|
|
1544
|
+
|
|
1545
|
+
req_id = f"hq-{int(time.time() * 1000)}"
|
|
1546
|
+
msg_type = "batch" if cfg.get("batch_mode") == "batch" else "batch-step"
|
|
1547
|
+
payload: dict[str, Any] = {
|
|
1548
|
+
"type": msg_type,
|
|
1549
|
+
"id": req_id,
|
|
1550
|
+
"requests": list(requests_payload),
|
|
1551
|
+
}
|
|
1552
|
+
if msg_type == "batch":
|
|
1553
|
+
payload["concurrency"] = int(cfg.get("concurrency", 5))
|
|
1554
|
+
await ws.send_json(payload)
|
|
1555
|
+
return {
|
|
1556
|
+
"mode": "ws",
|
|
1557
|
+
"queued": True,
|
|
1558
|
+
"request_id": req_id,
|
|
1559
|
+
"batch_mode": msg_type,
|
|
1560
|
+
"count": len(requests_payload),
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1358
1563
|
async def post_orders(
|
|
1359
1564
|
self,
|
|
1360
1565
|
orders: Iterable[tuple[Mapping[str, Any], str]],
|
|
@@ -1444,6 +1649,41 @@ class Polymarket:
|
|
|
1444
1649
|
}
|
|
1445
1650
|
)
|
|
1446
1651
|
|
|
1652
|
+
if self._forward_orders_cfg:
|
|
1653
|
+
session: aiohttp.ClientSession = getattr(self.client, "_session", None)
|
|
1654
|
+
if session is None:
|
|
1655
|
+
raise RuntimeError("pybotters client session missing")
|
|
1656
|
+
creds = getattr(session, "_polymarket_api_creds", None)
|
|
1657
|
+
if not creds:
|
|
1658
|
+
raise RuntimeError("Polymarket API creds missing; call create_or_derive_api_creds")
|
|
1659
|
+
api_key = creds.get("api_key")
|
|
1660
|
+
api_secret = creds.get("api_secret")
|
|
1661
|
+
api_passphrase = creds.get("api_passphrase")
|
|
1662
|
+
if not (api_key and api_secret and api_passphrase):
|
|
1663
|
+
raise RuntimeError("Polymarket API creds incomplete")
|
|
1664
|
+
|
|
1665
|
+
target_url = f"{self.rest_api.rstrip('/')}/order"
|
|
1666
|
+
requests_payload: list[dict[str, Any]] = []
|
|
1667
|
+
for payload in result_body:
|
|
1668
|
+
headers = self._build_l2_signed_headers(
|
|
1669
|
+
signer_addr=signer_addr,
|
|
1670
|
+
api_key=api_key,
|
|
1671
|
+
api_secret=api_secret,
|
|
1672
|
+
api_passphrase=api_passphrase,
|
|
1673
|
+
method="POST",
|
|
1674
|
+
path="/order",
|
|
1675
|
+
payload=payload,
|
|
1676
|
+
)
|
|
1677
|
+
requests_payload.append(
|
|
1678
|
+
{
|
|
1679
|
+
"method": "POST",
|
|
1680
|
+
"url": target_url,
|
|
1681
|
+
"headers": headers,
|
|
1682
|
+
"body": payload,
|
|
1683
|
+
}
|
|
1684
|
+
)
|
|
1685
|
+
return await self._submit_forward_requests(requests_payload)
|
|
1686
|
+
|
|
1447
1687
|
return await self._signed_request_via_session("POST", "/orders", result_body)
|
|
1448
1688
|
|
|
1449
1689
|
async def cancel(self, order_id: str) -> Any:
|
|
@@ -1452,11 +1692,48 @@ class Polymarket:
|
|
|
1452
1692
|
"""
|
|
1453
1693
|
return await self._signed_request_via_session("DELETE", "/order", {"orderID": order_id})
|
|
1454
1694
|
|
|
1455
|
-
async def cancel_orders(
|
|
1695
|
+
async def cancel_orders(
|
|
1696
|
+
self,
|
|
1697
|
+
order_ids: Sequence[str],
|
|
1698
|
+
) -> Any:
|
|
1456
1699
|
"""
|
|
1457
1700
|
{'not_canceled': {}, 'canceled': ['0xb3507e9fda9541c3e038afcb4f24b96efcfa667d46cf5e9e52c41620711818df', '0x4b9c3ee4dee8653f15f716653e8ac83f0a086a38597e6ec4b72be2389c79b8b4']}
|
|
1458
1701
|
"""
|
|
1459
|
-
|
|
1702
|
+
payload = list(order_ids)
|
|
1703
|
+
if self._forward_orders_cfg:
|
|
1704
|
+
session: aiohttp.ClientSession = getattr(self.client, "_session", None)
|
|
1705
|
+
if session is None:
|
|
1706
|
+
raise RuntimeError("pybotters client session missing")
|
|
1707
|
+
creds = getattr(session, "_polymarket_api_creds", None)
|
|
1708
|
+
if not creds:
|
|
1709
|
+
raise RuntimeError("Polymarket API creds missing; call create_or_derive_api_creds")
|
|
1710
|
+
api_key = creds.get("api_key")
|
|
1711
|
+
api_secret = creds.get("api_secret")
|
|
1712
|
+
api_passphrase = creds.get("api_passphrase")
|
|
1713
|
+
if not (api_key and api_secret and api_passphrase):
|
|
1714
|
+
raise RuntimeError("Polymarket API creds incomplete")
|
|
1715
|
+
|
|
1716
|
+
_, _, signer_addr = self._get_signing_context()
|
|
1717
|
+
headers = self._build_l2_signed_headers(
|
|
1718
|
+
signer_addr=signer_addr,
|
|
1719
|
+
api_key=api_key,
|
|
1720
|
+
api_secret=api_secret,
|
|
1721
|
+
api_passphrase=api_passphrase,
|
|
1722
|
+
method="DELETE",
|
|
1723
|
+
path="/orders",
|
|
1724
|
+
payload=payload,
|
|
1725
|
+
)
|
|
1726
|
+
requests_payload = [
|
|
1727
|
+
{
|
|
1728
|
+
"method": "DELETE",
|
|
1729
|
+
"url": f"{self.rest_api.rstrip('/')}/orders",
|
|
1730
|
+
"headers": headers,
|
|
1731
|
+
"body": payload,
|
|
1732
|
+
}
|
|
1733
|
+
]
|
|
1734
|
+
return await self._submit_forward_requests(requests_payload)
|
|
1735
|
+
|
|
1736
|
+
return await self._signed_request_via_session("DELETE", "/orders", payload)
|
|
1460
1737
|
|
|
1461
1738
|
async def cancel_all(self) -> Any:
|
|
1462
1739
|
"""
|
|
@@ -1846,14 +2123,22 @@ class Polymarket:
|
|
|
1846
2123
|
if dry_run:
|
|
1847
2124
|
results.append({**item, "tx": None, "dry_run": True})
|
|
1848
2125
|
continue
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
2126
|
+
try:
|
|
2127
|
+
tx_hash = await asyncio.to_thread(
|
|
2128
|
+
self._claim_via_safe_sync,
|
|
2129
|
+
candidates,
|
|
2130
|
+
item["condition_id"],
|
|
2131
|
+
item["index_sets"],
|
|
2132
|
+
gas,
|
|
2133
|
+
verbose,
|
|
2134
|
+
)
|
|
2135
|
+
except Exception as exc:
|
|
2136
|
+
raise RuntimeError(
|
|
2137
|
+
"claim_positions failed for "
|
|
2138
|
+
f"condition_id={item.get('condition_id')} "
|
|
2139
|
+
f"index_sets={item.get('index_sets')} "
|
|
2140
|
+
f"size={item.get('size')}: {exc}"
|
|
2141
|
+
) from exc
|
|
1857
2142
|
result_row = {**item, "tx": tx_hash, "dry_run": False}
|
|
1858
2143
|
if include_receipt:
|
|
1859
2144
|
try:
|
|
@@ -1976,22 +2261,7 @@ class Polymarket:
|
|
|
1976
2261
|
last_error: Exception | None = None
|
|
1977
2262
|
w3 = None
|
|
1978
2263
|
rpc_used = None
|
|
1979
|
-
|
|
1980
|
-
try:
|
|
1981
|
-
w3 = Web3(Web3.HTTPProvider(url, request_kwargs={"timeout": 30}))
|
|
1982
|
-
with suppress(Exception):
|
|
1983
|
-
from web3.middleware import geth_poa_middleware, ExtraDataToPOAMiddleware
|
|
1984
|
-
try:
|
|
1985
|
-
w3.middleware_onion.inject(geth_poa_middleware, layer=0)
|
|
1986
|
-
except Exception:
|
|
1987
|
-
w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
|
|
1988
|
-
rpc_used = url
|
|
1989
|
-
break
|
|
1990
|
-
except Exception as exc:
|
|
1991
|
-
last_error = exc
|
|
1992
|
-
continue
|
|
1993
|
-
if w3 is None:
|
|
1994
|
-
raise RuntimeError(f"All RPC endpoints failed: {candidates}") from last_error
|
|
2264
|
+
safe_nonce = None
|
|
1995
2265
|
|
|
1996
2266
|
pk_env = os.getenv("PK")
|
|
1997
2267
|
if pk_env:
|
|
@@ -2004,23 +2274,11 @@ class Polymarket:
|
|
|
2004
2274
|
raise RuntimeError("Safe/proxy wallet address未知, 请在 apis['polymarket'][2] 或构造函数 funder 设置")
|
|
2005
2275
|
|
|
2006
2276
|
ctf_addr = self._contracts(self.chain_id, False)["conditional_tokens"]
|
|
2007
|
-
ctf = w3.eth.contract(address=w3.to_checksum_address(ctf_addr), abi=CONDITIONAL_TOKENS_ABI)
|
|
2008
|
-
safe = w3.eth.contract(address=w3.to_checksum_address(safe_addr), abi=SAFE_ABI)
|
|
2009
2277
|
|
|
2010
2278
|
cond_bytes = bytes.fromhex(condition_id.replace("0x", ""))
|
|
2011
2279
|
if len(cond_bytes) != 32:
|
|
2012
2280
|
raise ValueError("condition_id must be 32-byte hex string")
|
|
2013
2281
|
|
|
2014
|
-
redeem_calldata = ctf.encode_abi(
|
|
2015
|
-
"redeemPositions",
|
|
2016
|
-
args=[
|
|
2017
|
-
w3.to_checksum_address(USDC_CONTRACT),
|
|
2018
|
-
bytes(32),
|
|
2019
|
-
cond_bytes,
|
|
2020
|
-
index_sets,
|
|
2021
|
-
],
|
|
2022
|
-
)
|
|
2023
|
-
|
|
2024
2282
|
safe_tx_gas = 0
|
|
2025
2283
|
base_gas = 0
|
|
2026
2284
|
gas_price = 0
|
|
@@ -2029,13 +2287,55 @@ class Polymarket:
|
|
|
2029
2287
|
value = 0
|
|
2030
2288
|
operation = 0 # CALL
|
|
2031
2289
|
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2290
|
+
ctf = None
|
|
2291
|
+
safe = None
|
|
2292
|
+
redeem_calldata = None
|
|
2293
|
+
for url in candidates:
|
|
2294
|
+
try:
|
|
2295
|
+
w3_candidate = Web3(Web3.HTTPProvider(url, request_kwargs={"timeout": 30}))
|
|
2296
|
+
with suppress(Exception):
|
|
2297
|
+
from web3.middleware import geth_poa_middleware, ExtraDataToPOAMiddleware
|
|
2298
|
+
try:
|
|
2299
|
+
w3_candidate.middleware_onion.inject(geth_poa_middleware, layer=0)
|
|
2300
|
+
except Exception:
|
|
2301
|
+
w3_candidate.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
|
|
2302
|
+
|
|
2303
|
+
ctf_candidate = w3_candidate.eth.contract(
|
|
2304
|
+
address=w3_candidate.to_checksum_address(ctf_addr),
|
|
2305
|
+
abi=CONDITIONAL_TOKENS_ABI,
|
|
2306
|
+
)
|
|
2307
|
+
safe_candidate = w3_candidate.eth.contract(
|
|
2308
|
+
address=w3_candidate.to_checksum_address(safe_addr),
|
|
2309
|
+
abi=SAFE_ABI,
|
|
2310
|
+
)
|
|
2311
|
+
redeem_calldata_candidate = ctf_candidate.encode_abi(
|
|
2312
|
+
"redeemPositions",
|
|
2313
|
+
args=[
|
|
2314
|
+
w3_candidate.to_checksum_address(USDC_CONTRACT),
|
|
2315
|
+
bytes(32),
|
|
2316
|
+
cond_bytes,
|
|
2317
|
+
index_sets,
|
|
2318
|
+
],
|
|
2319
|
+
)
|
|
2320
|
+
nonce_candidate = safe_candidate.functions.nonce().call()
|
|
2321
|
+
|
|
2322
|
+
w3 = w3_candidate
|
|
2323
|
+
rpc_used = url
|
|
2324
|
+
ctf = ctf_candidate
|
|
2325
|
+
safe = safe_candidate
|
|
2326
|
+
redeem_calldata = redeem_calldata_candidate
|
|
2327
|
+
safe_nonce = nonce_candidate
|
|
2328
|
+
break
|
|
2329
|
+
except Exception as exc:
|
|
2330
|
+
last_error = exc
|
|
2331
|
+
if verbose:
|
|
2332
|
+
print(f"[claim] rpc nonce probe failed: {url} -> {exc}")
|
|
2333
|
+
continue
|
|
2334
|
+
|
|
2335
|
+
if w3 is None or safe is None or ctf is None or redeem_calldata is None or safe_nonce is None:
|
|
2336
|
+
raise RuntimeError(
|
|
2337
|
+
f"无法获取 Safe nonce (tried rpc={candidates}); last_error={last_error}"
|
|
2338
|
+
) from last_error
|
|
2039
2339
|
|
|
2040
2340
|
try:
|
|
2041
2341
|
safe_tx_hash = safe.functions.getTransactionHash(
|
|
@@ -2065,43 +2365,71 @@ class Polymarket:
|
|
|
2065
2365
|
try:
|
|
2066
2366
|
acct = _A.from_key(private_key)
|
|
2067
2367
|
sender = acct.address
|
|
2068
|
-
# 使用 pending 避免重复使用已在 mempool 的 nonce 触发 replacement underpriced
|
|
2069
|
-
nonce = w3.eth.get_transaction_count(sender)
|
|
2070
|
-
gas_price_chain = w3.eth.gas_price
|
|
2071
2368
|
except Exception as exc:
|
|
2072
|
-
raise RuntimeError("获取 sender
|
|
2073
|
-
|
|
2074
|
-
tx = safe.functions.execTransaction(
|
|
2075
|
-
w3.to_checksum_address(ctf_addr),
|
|
2076
|
-
value,
|
|
2077
|
-
redeem_calldata,
|
|
2078
|
-
operation,
|
|
2079
|
-
safe_tx_gas,
|
|
2080
|
-
base_gas,
|
|
2081
|
-
gas_price,
|
|
2082
|
-
w3.to_checksum_address(gas_token),
|
|
2083
|
-
w3.to_checksum_address(refund_receiver),
|
|
2084
|
-
sig_bytes,
|
|
2085
|
-
).build_transaction(
|
|
2086
|
-
{
|
|
2087
|
-
"from": sender,
|
|
2088
|
-
"nonce": nonce,
|
|
2089
|
-
"gas": gas,
|
|
2090
|
-
"gasPrice": gas_price_chain,
|
|
2091
|
-
}
|
|
2092
|
-
)
|
|
2369
|
+
raise RuntimeError("获取 sender 地址失败") from exc
|
|
2093
2370
|
|
|
2094
|
-
signed_tx = w3.eth.account.sign_transaction(tx, private_key)
|
|
2095
2371
|
send_errors: list[str] = []
|
|
2096
2372
|
for attempt in range(3):
|
|
2097
2373
|
try:
|
|
2374
|
+
nonce = w3.eth.get_transaction_count(sender, "pending")
|
|
2375
|
+
gas_price_chain = int(w3.eth.gas_price)
|
|
2376
|
+
if attempt > 0:
|
|
2377
|
+
# retry 时小幅提价,减少卡在同价位的概率
|
|
2378
|
+
gas_price_chain = int(gas_price_chain * (1.0 + 0.10 * attempt))
|
|
2379
|
+
|
|
2380
|
+
exec_tx = safe.functions.execTransaction(
|
|
2381
|
+
w3.to_checksum_address(ctf_addr),
|
|
2382
|
+
value,
|
|
2383
|
+
redeem_calldata,
|
|
2384
|
+
operation,
|
|
2385
|
+
safe_tx_gas,
|
|
2386
|
+
base_gas,
|
|
2387
|
+
gas_price,
|
|
2388
|
+
w3.to_checksum_address(gas_token),
|
|
2389
|
+
w3.to_checksum_address(refund_receiver),
|
|
2390
|
+
sig_bytes,
|
|
2391
|
+
)
|
|
2392
|
+
try:
|
|
2393
|
+
simulated = exec_tx.call({"from": sender})
|
|
2394
|
+
except Exception as sim_exc:
|
|
2395
|
+
simulated = None
|
|
2396
|
+
if verbose:
|
|
2397
|
+
print(f"[claim] preflight call error (continue): {sim_exc}")
|
|
2398
|
+
if simulated is False:
|
|
2399
|
+
err = (
|
|
2400
|
+
f"Safe preflight failed (execTransaction call returned false): "
|
|
2401
|
+
f"safe_nonce={safe_nonce} sender_nonce={nonce}"
|
|
2402
|
+
)
|
|
2403
|
+
send_errors.append(err)
|
|
2404
|
+
if verbose:
|
|
2405
|
+
print(f"Safe redeem attempt {attempt+1} failed: {err}")
|
|
2406
|
+
break
|
|
2407
|
+
|
|
2408
|
+
tx = exec_tx.build_transaction(
|
|
2409
|
+
{
|
|
2410
|
+
"from": sender,
|
|
2411
|
+
"nonce": nonce,
|
|
2412
|
+
"gas": gas,
|
|
2413
|
+
"gasPrice": gas_price_chain,
|
|
2414
|
+
}
|
|
2415
|
+
)
|
|
2416
|
+
|
|
2417
|
+
signed_tx = w3.eth.account.sign_transaction(tx, private_key)
|
|
2098
2418
|
raw_tx = getattr(signed_tx, "rawTransaction", None) or getattr(signed_tx, "raw_transaction", None)
|
|
2099
2419
|
if raw_tx is None: # pragma: no cover
|
|
2100
2420
|
raise AttributeError("Signed transaction missing rawTransaction/raw_transaction")
|
|
2101
2421
|
tx_hash = w3.eth.send_raw_transaction(raw_tx)
|
|
2102
2422
|
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, poll_latency=2, timeout=180)
|
|
2103
2423
|
if receipt.get("status") != 1:
|
|
2104
|
-
|
|
2424
|
+
err = (
|
|
2425
|
+
f"Safe redeemPositions reverted on-chain: tx={tx_hash.hex()} "
|
|
2426
|
+
f"safe_nonce={safe_nonce} sender_nonce={nonce}"
|
|
2427
|
+
)
|
|
2428
|
+
send_errors.append(err)
|
|
2429
|
+
if verbose:
|
|
2430
|
+
print(f"Safe redeem attempt {attempt+1} failed: {err}")
|
|
2431
|
+
# 已经上链并回执失败,继续重试通常只会制造 nonce 噪音
|
|
2432
|
+
break
|
|
2105
2433
|
if verbose:
|
|
2106
2434
|
print(
|
|
2107
2435
|
{
|
|
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
|
|
File without changes
|
|
File without changes
|