hyperquant 1.33__tar.gz → 1.35__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.33 → hyperquant-1.35}/PKG-INFO +1 -1
- {hyperquant-1.33 → hyperquant-1.35}/pyproject.toml +1 -1
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/auth.py +60 -2
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/models/polymarket.py +82 -21
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/polymarket.py +290 -28
- {hyperquant-1.33 → hyperquant-1.35}/uv.lock +1 -1
- {hyperquant-1.33 → hyperquant-1.35}/.gitignore +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/README.md +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/requirements-dev.lock +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/requirements.lock +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/__init__.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/bitget.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/bitmart.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/coinw.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/deepcoin.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/edgex.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/hyperliquid.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/lbank.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/lib/hpstore.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/lib/hyper_types.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/lib/util.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/lighter.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/models/apexpro.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/models/bitget.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/models/bitmart.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/models/coinw.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/models/deepcoin.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/models/edgex.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/models/hyperliquid.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/models/lbank.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/models/lighter.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/models/ourbit.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/ourbit.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/broker/ws.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/core.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/datavison/_util.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/datavison/binance.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/datavison/coinglass.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/datavison/okx.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/db.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/draw.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/src/hyperquant/logkit.py +0 -0
- {hyperquant-1.33 → hyperquant-1.35}/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.35
|
|
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
|
|
@@ -499,9 +499,67 @@ class Auth:
|
|
|
499
499
|
if not creds:
|
|
500
500
|
return args
|
|
501
501
|
|
|
502
|
+
if isinstance(creds, tuple):
|
|
503
|
+
creds = list(creds)
|
|
504
|
+
session.__dict__["_apis"][api_name] = creds
|
|
505
|
+
|
|
502
506
|
private_key = creds[0] if len(creds) > 0 and creds[0] else None
|
|
503
|
-
|
|
504
|
-
|
|
507
|
+
if private_key:
|
|
508
|
+
pk_str = str(private_key)
|
|
509
|
+
if not pk_str.startswith("0x"):
|
|
510
|
+
pk_str = f"0x{pk_str}"
|
|
511
|
+
try:
|
|
512
|
+
session.__dict__["_apis"][api_name][0] = pk_str
|
|
513
|
+
except Exception:
|
|
514
|
+
pass
|
|
515
|
+
private_key = pk_str
|
|
516
|
+
|
|
517
|
+
packed_extra = creds[2] if len(creds) > 2 else None
|
|
518
|
+
packed_api_key = packed_api_secret = packed_passphrase = None
|
|
519
|
+
packed_chain_id = packed_wallet = None
|
|
520
|
+
if isinstance(packed_extra, (list, tuple)):
|
|
521
|
+
def _packed_value(idx: int):
|
|
522
|
+
if idx >= len(packed_extra):
|
|
523
|
+
return None
|
|
524
|
+
value = packed_extra[idx]
|
|
525
|
+
if isinstance(value, str):
|
|
526
|
+
value = value.strip()
|
|
527
|
+
return value or None
|
|
528
|
+
|
|
529
|
+
packed_api_key = _packed_value(0)
|
|
530
|
+
packed_api_secret = _packed_value(1)
|
|
531
|
+
packed_passphrase = _packed_value(2)
|
|
532
|
+
packed_chain_id = _packed_value(3)
|
|
533
|
+
packed_wallet = _packed_value(4)
|
|
534
|
+
elif isinstance(packed_extra, str):
|
|
535
|
+
packed_wallet = packed_extra or None
|
|
536
|
+
|
|
537
|
+
existing_chain_id = session.__dict__.get("_polymarket_chain_id")
|
|
538
|
+
if existing_chain_id is None:
|
|
539
|
+
chain_id = 137
|
|
540
|
+
if packed_chain_id is not None:
|
|
541
|
+
try:
|
|
542
|
+
chain_id = int(packed_chain_id)
|
|
543
|
+
except (TypeError, ValueError):
|
|
544
|
+
chain_id = 137
|
|
545
|
+
session.__dict__["_polymarket_chain_id"] = chain_id
|
|
546
|
+
else:
|
|
547
|
+
chain_id = existing_chain_id
|
|
548
|
+
|
|
549
|
+
api_meta = session.__dict__.get("_polymarket_api_creds") or {}
|
|
550
|
+
if (not api_meta.get("api_key") or not api_meta.get("api_secret") or not api_meta.get("api_passphrase")) and (
|
|
551
|
+
packed_api_key and packed_api_secret and packed_passphrase
|
|
552
|
+
):
|
|
553
|
+
api_meta = {
|
|
554
|
+
"api_key": packed_api_key,
|
|
555
|
+
"api_secret": packed_api_secret,
|
|
556
|
+
"api_passphrase": packed_passphrase,
|
|
557
|
+
}
|
|
558
|
+
session.__dict__["_polymarket_api_creds"] = api_meta
|
|
559
|
+
|
|
560
|
+
if packed_wallet and len(creds) > 2 and isinstance(creds[2], (list, tuple)):
|
|
561
|
+
creds[2] = packed_wallet
|
|
562
|
+
|
|
505
563
|
api_key = api_meta.get("api_key")
|
|
506
564
|
api_secret = api_meta.get("api_secret")
|
|
507
565
|
api_passphrase = api_meta.get("api_passphrase")
|
|
@@ -18,6 +18,50 @@ class Position(DataStore):
|
|
|
18
18
|
def _on_response(self, msg: Item) -> None:
|
|
19
19
|
self._update(msg)
|
|
20
20
|
|
|
21
|
+
def on_trade(self, trade: Item) -> None:
|
|
22
|
+
status = str(trade.get("status") or "").upper()
|
|
23
|
+
if status not in {"MATCHED"}:
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
asset_id = trade.get("asset_id")
|
|
27
|
+
outcome = trade.get("outcome")
|
|
28
|
+
side = str(trade.get("side") or "").upper()
|
|
29
|
+
size_raw = trade.get("size")
|
|
30
|
+
|
|
31
|
+
if not asset_id or not outcome or side not in {"BUY", "SELL"}:
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
size = float(size_raw)
|
|
36
|
+
except (TypeError, ValueError):
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
key = {"asset": asset_id, "outcome": outcome}
|
|
40
|
+
existing = self.get(key) or {}
|
|
41
|
+
|
|
42
|
+
cur_size = float(existing.get("size") or 0.0)
|
|
43
|
+
cur_total_bought = float(existing.get("totalBought") or 0.0)
|
|
44
|
+
|
|
45
|
+
if side == "BUY":
|
|
46
|
+
new_size = cur_size + size
|
|
47
|
+
total_bought = cur_total_bought + size
|
|
48
|
+
else: # SELL
|
|
49
|
+
new_size = cur_size - size
|
|
50
|
+
total_bought = cur_total_bought
|
|
51
|
+
|
|
52
|
+
rec: dict[str, Any] = {
|
|
53
|
+
"asset": asset_id,
|
|
54
|
+
"outcome": outcome,
|
|
55
|
+
"side": side,
|
|
56
|
+
"size": new_size,
|
|
57
|
+
"totalBought": total_bought,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if existing:
|
|
61
|
+
self._update([rec])
|
|
62
|
+
else:
|
|
63
|
+
self._insert([rec])
|
|
64
|
+
|
|
21
65
|
|
|
22
66
|
class Fill(DataStore):
|
|
23
67
|
"""Fill records keyed by maker order id."""
|
|
@@ -157,13 +201,17 @@ class MyTrade(DataStore):
|
|
|
157
201
|
class Trade(DataStore):
|
|
158
202
|
"""User trades keyed by trade id."""
|
|
159
203
|
|
|
160
|
-
_KEYS = ["
|
|
204
|
+
_KEYS = ["asset_id"]
|
|
161
205
|
_MAXLEN = 500
|
|
162
206
|
|
|
163
207
|
|
|
164
208
|
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
165
|
-
payload = msg
|
|
209
|
+
payload = msg or {}
|
|
166
210
|
if payload:
|
|
211
|
+
size = float(payload.get("size"))
|
|
212
|
+
price = float(payload.get("price"))
|
|
213
|
+
# share = size / price
|
|
214
|
+
# payload['shares'] = share
|
|
167
215
|
self._insert([payload])
|
|
168
216
|
|
|
169
217
|
class Book(DataStore):
|
|
@@ -364,10 +412,14 @@ class Detail(DataStore):
|
|
|
364
412
|
|
|
365
413
|
for token in tokens:
|
|
366
414
|
normalized = self._normalize_entry(market, token)
|
|
415
|
+
slug: str = market.get("slug")
|
|
416
|
+
# 取最后一个'-'之前部分
|
|
417
|
+
base_slug = slug.rsplit("-", 1)[0] if slug else slug
|
|
367
418
|
# Add or update additional fields from market
|
|
368
419
|
normalized.update({
|
|
369
420
|
"condition_id": market.get("conditionId"),
|
|
370
421
|
"slug": market.get("slug"),
|
|
422
|
+
"base_slug": base_slug,
|
|
371
423
|
"end_date": market.get("endDate"),
|
|
372
424
|
"start_date": market.get("startDate"),
|
|
373
425
|
"icon": market.get("icon"),
|
|
@@ -579,27 +631,35 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
579
631
|
def trade(self) -> Trade:
|
|
580
632
|
"""
|
|
581
633
|
_key asset
|
|
582
|
-
|
|
634
|
+
MATCHED进行快速捕捉
|
|
583
635
|
.. code:: json
|
|
584
636
|
{
|
|
585
|
-
"
|
|
586
|
-
"
|
|
587
|
-
"
|
|
588
|
-
"
|
|
589
|
-
"
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
"
|
|
600
|
-
"
|
|
601
|
-
"
|
|
602
|
-
"
|
|
637
|
+
"asset_id": "52114319501245915516055106046884209969926127482827954674443846427813813222426",
|
|
638
|
+
"event_type": "trade",
|
|
639
|
+
"id": "28c4d2eb-bbea-40e7-a9f0-b2fdb56b2c2e",
|
|
640
|
+
"last_update": "1672290701",
|
|
641
|
+
"maker_orders": [
|
|
642
|
+
{
|
|
643
|
+
"asset_id": "52114319501245915516055106046884209969926127482827954674443846427813813222426",
|
|
644
|
+
"matched_amount": "10",
|
|
645
|
+
"order_id": "0xff354cd7ca7539dfa9c28d90943ab5779a4eac34b9b37a757d7b32bdfb11790b",
|
|
646
|
+
"outcome": "YES",
|
|
647
|
+
"owner": "9180014b-33c8-9240-a14b-bdca11c0a465",
|
|
648
|
+
"price": "0.57"
|
|
649
|
+
}
|
|
650
|
+
],
|
|
651
|
+
"market": "0xbd31dc8a20211944f6b70f31557f1001557b59905b7738480ca09bd4532f84af",
|
|
652
|
+
"matchtime": "1672290701",
|
|
653
|
+
"outcome": "YES",
|
|
654
|
+
"owner": "9180014b-33c8-9240-a14b-bdca11c0a465",
|
|
655
|
+
"price": "0.57",
|
|
656
|
+
"side": "BUY",
|
|
657
|
+
"size": "10",
|
|
658
|
+
"status": "MATCHED",
|
|
659
|
+
"taker_order_id": "0x06bc63e346ed4ceddce9efd6b3af37c8f8f440c92fe7da6b2d0f9e4ccbc50c42",
|
|
660
|
+
"timestamp": "1672290701",
|
|
661
|
+
"trade_owner": "9180014b-33c8-9240-a14b-bdca11c0a465",
|
|
662
|
+
"type": "TRADE"
|
|
603
663
|
}
|
|
604
664
|
"""
|
|
605
665
|
return self._get("trade")
|
|
@@ -620,5 +680,6 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
620
680
|
elif msg_type == "trade":
|
|
621
681
|
self.trade._on_message(m)
|
|
622
682
|
self.fill._on_trade(m)
|
|
683
|
+
self.position.on_trade(m)
|
|
623
684
|
elif msg_type == 'orders_matched':
|
|
624
685
|
self.trade._on_message(m)
|
|
@@ -4,6 +4,7 @@ import asyncio
|
|
|
4
4
|
from contextlib import suppress
|
|
5
5
|
from datetime import UTC, datetime
|
|
6
6
|
from functools import lru_cache
|
|
7
|
+
import os
|
|
7
8
|
from typing import Any, Iterable, Iterator, Literal, Mapping, Sequence
|
|
8
9
|
|
|
9
10
|
import json
|
|
@@ -38,6 +39,14 @@ DEFAULT_POLYGON_RPCS = (
|
|
|
38
39
|
"https://rpc.ankr.com/polygon",
|
|
39
40
|
)
|
|
40
41
|
|
|
42
|
+
def parse_field(value):
|
|
43
|
+
"""尝试将字符串 JSON 转为对象,否则原样返回"""
|
|
44
|
+
if isinstance(value, str):
|
|
45
|
+
try:
|
|
46
|
+
return json.loads(value)
|
|
47
|
+
except json.JSONDecodeError:
|
|
48
|
+
return value
|
|
49
|
+
return value
|
|
41
50
|
|
|
42
51
|
def _iter_offsets(window: int) -> Iterator[int]:
|
|
43
52
|
yield 0
|
|
@@ -94,7 +103,7 @@ class Polymarket:
|
|
|
94
103
|
self._ws_personal: pybotters.ws.WebSocketApp | None = None
|
|
95
104
|
self.auth = False
|
|
96
105
|
|
|
97
|
-
self._ensure_session_entry(private_key=private_key, funder=funder)
|
|
106
|
+
self._ensure_session_entry(private_key=private_key, funder=funder, chain_id=chain_id)
|
|
98
107
|
|
|
99
108
|
async def __aenter__(self) -> "Polymarket":
|
|
100
109
|
if self.auth:
|
|
@@ -266,6 +275,7 @@ class Polymarket:
|
|
|
266
275
|
self._ws_personal = self.client.ws_connect(
|
|
267
276
|
"wss://ws-subscriptions-clob.polymarket.com/ws/user",
|
|
268
277
|
hdlr_json=effective_cb,
|
|
278
|
+
heartbeat=30,
|
|
269
279
|
auth=None,
|
|
270
280
|
)
|
|
271
281
|
await self._ws_personal._event.wait()
|
|
@@ -298,7 +308,7 @@ class Polymarket:
|
|
|
298
308
|
wsapp = self.client.ws_connect(
|
|
299
309
|
RTS_DATA_ENDPOINT,
|
|
300
310
|
hdlr_str=callback,
|
|
301
|
-
heartbeat=
|
|
311
|
+
heartbeat=30
|
|
302
312
|
)
|
|
303
313
|
await wsapp._event.wait()
|
|
304
314
|
await wsapp.current_ws.send_json(payload)
|
|
@@ -314,9 +324,12 @@ class Polymarket:
|
|
|
314
324
|
return await self._rest("GET", f"/markets/{market_id}")
|
|
315
325
|
|
|
316
326
|
async def get_market_by_slug(self, slug: str) -> Any:
|
|
317
|
-
"""Fetch a market using its human-readable slug.
|
|
318
|
-
|
|
319
|
-
|
|
327
|
+
"""Fetch a market using its human-readable slug.
|
|
328
|
+
https://docs.polymarket.com/api-reference/markets/get-market-by-slug
|
|
329
|
+
"""
|
|
330
|
+
market:dict = await self._rest("GET", f"/slug/{slug}", host='https://gamma-api.polymarket.com/markets')
|
|
331
|
+
market = {k: parse_field(v) for k, v in market.items()}
|
|
332
|
+
return market
|
|
320
333
|
|
|
321
334
|
async def get_order_book(self, token_id: str) -> Any:
|
|
322
335
|
return await self._rest("GET", "/book", params={"token_id": token_id})
|
|
@@ -498,7 +511,7 @@ class Polymarket:
|
|
|
498
511
|
side: str,
|
|
499
512
|
price: float,
|
|
500
513
|
size: float,
|
|
501
|
-
order_type:
|
|
514
|
+
order_type: Literal["GTC", 'FOK'] = "GTC",
|
|
502
515
|
tick_size: str | float | None = None,
|
|
503
516
|
fee_rate_bps: int | None = None,
|
|
504
517
|
expiration: int | None = None,
|
|
@@ -523,6 +536,9 @@ class Polymarket:
|
|
|
523
536
|
- nonce: onchain nonce, default 0
|
|
524
537
|
"""
|
|
525
538
|
|
|
539
|
+
if price <= 0:
|
|
540
|
+
raise ValueError("price must be positive; use place_market_order for market orders")
|
|
541
|
+
|
|
526
542
|
# Ensure L2 creds exist
|
|
527
543
|
if not self._api_creds():
|
|
528
544
|
raise RuntimeError("Polymarket API credentials missing; call create_or_derive_api_creds first")
|
|
@@ -552,6 +568,117 @@ class Polymarket:
|
|
|
552
568
|
}
|
|
553
569
|
return await self._signed_request_via_session("POST", "/order", payload)
|
|
554
570
|
|
|
571
|
+
async def place_market_order(
|
|
572
|
+
self,
|
|
573
|
+
*,
|
|
574
|
+
token_id: str,
|
|
575
|
+
side: str,
|
|
576
|
+
amount: float,
|
|
577
|
+
order_type: Literal["FOK", "GTC", "FAK", "GTD"] = "FOK",
|
|
578
|
+
price: float | None = None,
|
|
579
|
+
tick_size: str | float | None = None,
|
|
580
|
+
fee_rate_bps: int | None = None,
|
|
581
|
+
taker: str = ZERO_ADDRESS,
|
|
582
|
+
neg_risk: bool = False,
|
|
583
|
+
owner: str | None = None,
|
|
584
|
+
nonce: int | None = None,
|
|
585
|
+
) -> Any:
|
|
586
|
+
"""Create, sign and submit a market order similar to ``py_clob_client``.
|
|
587
|
+
|
|
588
|
+
BUY orders treat ``amount`` as collateral (USDC); SELL orders treat it as shares.
|
|
589
|
+
"""
|
|
590
|
+
|
|
591
|
+
if amount <= 0:
|
|
592
|
+
raise ValueError("amount must be greater than 0 for market orders")
|
|
593
|
+
|
|
594
|
+
if not self._api_creds():
|
|
595
|
+
raise RuntimeError("Polymarket API credentials missing; call create_or_derive_api_creds first")
|
|
596
|
+
|
|
597
|
+
private_key, maker_addr, signer_addr = self._get_signing_context()
|
|
598
|
+
owner_key = self._owner_key(owner)
|
|
599
|
+
order_type_str = (order_type or "FOK").upper()
|
|
600
|
+
|
|
601
|
+
if price is None or price <= 0:
|
|
602
|
+
price = await self._calculate_market_price(
|
|
603
|
+
token_id=token_id,
|
|
604
|
+
side=side,
|
|
605
|
+
amount=amount,
|
|
606
|
+
order_type=order_type_str,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
signed_dict = await self._build_signed_market_order(
|
|
610
|
+
private_key=private_key,
|
|
611
|
+
maker_addr=maker_addr,
|
|
612
|
+
signer_addr=signer_addr,
|
|
613
|
+
token_id=token_id,
|
|
614
|
+
side=side,
|
|
615
|
+
amount=amount,
|
|
616
|
+
price=price,
|
|
617
|
+
tick_size=tick_size,
|
|
618
|
+
fee_rate_bps=fee_rate_bps,
|
|
619
|
+
taker=taker,
|
|
620
|
+
neg_risk=neg_risk,
|
|
621
|
+
nonce=nonce,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
payload = {
|
|
625
|
+
"order": signed_dict,
|
|
626
|
+
"owner": owner_key,
|
|
627
|
+
"orderType": order_type_str,
|
|
628
|
+
}
|
|
629
|
+
return await self._signed_request_via_session("POST", "/order", payload)
|
|
630
|
+
|
|
631
|
+
async def _calculate_market_price(
|
|
632
|
+
self,
|
|
633
|
+
*,
|
|
634
|
+
token_id: str,
|
|
635
|
+
side: str,
|
|
636
|
+
amount: float,
|
|
637
|
+
order_type: str,
|
|
638
|
+
) -> float:
|
|
639
|
+
side_flag = side.upper()
|
|
640
|
+
if side_flag not in {"BUY", "SELL"}:
|
|
641
|
+
raise ValueError("side must be 'BUY' or 'SELL'")
|
|
642
|
+
if amount <= 0:
|
|
643
|
+
raise ValueError("amount must be greater than 0 for market pricing")
|
|
644
|
+
|
|
645
|
+
book = await self.get_order_book(token_id)
|
|
646
|
+
if not isinstance(book, Mapping):
|
|
647
|
+
raise RuntimeError("Polymarket order book unavailable for market order")
|
|
648
|
+
|
|
649
|
+
key = "asks" if side_flag == "BUY" else "bids"
|
|
650
|
+
raw_levels = book.get(key) or []
|
|
651
|
+
levels: list[tuple[float, float]] = []
|
|
652
|
+
for lvl in raw_levels:
|
|
653
|
+
try:
|
|
654
|
+
price = float(lvl.get("price"))
|
|
655
|
+
size = float(lvl.get("size"))
|
|
656
|
+
except (TypeError, ValueError):
|
|
657
|
+
continue
|
|
658
|
+
if price is None or size is None:
|
|
659
|
+
continue
|
|
660
|
+
levels.append((price, size))
|
|
661
|
+
|
|
662
|
+
if not levels:
|
|
663
|
+
raise RuntimeError(f"Polymarket market order has no {key} liquidity")
|
|
664
|
+
|
|
665
|
+
total = 0.0
|
|
666
|
+
if side_flag == "BUY":
|
|
667
|
+
for price, size in reversed(levels):
|
|
668
|
+
total += price * size
|
|
669
|
+
if total >= amount:
|
|
670
|
+
return price
|
|
671
|
+
else:
|
|
672
|
+
for price, size in reversed(levels):
|
|
673
|
+
total += size
|
|
674
|
+
if total >= amount:
|
|
675
|
+
return price
|
|
676
|
+
|
|
677
|
+
if (order_type or "FOK").upper() == "FOK":
|
|
678
|
+
raise RuntimeError("Polymarket market order exceeds available liquidity")
|
|
679
|
+
|
|
680
|
+
return levels[0][0]
|
|
681
|
+
|
|
555
682
|
async def _signed_request_via_session(
|
|
556
683
|
self, method: str, path: str, body: Mapping[str, Any] | list[Any] | None
|
|
557
684
|
) -> Any:
|
|
@@ -686,6 +813,76 @@ class Polymarket:
|
|
|
686
813
|
},
|
|
687
814
|
)
|
|
688
815
|
|
|
816
|
+
async def _build_signed_market_order(
|
|
817
|
+
self,
|
|
818
|
+
*,
|
|
819
|
+
private_key: str,
|
|
820
|
+
maker_addr: str,
|
|
821
|
+
signer_addr: str,
|
|
822
|
+
token_id: str,
|
|
823
|
+
side: str,
|
|
824
|
+
amount: float,
|
|
825
|
+
price: float,
|
|
826
|
+
tick_size: str | float | None,
|
|
827
|
+
fee_rate_bps: int | None,
|
|
828
|
+
taker: str,
|
|
829
|
+
neg_risk: bool,
|
|
830
|
+
nonce: int | None,
|
|
831
|
+
) -> dict[str, Any]:
|
|
832
|
+
side = side.upper()
|
|
833
|
+
tick = await self._resolve_tick_size(token_id, tick_size)
|
|
834
|
+
fee_bps = await self._resolve_fee_rate(token_id, fee_rate_bps)
|
|
835
|
+
|
|
836
|
+
price_d, size_d, amt_d = self._rounding_for_tick(tick)
|
|
837
|
+
price = float(self._round_normal(price, price_d))
|
|
838
|
+
if price <= 0:
|
|
839
|
+
raise ValueError("market price must be positive")
|
|
840
|
+
|
|
841
|
+
amt = float(amount)
|
|
842
|
+
if amt <= 0:
|
|
843
|
+
raise ValueError("amount must be greater than 0")
|
|
844
|
+
|
|
845
|
+
maker_amt_raw = self._round_down(amt, size_d)
|
|
846
|
+
if maker_amt_raw <= 0:
|
|
847
|
+
raise ValueError("amount too small for current tick size")
|
|
848
|
+
|
|
849
|
+
if side == "BUY":
|
|
850
|
+
taker_amt_raw = maker_amt_raw / price
|
|
851
|
+
elif side == "SELL":
|
|
852
|
+
taker_amt_raw = maker_amt_raw * price
|
|
853
|
+
else:
|
|
854
|
+
raise ValueError("side must be 'BUY' or 'SELL'")
|
|
855
|
+
|
|
856
|
+
if self._decimal_places(taker_amt_raw) > amt_d:
|
|
857
|
+
tmp = self._round_up(taker_amt_raw, amt_d + 4)
|
|
858
|
+
taker_amt_raw = tmp if self._decimal_places(tmp) <= amt_d else self._round_down(tmp, amt_d)
|
|
859
|
+
|
|
860
|
+
maker_amount = self._to_token_decimals(maker_amt_raw)
|
|
861
|
+
taker_amount = self._to_token_decimals(taker_amt_raw)
|
|
862
|
+
|
|
863
|
+
contract = self._contracts(self.chain_id, neg_risk)
|
|
864
|
+
side_flag = 0 if side == "BUY" else 1
|
|
865
|
+
sig_type = int(self.signature_type)
|
|
866
|
+
|
|
867
|
+
return Auth.sign_polymarket_order(
|
|
868
|
+
private_key=private_key,
|
|
869
|
+
chain_id=self.chain_id,
|
|
870
|
+
exchange_address=contract["exchange"],
|
|
871
|
+
order={
|
|
872
|
+
"maker": maker_addr,
|
|
873
|
+
"signer": signer_addr,
|
|
874
|
+
"taker": taker or ZERO_ADDRESS,
|
|
875
|
+
"tokenId": str(token_id),
|
|
876
|
+
"makerAmount": int(maker_amount),
|
|
877
|
+
"takerAmount": int(taker_amount),
|
|
878
|
+
"expiration": 0,
|
|
879
|
+
"nonce": int(nonce or 0),
|
|
880
|
+
"feeRateBps": int(fee_bps or 0),
|
|
881
|
+
"side": side_flag,
|
|
882
|
+
"signatureType": sig_type,
|
|
883
|
+
},
|
|
884
|
+
)
|
|
885
|
+
|
|
689
886
|
async def _resolve_tick_size(self, token_id: str, tick_size: str | float | None) -> str:
|
|
690
887
|
if tick_size is not None:
|
|
691
888
|
return str(tick_size)
|
|
@@ -878,6 +1075,7 @@ class Polymarket:
|
|
|
878
1075
|
|
|
879
1076
|
async def get_trades(self, params: Mapping[str, Any] | None = None) -> list[Any]:
|
|
880
1077
|
return await self._paginate("/data/trades", params)
|
|
1078
|
+
|
|
881
1079
|
|
|
882
1080
|
async def get_notifications(self, signature_type: int | None = None) -> Any:
|
|
883
1081
|
sig = signature_type if signature_type is not None else self.signature_type
|
|
@@ -889,12 +1087,30 @@ class Polymarket:
|
|
|
889
1087
|
return await self._rest("DELETE", "/notifications", params=params)
|
|
890
1088
|
|
|
891
1089
|
async def get_balance_allowance(self, **params: Any) -> Any:
|
|
892
|
-
|
|
1090
|
+
query = dict(params or {})
|
|
1091
|
+
query.setdefault("signature_type", self.signature_type)
|
|
1092
|
+
return await self._rest("GET", "/balance-allowance", params=query or None)
|
|
893
1093
|
|
|
894
1094
|
async def update_balance_allowance(self, **params: Any) -> Any:
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1095
|
+
body = dict(params or {})
|
|
1096
|
+
body.setdefault("signature_type", self.signature_type)
|
|
1097
|
+
return await self._rest("POST", "/balance-allowance/update", json=body or None)
|
|
1098
|
+
|
|
1099
|
+
async def get_usdc(self):
|
|
1100
|
+
data = await self.get_balance_allowance(asset_type='COLLATERAL')
|
|
1101
|
+
balance = float(data.get('balance', 0.0))
|
|
1102
|
+
if balance > 0:
|
|
1103
|
+
balance = balance / 1e6
|
|
1104
|
+
return balance
|
|
1105
|
+
|
|
1106
|
+
async def get_position(self, token_id: str) -> Any:
|
|
1107
|
+
data = await self.get_balance_allowance(asset_type='CONDITIONAL', token_id=token_id)
|
|
1108
|
+
position = float(data.get('balance', 0.0))
|
|
1109
|
+
if position > 0:
|
|
1110
|
+
position = position / 1e6
|
|
1111
|
+
return position
|
|
1112
|
+
|
|
1113
|
+
async def get_usdc_web3(
|
|
898
1114
|
self,
|
|
899
1115
|
wallet: str = None,
|
|
900
1116
|
rpc_urls: Sequence[str] | None = None,
|
|
@@ -966,14 +1182,7 @@ class Polymarket:
|
|
|
966
1182
|
if not event:
|
|
967
1183
|
continue
|
|
968
1184
|
|
|
969
|
-
|
|
970
|
-
"""尝试将字符串 JSON 转为对象,否则原样返回"""
|
|
971
|
-
if isinstance(value, str):
|
|
972
|
-
try:
|
|
973
|
-
return json.loads(value)
|
|
974
|
-
except json.JSONDecodeError:
|
|
975
|
-
return value
|
|
976
|
-
return value
|
|
1185
|
+
|
|
977
1186
|
|
|
978
1187
|
event = {k: parse_field(v) for k, v in event.items()}
|
|
979
1188
|
|
|
@@ -1113,6 +1322,7 @@ class Polymarket:
|
|
|
1113
1322
|
*,
|
|
1114
1323
|
private_key: str | None,
|
|
1115
1324
|
funder: str | None,
|
|
1325
|
+
chain_id: int | None,
|
|
1116
1326
|
) -> None:
|
|
1117
1327
|
session = getattr(self.client, "_session", None)
|
|
1118
1328
|
if session is None:
|
|
@@ -1125,27 +1335,66 @@ class Polymarket:
|
|
|
1125
1335
|
if not entry and not private_key:
|
|
1126
1336
|
return
|
|
1127
1337
|
|
|
1338
|
+
packed = entry[2] if len(entry) > 2 else None
|
|
1339
|
+
if not isinstance(packed, (list, tuple)):
|
|
1340
|
+
packed = None
|
|
1341
|
+
|
|
1342
|
+
def _packed_value(idx: int) -> Any | None:
|
|
1343
|
+
if packed is None:
|
|
1344
|
+
return None
|
|
1345
|
+
if idx >= len(packed):
|
|
1346
|
+
return None
|
|
1347
|
+
value = packed[idx]
|
|
1348
|
+
if isinstance(value, str):
|
|
1349
|
+
value = value.strip()
|
|
1350
|
+
return value or None
|
|
1351
|
+
|
|
1352
|
+
packed_api_key = _packed_value(0)
|
|
1353
|
+
packed_api_secret = _packed_value(1)
|
|
1354
|
+
packed_passphrase = _packed_value(2)
|
|
1355
|
+
packed_chain_id = _packed_value(3)
|
|
1356
|
+
packed_wallet = _packed_value(4)
|
|
1357
|
+
|
|
1128
1358
|
while len(entry) < 3:
|
|
1129
1359
|
entry.append("")
|
|
1130
1360
|
|
|
1131
|
-
existing_pk = entry[0]
|
|
1361
|
+
existing_pk = entry[0] if entry else None
|
|
1132
1362
|
normalized_pk: str | None = None
|
|
1133
|
-
|
|
1363
|
+
candidate_pk = private_key or existing_pk
|
|
1364
|
+
if candidate_pk:
|
|
1365
|
+
candidate_pk = str(candidate_pk)
|
|
1134
1366
|
normalized_pk = (
|
|
1135
|
-
|
|
1136
|
-
)
|
|
1137
|
-
elif existing_pk:
|
|
1138
|
-
normalized_pk = (
|
|
1139
|
-
existing_pk if existing_pk.startswith("0x") else f"0x{existing_pk}"
|
|
1367
|
+
candidate_pk if candidate_pk.startswith("0x") else f"0x{candidate_pk}"
|
|
1140
1368
|
)
|
|
1141
1369
|
|
|
1142
1370
|
if not normalized_pk:
|
|
1143
1371
|
raise RuntimeError("Polymarket需要钱包私钥 (apis['polymarket'][0])")
|
|
1144
1372
|
|
|
1145
1373
|
entry[0] = normalized_pk
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1374
|
+
|
|
1375
|
+
existing_wallet = entry[2] if isinstance(entry[2], str) and entry[2] else None
|
|
1376
|
+
effective_wallet = funder or packed_wallet or existing_wallet or self.funder
|
|
1377
|
+
if effective_wallet:
|
|
1378
|
+
entry[2] = effective_wallet
|
|
1379
|
+
self.funder = effective_wallet
|
|
1380
|
+
else:
|
|
1381
|
+
entry[2] = ""
|
|
1382
|
+
|
|
1383
|
+
derived_chain_id: int | None = None
|
|
1384
|
+
if packed_chain_id is not None:
|
|
1385
|
+
try:
|
|
1386
|
+
derived_chain_id = int(packed_chain_id)
|
|
1387
|
+
except (TypeError, ValueError):
|
|
1388
|
+
derived_chain_id = None
|
|
1389
|
+
if chain_id is None and derived_chain_id is not None:
|
|
1390
|
+
self.chain_id = derived_chain_id
|
|
1391
|
+
|
|
1392
|
+
if packed_api_key and packed_api_secret and packed_passphrase:
|
|
1393
|
+
session.__dict__["_polymarket_api_creds"] = {
|
|
1394
|
+
"api_key": packed_api_key,
|
|
1395
|
+
"api_secret": packed_api_secret,
|
|
1396
|
+
"api_passphrase": packed_passphrase,
|
|
1397
|
+
}
|
|
1149
1398
|
|
|
1150
1399
|
apis[API_NAME] = entry
|
|
1151
1400
|
session.__dict__["_apis"] = apis
|
|
@@ -1153,6 +1402,19 @@ class Polymarket:
|
|
|
1153
1402
|
session.__dict__["_polymarket_signature_type"] = self.signature_type
|
|
1154
1403
|
self.auth = True
|
|
1155
1404
|
|
|
1405
|
+
@staticmethod
|
|
1406
|
+
def load_poly_api():
|
|
1407
|
+
from dotenv import load_dotenv
|
|
1408
|
+
|
|
1409
|
+
load_dotenv()
|
|
1410
|
+
pk = os.getenv("PK")
|
|
1411
|
+
api_key = os.getenv("CLOB_API_KEY")
|
|
1412
|
+
api_secret = os.getenv("CLOB_API_SECRET")
|
|
1413
|
+
passphrase = os.getenv("CLOB_API_PASSPHRASE")
|
|
1414
|
+
chain_id = os.getenv("CHAIN_ID") or 137
|
|
1415
|
+
wallet_address = os.getenv("POLY_WALLET_ADDRESS")
|
|
1416
|
+
return [pk, "", (api_key, api_secret, passphrase, chain_id, wallet_address)]
|
|
1417
|
+
|
|
1156
1418
|
@lru_cache(maxsize=8)
|
|
1157
1419
|
def _get_web3(rpc_url: str):
|
|
1158
1420
|
from web3 import Web3
|
|
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
|