polymarket-backtest 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,35 @@
1
+ """
2
+ polymarket-backtest
3
+ ===================
4
+
5
+ Polymarket API 封装与策略回测评估工具包。
6
+
7
+ 快速开始:
8
+ # 1. 查询市场信息
9
+ from polymarket_backtest.api import GammaClient, ClobClient
10
+
11
+ gamma = GammaClient()
12
+ market = gamma.get_market_info("BTC")
13
+ print(market.slug, market.up_price)
14
+
15
+ # 2. 拉取赔率历史
16
+ clob = ClobClient()
17
+ history = clob.get_price_history(market.up_token_id, interval="1d")
18
+ print(f"{len(history)} 个价格点")
19
+
20
+ # 3. 加载内置示例数据集
21
+ from polymarket_backtest.data import load_trades, load_orderbook
22
+
23
+ trades = load_trades("flash_crash")
24
+ ob = load_orderbook("BTC")
25
+
26
+ # 4. 计算回测指标
27
+ from polymarket_backtest.backtest import summary
28
+
29
+ pnl = trades["gross_pnl"].dropna().tolist()
30
+ result = summary(pnl)
31
+ print(result)
32
+ # {'net_pnl': 47.92, 'win_rate': 0.143, 'sharpe_ratio': 0.85, 'max_drawdown': 12.5, ...}
33
+ """
34
+
35
+ __version__ = "0.1.0"
@@ -0,0 +1,13 @@
1
+ """Polymarket API 封装模块。"""
2
+
3
+ from .clob import ClobClient
4
+ from .gamma import GammaClient
5
+ from .models import MarketInfo, OddsHistory, PricePoint
6
+
7
+ __all__ = [
8
+ "GammaClient",
9
+ "ClobClient",
10
+ "MarketInfo",
11
+ "PricePoint",
12
+ "OddsHistory",
13
+ ]
@@ -0,0 +1,136 @@
1
+ """
2
+ Polymarket CLOB API 客户端(只读)
3
+
4
+ 封装赔率历史数据查询。无需认证。
5
+
6
+ 用法示例:
7
+ from polymarket_backtest.api import GammaClient, ClobClient
8
+
9
+ gamma = GammaClient()
10
+ clob = ClobClient()
11
+
12
+ market = gamma.get_market_info("BTC")
13
+ history = clob.get_price_history(market.up_token_id, interval="1d")
14
+
15
+ for point in history.points:
16
+ print(point.timestamp, point.price)
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import Literal
22
+
23
+ import requests
24
+
25
+ from .models import OddsHistory, PricePoint
26
+
27
+ Interval = Literal["1m", "1h", "6h", "1d", "1w", "max"]
28
+
29
+
30
+ class ClobClient:
31
+ """
32
+ Polymarket CLOB API 客户端(只读子集)。
33
+
34
+ 提供赔率历史查询,无需 API 密钥。
35
+ """
36
+
37
+ BASE_URL = "https://clob.polymarket.com"
38
+
39
+ def __init__(self, base_url: str = BASE_URL, timeout: int = 15):
40
+ self.base_url = base_url.rstrip("/")
41
+ self.timeout = timeout
42
+ self._session = requests.Session()
43
+
44
+ # ------------------------------------------------------------------
45
+ # 公开接口
46
+ # ------------------------------------------------------------------
47
+
48
+ def get_price_history(
49
+ self,
50
+ token_id: str,
51
+ interval: Interval | None = "1d",
52
+ fidelity: int = 60,
53
+ start_ts: int | None = None,
54
+ end_ts: int | None = None,
55
+ ) -> OddsHistory:
56
+ """
57
+ 拉取某 token 的赔率历史时间序列。
58
+
59
+ 参数:
60
+ token_id: CLOB token ID(从 GammaClient 获取)
61
+ interval: 时间窗口("1m","1h","6h","1d","1w","max")
62
+ 与 start_ts/end_ts 互斥
63
+ fidelity: 数据分辨率(分钟),例如 60 = 每小时一个点
64
+ start_ts: 开始时间 Unix 时间戳(UTC)
65
+ end_ts: 结束时间 Unix 时间戳(UTC)
66
+
67
+ 返回:
68
+ OddsHistory 对象,包含时间戳和对应价格列表
69
+
70
+ 示例:
71
+ # 拉取最近 1 天数据,每小时一个点
72
+ history = clob.get_price_history(token_id, interval="1d", fidelity=60)
73
+
74
+ # 拉取指定时间范围
75
+ history = clob.get_price_history(
76
+ token_id,
77
+ start_ts=1697875200,
78
+ end_ts=1697961600,
79
+ fidelity=5,
80
+ )
81
+ """
82
+ params: dict = {"market": token_id, "fidelity": fidelity}
83
+
84
+ if start_ts is not None or end_ts is not None:
85
+ if start_ts is not None:
86
+ params["startTs"] = start_ts
87
+ if end_ts is not None:
88
+ params["endTs"] = end_ts
89
+ elif interval is not None:
90
+ params["interval"] = interval
91
+
92
+ url = f"{self.base_url}/prices-history"
93
+ resp = self._session.get(url, params=params, timeout=self.timeout)
94
+ resp.raise_for_status()
95
+
96
+ data = resp.json()
97
+ raw_points = data.get("history", [])
98
+ points = [PricePoint(timestamp=int(p["t"]), price=float(p["p"])) for p in raw_points]
99
+
100
+ return OddsHistory(
101
+ token_id=token_id,
102
+ interval=interval or "custom",
103
+ fidelity=fidelity,
104
+ points=points,
105
+ )
106
+
107
+ def get_price_history_df(
108
+ self,
109
+ token_id: str,
110
+ interval: Interval | None = "1d",
111
+ fidelity: int = 60,
112
+ start_ts: int | None = None,
113
+ end_ts: int | None = None,
114
+ ):
115
+ """
116
+ 同 get_price_history,但直接返回 pandas DataFrame。
117
+
118
+ DataFrame 列:timestamp(Unix)、price、datetime(UTC)
119
+ """
120
+ import pandas as pd
121
+
122
+ history = self.get_price_history(
123
+ token_id,
124
+ interval=interval,
125
+ fidelity=fidelity,
126
+ start_ts=start_ts,
127
+ end_ts=end_ts,
128
+ )
129
+ if not history.points:
130
+ return pd.DataFrame(columns=["timestamp", "price", "datetime"])
131
+
132
+ df = pd.DataFrame(
133
+ {"timestamp": history.timestamps(), "price": history.prices()}
134
+ )
135
+ df["datetime"] = pd.to_datetime(df["timestamp"], unit="s", utc=True)
136
+ return df
@@ -0,0 +1,194 @@
1
+ """
2
+ Polymarket Gamma API 客户端
3
+
4
+ 封装市场发现与合约信息查询,支持 BTC/ETH/SOL/XRP 的 15 分钟涨跌市场。
5
+
6
+ 用法示例:
7
+ from polymarket_backtest.api import GammaClient
8
+
9
+ client = GammaClient()
10
+ market = client.get_market_info("ETH")
11
+ print(market.slug, market.up_price, market.down_price)
12
+
13
+ # 列出近期市场
14
+ markets = client.list_recent_markets("BTC", n=5)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import time
21
+ from datetime import datetime, timezone
22
+ from typing import Any
23
+
24
+ import requests
25
+
26
+ from .models import MarketInfo
27
+
28
+
29
+ class GammaClient:
30
+ """
31
+ Polymarket Gamma API 客户端。
32
+
33
+ 用于发现市场和获取市场元数据(无需认证)。
34
+ """
35
+
36
+ BASE_URL = "https://gamma-api.polymarket.com"
37
+
38
+ COIN_SLUGS: dict[str, str] = {
39
+ "BTC": "btc-updown-15m",
40
+ "ETH": "eth-updown-15m",
41
+ "SOL": "sol-updown-15m",
42
+ "XRP": "xrp-updown-15m",
43
+ }
44
+ SUPPORTED_COINS = list(COIN_SLUGS.keys())
45
+
46
+ def __init__(self, base_url: str = BASE_URL, timeout: int = 10):
47
+ self.base_url = base_url.rstrip("/")
48
+ self.timeout = timeout
49
+ self._session = requests.Session()
50
+
51
+ # ------------------------------------------------------------------
52
+ # 公开接口
53
+ # ------------------------------------------------------------------
54
+
55
+ def get_market_info(self, coin: str) -> MarketInfo | None:
56
+ """
57
+ 获取某币种当前活跃的 15 分钟市场信息。
58
+
59
+ 参数:
60
+ coin: 币种符号(BTC / ETH / SOL / XRP,不区分大小写)
61
+
62
+ 返回:
63
+ MarketInfo 对象,无活跃市场时返回 None
64
+ """
65
+ market = self._get_active_market(coin.upper())
66
+ if not market:
67
+ return None
68
+ return self._parse_market(market, coin.upper())
69
+
70
+ def get_market_by_slug(self, slug: str) -> MarketInfo | None:
71
+ """
72
+ 通过 slug 精确查询市场。
73
+
74
+ 参数:
75
+ slug: 市场 slug,例如 "eth-updown-15m-1775035200"
76
+
77
+ 返回:
78
+ MarketInfo 对象,未找到时返回 None
79
+ """
80
+ coin = self._coin_from_slug(slug)
81
+ raw = self._fetch_by_slug(slug)
82
+ if not raw:
83
+ return None
84
+ return self._parse_market(raw, coin)
85
+
86
+ def list_recent_markets(self, coin: str, n: int = 10) -> list[MarketInfo]:
87
+ """
88
+ 列出某币种最近 n 个市场(从当前窗口往前推算)。
89
+
90
+ 参数:
91
+ coin: 币种符号
92
+ n: 返回的市场数量
93
+
94
+ 返回:
95
+ MarketInfo 列表(按时间从新到旧排列)
96
+ """
97
+ coin = coin.upper()
98
+ self._check_coin(coin)
99
+ prefix = self.COIN_SLUGS[coin]
100
+
101
+ now = datetime.now(timezone.utc)
102
+ minute = (now.minute // 15) * 15
103
+ current_ts = int(now.replace(minute=minute, second=0, microsecond=0).timestamp())
104
+
105
+ results: list[MarketInfo] = []
106
+ ts = current_ts
107
+ while len(results) < n:
108
+ slug = f"{prefix}-{ts}"
109
+ raw = self._fetch_by_slug(slug)
110
+ if raw:
111
+ results.append(self._parse_market(raw, coin))
112
+ ts -= 900 # 往前 15 分钟
113
+ if ts < current_ts - 900 * 200: # 最多往前 200 个窗口(约 2 天)
114
+ break
115
+
116
+ return results
117
+
118
+ # ------------------------------------------------------------------
119
+ # 内部方法
120
+ # ------------------------------------------------------------------
121
+
122
+ def _get_active_market(self, coin: str) -> dict[str, Any] | None:
123
+ """尝试当前、下一个、上一个窗口,找到 acceptingOrders 的市场。"""
124
+ self._check_coin(coin)
125
+ prefix = self.COIN_SLUGS[coin]
126
+
127
+ now = datetime.now(timezone.utc)
128
+ minute = (now.minute // 15) * 15
129
+ current_ts = int(now.replace(minute=minute, second=0, microsecond=0).timestamp())
130
+
131
+ for offset in [0, 900, -900]:
132
+ ts = current_ts + offset
133
+ raw = self._fetch_by_slug(f"{prefix}-{ts}")
134
+ if raw and raw.get("acceptingOrders"):
135
+ return raw
136
+ return None
137
+
138
+ def _fetch_by_slug(self, slug: str, retries: int = 3) -> dict[str, Any] | None:
139
+ """GET /markets/slug/{slug},SSL 错误时自动重试。"""
140
+ url = f"{self.base_url}/markets/slug/{slug}"
141
+ for attempt in range(retries):
142
+ try:
143
+ resp = self._session.get(url, timeout=self.timeout)
144
+ if resp.status_code == 200:
145
+ return resp.json()
146
+ return None
147
+ except Exception as e:
148
+ err = str(e).lower()
149
+ is_network = any(k in err for k in ("ssl", "eof", "tls", "connection"))
150
+ if is_network and attempt < retries - 1:
151
+ time.sleep(0.5 * (attempt + 1))
152
+ continue
153
+ return None
154
+ return None
155
+
156
+ def _parse_market(self, raw: dict[str, Any], coin: str) -> MarketInfo:
157
+ """将原始 API 响应解析为 MarketInfo。"""
158
+ outcomes = self._parse_json_field(raw.get("outcomes", '["Up", "Down"]'))
159
+ token_ids_raw = self._parse_json_field(raw.get("clobTokenIds", "[]"))
160
+ prices_raw = self._parse_json_field(raw.get("outcomePrices", '["0.5", "0.5"]'))
161
+
162
+ token_ids = {str(o).lower(): str(v) for o, v in zip(outcomes, token_ids_raw)}
163
+ prices = {str(o).lower(): float(v) for o, v in zip(outcomes, prices_raw)}
164
+
165
+ return MarketInfo(
166
+ slug=raw.get("slug", ""),
167
+ question=raw.get("question", ""),
168
+ coin=coin,
169
+ end_date=raw.get("endDate", ""),
170
+ token_ids=token_ids,
171
+ prices=prices,
172
+ accepting_orders=bool(raw.get("acceptingOrders", False)),
173
+ raw=raw,
174
+ )
175
+
176
+ @staticmethod
177
+ def _parse_json_field(value: Any) -> list:
178
+ if isinstance(value, str):
179
+ return json.loads(value)
180
+ return list(value)
181
+
182
+ def _check_coin(self, coin: str) -> None:
183
+ if coin not in self.COIN_SLUGS:
184
+ raise ValueError(
185
+ f"不支持的币种: {coin!r}。支持: {self.SUPPORTED_COINS}"
186
+ )
187
+
188
+ @staticmethod
189
+ def _coin_from_slug(slug: str) -> str:
190
+ slug_lower = slug.lower()
191
+ for coin, prefix in GammaClient.COIN_SLUGS.items():
192
+ if slug_lower.startswith(prefix):
193
+ return coin
194
+ return "UNKNOWN"
@@ -0,0 +1,69 @@
1
+ """
2
+ Polymarket API 数据模型
3
+
4
+ 定义 API 调用的输入输出数据结构。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+
11
+
12
+ @dataclass
13
+ class MarketInfo:
14
+ """Polymarket 市场信息。"""
15
+
16
+ slug: str
17
+ question: str
18
+ coin: str
19
+ end_date: str
20
+ token_ids: dict[str, str] # {"up": "token_id", "down": "token_id"}
21
+ prices: dict[str, float] # {"up": 0.52, "down": 0.48}
22
+ accepting_orders: bool
23
+ raw: dict = field(default_factory=dict, repr=False)
24
+
25
+ @property
26
+ def up_token_id(self) -> str | None:
27
+ return self.token_ids.get("up")
28
+
29
+ @property
30
+ def down_token_id(self) -> str | None:
31
+ return self.token_ids.get("down")
32
+
33
+ @property
34
+ def up_price(self) -> float:
35
+ return self.prices.get("up", 0.5)
36
+
37
+ @property
38
+ def down_price(self) -> float:
39
+ return self.prices.get("down", 0.5)
40
+
41
+
42
+ @dataclass
43
+ class PricePoint:
44
+ """单个历史价格点。"""
45
+
46
+ timestamp: int # Unix UTC
47
+ price: float
48
+
49
+ def __repr__(self) -> str:
50
+ return f"PricePoint(t={self.timestamp}, p={self.price:.4f})"
51
+
52
+
53
+ @dataclass
54
+ class OddsHistory:
55
+ """某 token 的赔率历史时间序列。"""
56
+
57
+ token_id: str
58
+ interval: str
59
+ fidelity: int # 分辨率(分钟)
60
+ points: list[PricePoint] = field(default_factory=list)
61
+
62
+ def __len__(self) -> int:
63
+ return len(self.points)
64
+
65
+ def prices(self) -> list[float]:
66
+ return [p.price for p in self.points]
67
+
68
+ def timestamps(self) -> list[int]:
69
+ return [p.timestamp for p in self.points]
@@ -0,0 +1,21 @@
1
+ """策略回测评估模块。"""
2
+
3
+ from .metrics import (
4
+ calmar_ratio,
5
+ max_drawdown,
6
+ profit_factor,
7
+ sharpe_ratio,
8
+ summary,
9
+ summary_from_df,
10
+ win_rate,
11
+ )
12
+
13
+ __all__ = [
14
+ "sharpe_ratio",
15
+ "max_drawdown",
16
+ "win_rate",
17
+ "profit_factor",
18
+ "calmar_ratio",
19
+ "summary",
20
+ "summary_from_df",
21
+ ]