@dshtrading/connector-polymarket 0.6.2 → 0.7.0
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.
- package/README.md +26 -3
- package/lib/clob.d.ts +73 -0
- package/lib/clob.js +122 -0
- package/lib/discovery.js +15 -16
- package/lib/errors.d.ts +11 -1
- package/lib/errors.js +14 -1
- package/lib/events.d.ts +97 -0
- package/lib/events.js +165 -0
- package/lib/gamma.d.ts +15 -2
- package/lib/gamma.js +19 -9
- package/lib/index.d.ts +7 -4
- package/lib/index.js +7 -4
- package/lib/json.js +63 -0
- package/lib/plugin.d.ts +9 -3
- package/lib/plugin.js +18 -3
- package/lib/service.d.ts +124 -6
- package/lib/service.js +200 -12
- package/lib/tools.d.ts +12 -0
- package/lib/tools.js +205 -0
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# @dshtrading/connector-polymarket
|
|
2
2
|
|
|
3
3
|
Polymarket 预测市场连接器(**只读**公开 API,无密钥)。v1 只覆盖 **FOMC 决议路径**:
|
|
4
|
-
|
|
4
|
+
给出一场会议的各档位标签与隐含概率。同一份取数服务供两处消费——右侧栏预测市场面板
|
|
5
|
+
(经桥 `/dshtrading/api/prediction/fomc`)与三个 agent 只读工具(见下);**没有第二份取数逻辑**。
|
|
5
6
|
|
|
6
7
|
判据与口径的唯一来源是 [spikes/impl-polymarket/EVIDENCE.md](../../spikes/impl-polymarket/EVIDENCE.md)
|
|
7
8
|
(P0 spike 裁决)。**不要另起一套猜法**——发现规则与概率口径都在那份证据里钉死了。
|
|
@@ -11,13 +12,35 @@ Polymarket 预测市场连接器(**只读**公开 API,无密钥)。v1 只
|
|
|
11
12
|
- `src/gamma.ts` —— gamma HTTP 传输层。固定自有 UA(实测 200;`Python-urllib` 会触发
|
|
12
13
|
Cloudflare 1010 → 403 且响应体是 text/plain)、显式超时、三层失败语义
|
|
13
14
|
(传输 → `TRADING_NETWORK`;4xx/5xx → `TRADING_UPSTREAM_ERROR`;200 非 JSON → 契约破损)。
|
|
15
|
+
- `src/json.ts` —— 上游 JSON 取值原语(字符串化数组、字符串数字、空串判定),发现面与
|
|
16
|
+
事件面共用同一份口径。
|
|
14
17
|
- `src/discovery.ts` —— 发现规则 L0..L5(`series_id=35` → `series[].id` 校验 → 标题正则
|
|
15
18
|
→ `negRisk` + 档位数 4..6 → 噪声排除 → 按 id 去重/`endDate` 升序)与档位归一化。
|
|
16
19
|
纯函数,可离线复核历史证据。
|
|
20
|
+
- `src/events.ts` —— 事件读取面:关键词检索(单页本地过滤)与单事件详情,纯函数。
|
|
17
21
|
- `src/snapshot.ts` —— 本地快照(存 trading home)。只回答「自上次观察变了多少」,
|
|
18
22
|
缺失时 `changeSincePrevious` **缺省而不是 0**。
|
|
19
|
-
- `src/service.ts` —— 取数服务 + host 面服务对象(`tradingPredictionFeed
|
|
20
|
-
- `src/
|
|
23
|
+
- `src/service.ts` —— 取数服务 + host 面服务对象(`tradingPredictionFeed`)+ 工具面方法。
|
|
24
|
+
- `src/tools.ts` —— agent 工具面(三个只读工具)。
|
|
25
|
+
- `src/plugin.ts` —— 插件入口(`inject: ['tools']`;provide 服务并注册工具)。
|
|
26
|
+
|
|
27
|
+
## agent 工具面
|
|
28
|
+
|
|
29
|
+
| 工具 | 入参 | 说明 |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `prediction_fomc_meetings` | `meetingId?`(id 或 slug) | FOMC 档位与隐含概率(含已结算),每档带 `changeSincePrevious`(有历史快照时才有,缺失 = 不知道) |
|
|
32
|
+
| `prediction_search_events` | `keyword`(必填)、`limit?`(1..50,默认 20) | 关键词列出预测市场事件 |
|
|
33
|
+
| `prediction_event_detail` | `eventId`(必填,id 或 slug) | 单事件详情:各档 `outcomes`/`outcomePrices`/报价/流动性/`negRisk` |
|
|
34
|
+
|
|
35
|
+
三个工具每次调用只发**一次** `GET /events`,出参一律带 `observedAt`(取数时刻)、
|
|
36
|
+
`source`(读取面)、`stale`(`cf-cache-status: HIT` = 拿到的是最多 5 分钟前的 CDN 副本;
|
|
37
|
+
`false` 的含义是「没有观察到缓存命中」,不是「已证明实时」)。
|
|
38
|
+
|
|
39
|
+
**上游 `/events` 没有关键词参数**(`q=` / `title=` 被忽略,默认排序是 event id 升序),
|
|
40
|
+
因此检索是「一次拉 100 条开盘中 24h 成交额降序的事件 + 本地过滤」,出参显式标注
|
|
41
|
+
`filtering: "local-page-filter"`、`pageLimit`、`scanned`、`matched`(截断前命中数)。
|
|
42
|
+
命中面是事件**标题与 slug**,不含档位标签(否则 `fed` 会命中人名 `Federico`)。
|
|
43
|
+
实测与原始响应:[spikes/impl-polymarket/agent-tools-ACCEPTANCE.md](../../spikes/impl-polymarket/agent-tools-ACCEPTANCE.md)。
|
|
21
44
|
|
|
22
45
|
## 只读边界(硬约束)
|
|
23
46
|
|
package/lib/clob.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { PredictionPricePoint } from "@dshtrading/api";
|
|
2
|
+
//#region src/clob.d.ts
|
|
3
|
+
declare const CLOB_BASE_URL = "https://clob.polymarket.com";
|
|
4
|
+
/**
|
|
5
|
+
* 历史窗口上限 = 336 小时(Q3.2 实测:168h/336h 可查,720h 直接报窗口过长)。
|
|
6
|
+
*
|
|
7
|
+
* 桥侧(`PREDICTION_HISTORY_MAX_WINDOW_MS`)有一道同值闸门,本连接器再拦一次:桥是
|
|
8
|
+
* 可选面(HTTP 客户端可能绕过它),而**必然失败的长窗口不该被发到上游**。
|
|
9
|
+
*/
|
|
10
|
+
declare const PRICE_HISTORY_MAX_WINDOW_MS: number;
|
|
11
|
+
/**
|
|
12
|
+
* fidelity 缺省 = 5 分钟。
|
|
13
|
+
*
|
|
14
|
+
* 依据:面板「事件对照」的对照粒度是事件时点(CPI/PPI 公布时刻),5 分钟足以把公布
|
|
15
|
+
* 前后几小时的变动读出来;同时上游对 1w 及以上窗口要求 `fidelity >= 5`
|
|
16
|
+
* (Q3.1:`interval=1w` 原文 `minimum 'fidelity' for '1w' range is 5`),取 5 不会在
|
|
17
|
+
* 任何合法窗口上被上游拒绝。体积实测(EVIDENCE Q3.6,2026-10-20):24h/5min 一档
|
|
18
|
+
* **289 点、约 8 KB**;336h/5min 一档 **3969 点**,仍在单次响应的可接受体积内。
|
|
19
|
+
*/
|
|
20
|
+
declare const DEFAULT_PRICE_HISTORY_FIDELITY_MINUTES = 5;
|
|
21
|
+
interface ClobTransportOptions {
|
|
22
|
+
/** 单次请求超时(ms),缺省与 gamma 相同的 10s(`DEFAULT_TIMEOUT_MS`)。 */
|
|
23
|
+
readonly timeoutMs?: number;
|
|
24
|
+
/** 注入的 fetch(测试用契约化替身;缺省 globalThis.fetch)。 */
|
|
25
|
+
readonly fetchImpl?: typeof globalThis.fetch;
|
|
26
|
+
/** 基址覆盖(测试指向本地夹具服务用)。 */
|
|
27
|
+
readonly baseUrl?: string;
|
|
28
|
+
/** UA 覆盖。 */
|
|
29
|
+
readonly userAgent?: string;
|
|
30
|
+
}
|
|
31
|
+
interface ClobPriceHistoryRequest {
|
|
32
|
+
/** CLOB token(来自 gamma market 的 `clobTokenIds[0]`;公开数据,非凭据)。 */
|
|
33
|
+
readonly token: string;
|
|
34
|
+
/** 窗口起点(epoch ms;本层换算成上游的 unix 秒)。 */
|
|
35
|
+
readonly fromMs: number;
|
|
36
|
+
/** 窗口终点(epoch ms)。 */
|
|
37
|
+
readonly toMs: number;
|
|
38
|
+
/** 粒度(分钟);缺省 {@link DEFAULT_PRICE_HISTORY_FIDELITY_MINUTES}。 */
|
|
39
|
+
readonly fidelityMinutes?: number;
|
|
40
|
+
}
|
|
41
|
+
/** 一档的窗口内读数(`t` 为上游 unix 秒,`p` 为上游 midpoint 概率,均原样)。 */
|
|
42
|
+
interface ClobPriceHistory {
|
|
43
|
+
readonly points: readonly PredictionPricePoint[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 发请求前的窗口/粒度闸门(服务层与本层共用:服务层用它做到「非法请求零请求」)。
|
|
47
|
+
*
|
|
48
|
+
* 违约抛 `TRADING_UNSUPPORTED_INTERVAL`:上游对长窗口的原文就是 `… interval is too long`、
|
|
49
|
+
* 对非法粒度是 `minimum 'fidelity' for '1w' range is 5`——两者都是「这个区间/粒度我不服务」,
|
|
50
|
+
* 与 `TRADING_UPSTREAM_ERROR`(请求发出去了、上游报错)区分开:这里**一次请求都没发**。
|
|
51
|
+
*/
|
|
52
|
+
declare function assertPriceHistoryRange(fromMs: number, toMs: number, fidelityMinutes: number): void;
|
|
53
|
+
/**
|
|
54
|
+
* CLOB 只读传输层:一次 `GET /prices-history` = 一档一个窗口。
|
|
55
|
+
*
|
|
56
|
+
* 失败语义三层(**全部抛结构化错误,绝不返回空数组冒充「没有变动」**):
|
|
57
|
+
* - 传输层(DNS/TLS/超时)→ `TRADING_NETWORK`;
|
|
58
|
+
* - HTTP 非 2xx(403 的 text/plain 就是 Cloudflare 1010)→ `TRADING_UPSTREAM_ERROR`,
|
|
59
|
+
* 消息带状态码与响应片段;
|
|
60
|
+
* - 200 但体不是 JSON / 没有 `history` 数组 → `TRADING_UPSTREAM_ERROR`(契约破损)。
|
|
61
|
+
* 唯一不抛的「空」是 **200 + `{"history":[]}`**:那是上游的真实回答(见文件头第 4 条)。
|
|
62
|
+
*/
|
|
63
|
+
declare class ClobClient {
|
|
64
|
+
private readonly timeoutMs;
|
|
65
|
+
private readonly fetchImpl;
|
|
66
|
+
private readonly baseUrl;
|
|
67
|
+
private readonly userAgent;
|
|
68
|
+
constructor(options?: ClobTransportOptions);
|
|
69
|
+
/** 取一档在 [fromMs, toMs] 内的历史读数(按 `t` 升序;空 = 上游没有读数,不是失败)。 */
|
|
70
|
+
priceHistory(request: ClobPriceHistoryRequest): Promise<ClobPriceHistory>;
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
export { CLOB_BASE_URL, ClobClient, ClobPriceHistory, ClobPriceHistoryRequest, ClobTransportOptions, DEFAULT_PRICE_HISTORY_FIDELITY_MINUTES, PRICE_HISTORY_MAX_WINDOW_MS, assertPriceHistoryRange };
|
package/lib/clob.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { PolymarketError, bodySnippet, networkError } from "./errors.js";
|
|
2
|
+
import "./gamma.js";
|
|
3
|
+
//#region src/clob.ts
|
|
4
|
+
const CLOB_BASE_URL = "https://clob.polymarket.com";
|
|
5
|
+
/**
|
|
6
|
+
* 历史窗口上限 = 336 小时(Q3.2 实测:168h/336h 可查,720h 直接报窗口过长)。
|
|
7
|
+
*
|
|
8
|
+
* 桥侧(`PREDICTION_HISTORY_MAX_WINDOW_MS`)有一道同值闸门,本连接器再拦一次:桥是
|
|
9
|
+
* 可选面(HTTP 客户端可能绕过它),而**必然失败的长窗口不该被发到上游**。
|
|
10
|
+
*/
|
|
11
|
+
const PRICE_HISTORY_MAX_WINDOW_MS = 12096e5;
|
|
12
|
+
/**
|
|
13
|
+
* fidelity 缺省 = 5 分钟。
|
|
14
|
+
*
|
|
15
|
+
* 依据:面板「事件对照」的对照粒度是事件时点(CPI/PPI 公布时刻),5 分钟足以把公布
|
|
16
|
+
* 前后几小时的变动读出来;同时上游对 1w 及以上窗口要求 `fidelity >= 5`
|
|
17
|
+
* (Q3.1:`interval=1w` 原文 `minimum 'fidelity' for '1w' range is 5`),取 5 不会在
|
|
18
|
+
* 任何合法窗口上被上游拒绝。体积实测(EVIDENCE Q3.6,2026-10-20):24h/5min 一档
|
|
19
|
+
* **289 点、约 8 KB**;336h/5min 一档 **3969 点**,仍在单次响应的可接受体积内。
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_PRICE_HISTORY_FIDELITY_MINUTES = 5;
|
|
22
|
+
/**
|
|
23
|
+
* 发请求前的窗口/粒度闸门(服务层与本层共用:服务层用它做到「非法请求零请求」)。
|
|
24
|
+
*
|
|
25
|
+
* 违约抛 `TRADING_UNSUPPORTED_INTERVAL`:上游对长窗口的原文就是 `… interval is too long`、
|
|
26
|
+
* 对非法粒度是 `minimum 'fidelity' for '1w' range is 5`——两者都是「这个区间/粒度我不服务」,
|
|
27
|
+
* 与 `TRADING_UPSTREAM_ERROR`(请求发出去了、上游报错)区分开:这里**一次请求都没发**。
|
|
28
|
+
*/
|
|
29
|
+
function assertPriceHistoryRange(fromMs, toMs, fidelityMinutes) {
|
|
30
|
+
if (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || fromMs >= toMs) throw new PolymarketError("TRADING_UNSUPPORTED_INTERVAL", `polymarket clob /prices-history: window must satisfy fromMs < toMs (got fromMs=${fromMs}, toMs=${toMs})`);
|
|
31
|
+
if (toMs - fromMs > 12096e5) throw new PolymarketError("TRADING_UNSUPPORTED_INTERVAL", `polymarket clob /prices-history: window ${toMs - fromMs}ms exceeds the upstream limit of ${PRICE_HISTORY_MAX_WINDOW_MS}ms (336h) — not sent`);
|
|
32
|
+
if (!Number.isFinite(fidelityMinutes) || fidelityMinutes <= 0) throw new PolymarketError("TRADING_UNSUPPORTED_INTERVAL", `polymarket clob /prices-history: fidelityMinutes must be a positive number of minutes (got ${fidelityMinutes}) — not sent`);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* CLOB 只读传输层:一次 `GET /prices-history` = 一档一个窗口。
|
|
36
|
+
*
|
|
37
|
+
* 失败语义三层(**全部抛结构化错误,绝不返回空数组冒充「没有变动」**):
|
|
38
|
+
* - 传输层(DNS/TLS/超时)→ `TRADING_NETWORK`;
|
|
39
|
+
* - HTTP 非 2xx(403 的 text/plain 就是 Cloudflare 1010)→ `TRADING_UPSTREAM_ERROR`,
|
|
40
|
+
* 消息带状态码与响应片段;
|
|
41
|
+
* - 200 但体不是 JSON / 没有 `history` 数组 → `TRADING_UPSTREAM_ERROR`(契约破损)。
|
|
42
|
+
* 唯一不抛的「空」是 **200 + `{"history":[]}`**:那是上游的真实回答(见文件头第 4 条)。
|
|
43
|
+
*/
|
|
44
|
+
var ClobClient = class {
|
|
45
|
+
timeoutMs;
|
|
46
|
+
fetchImpl;
|
|
47
|
+
baseUrl;
|
|
48
|
+
userAgent;
|
|
49
|
+
constructor(options = {}) {
|
|
50
|
+
this.timeoutMs = options.timeoutMs ?? 1e4;
|
|
51
|
+
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
52
|
+
this.baseUrl = options.baseUrl ?? "https://clob.polymarket.com";
|
|
53
|
+
this.userAgent = options.userAgent ?? "dsh-trading-connector-polymarket/0.6.1";
|
|
54
|
+
}
|
|
55
|
+
/** 取一档在 [fromMs, toMs] 内的历史读数(按 `t` 升序;空 = 上游没有读数,不是失败)。 */
|
|
56
|
+
async priceHistory(request) {
|
|
57
|
+
const token = request.token.trim();
|
|
58
|
+
if (token === "") throw new PolymarketError("TRADING_UNSUPPORTED_SYMBOL", "polymarket clob /prices-history: empty clob token id — no request sent");
|
|
59
|
+
const fidelityMinutes = request.fidelityMinutes ?? 5;
|
|
60
|
+
assertPriceHistoryRange(request.fromMs, request.toMs, fidelityMinutes);
|
|
61
|
+
const url = new URL("/prices-history", this.baseUrl);
|
|
62
|
+
url.searchParams.set("market", token);
|
|
63
|
+
url.searchParams.set("startTs", String(Math.floor(request.fromMs / 1e3)));
|
|
64
|
+
url.searchParams.set("endTs", String(Math.floor(request.toMs / 1e3)));
|
|
65
|
+
url.searchParams.set("fidelity", String(fidelityMinutes));
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timer = setTimeout(() => {
|
|
68
|
+
controller.abort();
|
|
69
|
+
}, this.timeoutMs);
|
|
70
|
+
let response;
|
|
71
|
+
try {
|
|
72
|
+
response = await this.fetchImpl(url, {
|
|
73
|
+
method: "GET",
|
|
74
|
+
headers: {
|
|
75
|
+
"user-agent": this.userAgent,
|
|
76
|
+
accept: "application/json"
|
|
77
|
+
},
|
|
78
|
+
signal: controller.signal
|
|
79
|
+
});
|
|
80
|
+
} catch (cause) {
|
|
81
|
+
throw networkError(`polymarket clob ${url.pathname}`, cause);
|
|
82
|
+
} finally {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
}
|
|
85
|
+
const text = await response.text();
|
|
86
|
+
if (!response.ok) throw new PolymarketError("TRADING_UPSTREAM_ERROR", `polymarket clob ${url.pathname}: HTTP ${response.status} — ${bodySnippet(text)}`);
|
|
87
|
+
let body;
|
|
88
|
+
try {
|
|
89
|
+
body = JSON.parse(text);
|
|
90
|
+
} catch {
|
|
91
|
+
throw new PolymarketError("TRADING_UPSTREAM_ERROR", `polymarket clob ${url.pathname}: HTTP ${response.status} but the body is not JSON — ${bodySnippet(text)}`);
|
|
92
|
+
}
|
|
93
|
+
return { points: readPoints(url.pathname, body, text) };
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* `{"history":[…]}` → 读数数组。
|
|
98
|
+
*
|
|
99
|
+
* - 缺 `history` 数组 = 契约破损(抛错)——`{"history":[]}` 才是合法的空;
|
|
100
|
+
* - 逐条丢弃非法读数(`t` 非有限数、`p` 非有限数或不在 0..1);**不猜、不插值、
|
|
101
|
+
* 不用相邻值补位**,少一个点就是上游没给这个点;
|
|
102
|
+
* - 只按 `t` 升序排列(上游偶有乱序),不做去重/重采样/单位换算。
|
|
103
|
+
*/
|
|
104
|
+
function readPoints(pathname, body, text) {
|
|
105
|
+
const history = typeof body === "object" && body !== null ? body.history : void 0;
|
|
106
|
+
if (!Array.isArray(history)) throw new PolymarketError("TRADING_UPSTREAM_ERROR", `polymarket clob ${pathname}: HTTP 200 but the body has no history array — ${bodySnippet(text)}`);
|
|
107
|
+
const points = [];
|
|
108
|
+
for (const row of history) {
|
|
109
|
+
if (typeof row !== "object" || row === null) continue;
|
|
110
|
+
const { t, p } = row;
|
|
111
|
+
if (typeof t !== "number" || !Number.isFinite(t)) continue;
|
|
112
|
+
if (typeof p !== "number" || !Number.isFinite(p) || p < 0 || p > 1) continue;
|
|
113
|
+
points.push({
|
|
114
|
+
t,
|
|
115
|
+
p
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
points.sort((left, right) => left.t - right.t);
|
|
119
|
+
return points;
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
export { CLOB_BASE_URL, ClobClient, DEFAULT_PRICE_HISTORY_FIDELITY_MINUTES, PRICE_HISTORY_MAX_WINDOW_MS, assertPriceHistoryRange };
|
package/lib/discovery.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord, num, parseStringArray, text } from "./json.js";
|
|
1
2
|
//#region src/discovery.ts
|
|
2
3
|
/** 判据来源系列:FOMC 决议(证据 Q1.2,39/39 命中、噪声 0)。 */
|
|
3
4
|
const FOMC_SERIES_ID = "35";
|
|
@@ -48,21 +49,6 @@ const PROBABILITY_SUM_MIN = .98;
|
|
|
48
49
|
const PROBABILITY_SUM_MAX = 1.03;
|
|
49
50
|
/** 会议日程解析(上游 description 原文:`its meeting scheduled for October 27-28, 2026`)。 */
|
|
50
51
|
const MEETING_WINDOW_RE = /meeting scheduled for ([A-Z][a-z]+ \d+(?:-\d+)?, \d{4})/;
|
|
51
|
-
function isRecord(value) {
|
|
52
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
53
|
-
}
|
|
54
|
-
function num(value) {
|
|
55
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
56
|
-
if (typeof value === "string" && value.trim() !== "") {
|
|
57
|
-
const parsed = Number(value);
|
|
58
|
-
if (Number.isFinite(parsed)) return parsed;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
function text(value) {
|
|
62
|
-
if (typeof value !== "string") return void 0;
|
|
63
|
-
const trimmed = value.trim();
|
|
64
|
-
return trimmed.length > 0 ? trimmed : void 0;
|
|
65
|
-
}
|
|
66
52
|
/** 档位标签 → 稳定键与利率变动(证据 Q1.7:标签在大小写与 `+` 上会漂移)。 */
|
|
67
53
|
function canonicalBucket(label) {
|
|
68
54
|
const value = label.trim();
|
|
@@ -93,6 +79,17 @@ function hasSeries(event) {
|
|
|
93
79
|
}
|
|
94
80
|
/** `outcomePrices` 是**字符串化的 JSON 数组**(如 `"[\"0.0035\",\"0.9965\"]"`)。 */
|
|
95
81
|
function parseOutcomePrices(value) {
|
|
82
|
+
const parsed = parseStringArray(value);
|
|
83
|
+
if (parsed === void 0 || parsed.length === 0) return void 0;
|
|
84
|
+
return num(parsed[0]);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* `clobTokenIds` → 第 0 个 token(Yes 档,与 `outcomePrices[0]` 同档)。
|
|
88
|
+
*
|
|
89
|
+
* 上游把数组装成字符串(`"[\"706…\",\"528…\"]"`),与 `outcomePrices` 同一种包装;
|
|
90
|
+
* 缺省/非字符串/不是数组/首元素不是非空字符串一律返回 undefined(不填,不猜)。
|
|
91
|
+
*/
|
|
92
|
+
function parseClobTokenId(value) {
|
|
96
93
|
if (typeof value !== "string") return void 0;
|
|
97
94
|
let parsed;
|
|
98
95
|
try {
|
|
@@ -101,7 +98,7 @@ function parseOutcomePrices(value) {
|
|
|
101
98
|
return;
|
|
102
99
|
}
|
|
103
100
|
if (!Array.isArray(parsed) || parsed.length === 0) return void 0;
|
|
104
|
-
return
|
|
101
|
+
return text(parsed[0]);
|
|
105
102
|
}
|
|
106
103
|
function parseBucket(market) {
|
|
107
104
|
const label = text(market.groupItemTitle);
|
|
@@ -114,6 +111,7 @@ function parseBucket(market) {
|
|
|
114
111
|
const volume = num(market.volumeNum);
|
|
115
112
|
const volume24h = num(market.volume24hr);
|
|
116
113
|
const tickSize = num(market.orderPriceMinTickSize);
|
|
114
|
+
const clobTokenId = parseClobTokenId(market.clobTokenIds);
|
|
117
115
|
return {
|
|
118
116
|
key,
|
|
119
117
|
label,
|
|
@@ -126,6 +124,7 @@ function parseBucket(market) {
|
|
|
126
124
|
...volume !== void 0 ? { volume } : {},
|
|
127
125
|
...volume24h !== void 0 ? { volume24h } : {},
|
|
128
126
|
...tickSize !== void 0 ? { tickSize } : {},
|
|
127
|
+
...clobTokenId !== void 0 ? { clobTokenId } : {},
|
|
129
128
|
acceptingOrders: market.acceptingOrders === true,
|
|
130
129
|
closed: market.closed === true
|
|
131
130
|
};
|
package/lib/errors.d.ts
CHANGED
|
@@ -18,5 +18,15 @@ declare class PolymarketError extends Error {
|
|
|
18
18
|
* 「失败路径不产生空数据假象」的第一道闸门。
|
|
19
19
|
*/
|
|
20
20
|
declare function bodySnippet(text: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* 传输层失败(DNS/TLS/连接/超时)→ `TRADING_NETWORK`。
|
|
23
|
+
*
|
|
24
|
+
* 只在**请求没拿到响应**时调用;拿到响应但状态非 2xx 或体破损一律是
|
|
25
|
+
* `TRADING_UPSTREAM_ERROR`(由调用方构造,消息带状态码与响应片段)——这条分界是
|
|
26
|
+
* 「上游说不行」与「我们连不上」的区别,重试策略完全不同。
|
|
27
|
+
*
|
|
28
|
+
* 两个传输层(gamma / CLOB)共用这一处措辞,避免同一语义出现两种说法。
|
|
29
|
+
*/
|
|
30
|
+
declare function networkError(scope: string, cause: unknown): PolymarketError;
|
|
21
31
|
//#endregion
|
|
22
|
-
export { PolymarketError, bodySnippet };
|
|
32
|
+
export { PolymarketError, bodySnippet, networkError };
|
package/lib/errors.js
CHANGED
|
@@ -25,5 +25,18 @@ function bodySnippet(text) {
|
|
|
25
25
|
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
26
26
|
return collapsed.length > 200 ? `${collapsed.slice(0, 200)}…` : collapsed;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* 传输层失败(DNS/TLS/连接/超时)→ `TRADING_NETWORK`。
|
|
30
|
+
*
|
|
31
|
+
* 只在**请求没拿到响应**时调用;拿到响应但状态非 2xx 或体破损一律是
|
|
32
|
+
* `TRADING_UPSTREAM_ERROR`(由调用方构造,消息带状态码与响应片段)——这条分界是
|
|
33
|
+
* 「上游说不行」与「我们连不上」的区别,重试策略完全不同。
|
|
34
|
+
*
|
|
35
|
+
* 两个传输层(gamma / CLOB)共用这一处措辞,避免同一语义出现两种说法。
|
|
36
|
+
*/
|
|
37
|
+
function networkError(scope, cause) {
|
|
38
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
39
|
+
return new PolymarketError("TRADING_NETWORK", `${scope}: ${cause instanceof Error && (cause.name === "TimeoutError" || cause.name === "AbortError") ? "request timed out" : "network failure"} — ${reason}`, cause);
|
|
40
|
+
}
|
|
28
41
|
//#endregion
|
|
29
|
-
export { PolymarketError, bodySnippet };
|
|
42
|
+
export { PolymarketError, bodySnippet, networkError };
|
package/lib/events.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
//#region src/events.d.ts
|
|
2
|
+
/** 一个档位(市场)的规范化读数。 */
|
|
3
|
+
interface EventBucketQuote {
|
|
4
|
+
/** 稳定档位键(如 `no_change` / `dec_50plus`;未识别形态 = `raw:<原文>`)。 */
|
|
5
|
+
readonly key: string;
|
|
6
|
+
/** 展示标签原文(`groupItemTitle`,缺失时退到 `outcomes[0]`)。 */
|
|
7
|
+
readonly label: string;
|
|
8
|
+
/** 隐含概率 = `outcomePrices[0]`;解析不出则缺省(不猜)。 */
|
|
9
|
+
readonly probability?: number | undefined;
|
|
10
|
+
}
|
|
11
|
+
/** 检索结果里的一条事件(元数据 + 档位概率)。 */
|
|
12
|
+
interface EventSummary {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly title: string;
|
|
15
|
+
readonly slug: string;
|
|
16
|
+
readonly closed: boolean;
|
|
17
|
+
/** 上游 event `endDate`(ISO 8601 UTC);缺失则缺省。 */
|
|
18
|
+
readonly endDate?: string | undefined;
|
|
19
|
+
/** 累计成交额(上游 event `volume`,美元)。 */
|
|
20
|
+
readonly volume?: number | undefined;
|
|
21
|
+
/** 盘口深度(上游 event `liquidity`,美元)。 */
|
|
22
|
+
readonly liquidity?: number | undefined;
|
|
23
|
+
readonly buckets: readonly EventBucketQuote[];
|
|
24
|
+
}
|
|
25
|
+
/** 单市场详情里的一条档位/市场(保留上游原始报价维度)。 */
|
|
26
|
+
interface EventMarketQuote {
|
|
27
|
+
readonly key: string;
|
|
28
|
+
readonly label: string;
|
|
29
|
+
/** 上游 `question`(市场问句原文)。 */
|
|
30
|
+
readonly question?: string | undefined;
|
|
31
|
+
/** 上游 `outcomes` 解析后的数组(如 `["Yes","No"]`);解析不出则缺省。 */
|
|
32
|
+
readonly outcomes?: readonly string[] | undefined;
|
|
33
|
+
/** 上游 `outcomePrices` 解析后的数组(与 {@link outcomes} 同序);解析不出则缺省。 */
|
|
34
|
+
readonly outcomePrices?: readonly number[] | undefined;
|
|
35
|
+
/** 最优买价(结算后上游为 null → 缺省)。 */
|
|
36
|
+
readonly bestBid?: number | undefined;
|
|
37
|
+
/** 最优卖价。 */
|
|
38
|
+
readonly bestAsk?: number | undefined;
|
|
39
|
+
/** 价差(上游 `spread`,缺省时由 bestAsk − bestBid 推得;两端缺失则缺省)。 */
|
|
40
|
+
readonly spread?: number | undefined;
|
|
41
|
+
/** 盘口深度(上游 `liquidityNum`,美元)。 */
|
|
42
|
+
readonly liquidity?: number | undefined;
|
|
43
|
+
/** 累计成交额(上游 `volumeNum`,美元)。 */
|
|
44
|
+
readonly volume?: number | undefined;
|
|
45
|
+
/** 该市场是否已结算(上游 market `closed`)。 */
|
|
46
|
+
readonly closed: boolean;
|
|
47
|
+
/** 是否仍可下单(上游 market `acceptingOrders`;**不用 `active` 判断**)。 */
|
|
48
|
+
readonly acceptingOrders: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** 单个事件的详情。 */
|
|
51
|
+
interface EventDetail {
|
|
52
|
+
readonly id: string;
|
|
53
|
+
readonly slug: string;
|
|
54
|
+
readonly title: string;
|
|
55
|
+
readonly closed: boolean;
|
|
56
|
+
readonly endDate?: string | undefined;
|
|
57
|
+
/** 上游 event `negRisk`(互斥档位集合标记)。 */
|
|
58
|
+
readonly negRisk: boolean;
|
|
59
|
+
readonly volume?: number | undefined;
|
|
60
|
+
readonly liquidity?: number | undefined;
|
|
61
|
+
readonly markets: readonly EventMarketQuote[];
|
|
62
|
+
}
|
|
63
|
+
/** 一页事件的关键词命中统计(`matched` 是**截断前**的命中数)。 */
|
|
64
|
+
interface EventSearchPage {
|
|
65
|
+
readonly scanned: number;
|
|
66
|
+
readonly matched: number;
|
|
67
|
+
readonly events: readonly EventSummary[];
|
|
68
|
+
}
|
|
69
|
+
/** 单条上游事件 → 检索摘要;缺 id/title/slug 的事件不算一条(不编)。 */
|
|
70
|
+
declare function parseEventSummary(event: unknown): EventSummary | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* 关键词命中:事件标题或 slug(大小写不敏感子串)。
|
|
73
|
+
*
|
|
74
|
+
* **不搜档位标签**(2026-10-15 实测决定的边界):把档位标签算进命中面后,
|
|
75
|
+
* `fed` 会命中 `Ballon d'Or Winner 2026`——它的某个候选人是 `Federico ...`,
|
|
76
|
+
* 子串 `fed` 合法命中。检索工具的价值在精度,「命中了一个人名」对问「有没有
|
|
77
|
+
* 美联储相关盘口」的调用方是纯噪声。档位标签依然逐条回给调用方(`buckets`),
|
|
78
|
+
* 只是不参与匹配。
|
|
79
|
+
*/
|
|
80
|
+
declare function matchesKeyword(event: EventSummary, keyword: string): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* 对**一页**上游事件做本地关键词过滤(上游没有检索参数,见本模块头注)。
|
|
83
|
+
*
|
|
84
|
+
* 非数组体抛 TypeError 而不返回空结果:上游把「参数错」和「真的没有数据」都表现成
|
|
85
|
+
* 200(证据 Q5.2),这里只能在体形状上守边界。
|
|
86
|
+
*/
|
|
87
|
+
declare function searchEventsInPage(payload: unknown, keyword: string, limit: number): EventSearchPage;
|
|
88
|
+
/**
|
|
89
|
+
* 单事件响应体 → 详情。
|
|
90
|
+
*
|
|
91
|
+
* `id=` / `slug=` 是精确匹配(实测最多一条),所以取首条可解析记录;
|
|
92
|
+
* 返回 undefined = 上游 200 但空集(**没有这个事件**,调用方抛 TRADING_SYMBOL_NOT_FOUND),
|
|
93
|
+
* 与「上游故障」是两件事,调用方不得把前者读成后者或反过来。
|
|
94
|
+
*/
|
|
95
|
+
declare function parseEventDetail(payload: unknown): EventDetail | undefined;
|
|
96
|
+
//#endregion
|
|
97
|
+
export { EventBucketQuote, EventDetail, EventMarketQuote, EventSearchPage, EventSummary, matchesKeyword, parseEventDetail, parseEventSummary, searchEventsInPage };
|
package/lib/events.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { isRecord, num, parseNumberArray, parseTextArray, text } from "./json.js";
|
|
2
|
+
import { canonicalBucket } from "./discovery.js";
|
|
3
|
+
//#region src/events.ts
|
|
4
|
+
/**
|
|
5
|
+
* 事件读取面:关键词检索与单事件详情——两条都只走 `GET /events` **一次**。
|
|
6
|
+
*
|
|
7
|
+
* 上游约束(2026-10-15 实测,原始证据 spikes/impl-polymarket/agent-tools-ACCEPTANCE.md):
|
|
8
|
+
*
|
|
9
|
+
* 1. **`/events` 没有关键词检索参数**:`q=fed` 与 `title=fed` 返回与无参数**逐条相同**
|
|
10
|
+
* 的结果集(默认排序 = event id 升序,头几条是 2022 年的冷门体育盘),服务端过滤
|
|
11
|
+
* 不存在。因此关键词检索 = 单次拉「开盘中、24h 成交额降序」的一页,再本地过滤;
|
|
12
|
+
* 输出必须显式带 `filtering: 'local-page-filter'`,绝不冒充上游检索。
|
|
13
|
+
* 2. **`order` 默认升序**:`order=volume24hr` 单用会返回成交额 0 的盘;
|
|
14
|
+
* `order=volume24hr&ascending=false` 才是「热门前 N」(实测把 Fed Decision 排到第 2)。
|
|
15
|
+
* 3. **`id=<n>` 与 `slug=<精确 slug>` 精确匹配**:命中回长度 1 的数组,命中不到回
|
|
16
|
+
* `[]`(200)。「200 空数组」是唯一合法的「没有这个事件」形态,据此抛
|
|
17
|
+
* TRADING_SYMBOL_NOT_FOUND,不与上游故障混淆。
|
|
18
|
+
*
|
|
19
|
+
* 档位标签口径与 FOMC 面同源(discovery.ts 的 `canonicalBucket`):`groupItemTitle`
|
|
20
|
+
* 优先,缺失时退到 `outcomes[0]`(二元盘就是 `Yes`/`No`)——两者都不存在才丢弃该档,
|
|
21
|
+
* 宁可少一档也不编一个概率出来。
|
|
22
|
+
*/
|
|
23
|
+
/** 档位标签:`groupItemTitle` 优先,缺失时退到首个 outcome。 */
|
|
24
|
+
function bucketLabel(market) {
|
|
25
|
+
return text(market.groupItemTitle) ?? parseTextArray(market.outcomes)?.[0];
|
|
26
|
+
}
|
|
27
|
+
function parseBucketQuote(market) {
|
|
28
|
+
if (!isRecord(market)) return void 0;
|
|
29
|
+
const label = bucketLabel(market);
|
|
30
|
+
if (label === void 0) return void 0;
|
|
31
|
+
const probability = parseNumberArray(market.outcomePrices)?.[0];
|
|
32
|
+
return {
|
|
33
|
+
key: canonicalBucket(label).key,
|
|
34
|
+
label,
|
|
35
|
+
...probability !== void 0 ? { probability } : {}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function parseMarketQuote(market) {
|
|
39
|
+
if (!isRecord(market)) return void 0;
|
|
40
|
+
const label = bucketLabel(market);
|
|
41
|
+
if (label === void 0) return void 0;
|
|
42
|
+
const question = text(market.question);
|
|
43
|
+
const outcomes = parseTextArray(market.outcomes);
|
|
44
|
+
const outcomePrices = parseNumberArray(market.outcomePrices);
|
|
45
|
+
const bestBid = num(market.bestBid);
|
|
46
|
+
const bestAsk = num(market.bestAsk);
|
|
47
|
+
const spread = num(market.spread) ?? (bestBid !== void 0 && bestAsk !== void 0 ? Number((bestAsk - bestBid).toFixed(6)) : void 0);
|
|
48
|
+
const liquidity = num(market.liquidityNum);
|
|
49
|
+
const volume = num(market.volumeNum);
|
|
50
|
+
return {
|
|
51
|
+
key: canonicalBucket(label).key,
|
|
52
|
+
label,
|
|
53
|
+
...question !== void 0 ? { question } : {},
|
|
54
|
+
...outcomes !== void 0 ? { outcomes } : {},
|
|
55
|
+
...outcomePrices !== void 0 ? { outcomePrices } : {},
|
|
56
|
+
...bestBid !== void 0 ? { bestBid } : {},
|
|
57
|
+
...bestAsk !== void 0 ? { bestAsk } : {},
|
|
58
|
+
...spread !== void 0 ? { spread } : {},
|
|
59
|
+
...liquidity !== void 0 ? { liquidity } : {},
|
|
60
|
+
...volume !== void 0 ? { volume } : {},
|
|
61
|
+
closed: market.closed === true,
|
|
62
|
+
acceptingOrders: market.acceptingOrders === true
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** 单条上游事件 → 检索摘要;缺 id/title/slug 的事件不算一条(不编)。 */
|
|
66
|
+
function parseEventSummary(event) {
|
|
67
|
+
if (!isRecord(event)) return void 0;
|
|
68
|
+
const id = text(String(event.id ?? ""));
|
|
69
|
+
const title = text(event.title);
|
|
70
|
+
const slug = text(event.slug);
|
|
71
|
+
if (id === void 0 || title === void 0 || slug === void 0) return void 0;
|
|
72
|
+
const endDate = text(event.endDate);
|
|
73
|
+
const volume = num(event.volume);
|
|
74
|
+
const liquidity = num(event.liquidity);
|
|
75
|
+
const buckets = [];
|
|
76
|
+
if (Array.isArray(event.markets)) for (const market of event.markets) {
|
|
77
|
+
const quote = parseBucketQuote(market);
|
|
78
|
+
if (quote !== void 0) buckets.push(quote);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
id,
|
|
82
|
+
title,
|
|
83
|
+
slug,
|
|
84
|
+
closed: event.closed === true,
|
|
85
|
+
...endDate !== void 0 ? { endDate } : {},
|
|
86
|
+
...volume !== void 0 ? { volume } : {},
|
|
87
|
+
...liquidity !== void 0 ? { liquidity } : {},
|
|
88
|
+
buckets
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 关键词命中:事件标题或 slug(大小写不敏感子串)。
|
|
93
|
+
*
|
|
94
|
+
* **不搜档位标签**(2026-10-15 实测决定的边界):把档位标签算进命中面后,
|
|
95
|
+
* `fed` 会命中 `Ballon d'Or Winner 2026`——它的某个候选人是 `Federico ...`,
|
|
96
|
+
* 子串 `fed` 合法命中。检索工具的价值在精度,「命中了一个人名」对问「有没有
|
|
97
|
+
* 美联储相关盘口」的调用方是纯噪声。档位标签依然逐条回给调用方(`buckets`),
|
|
98
|
+
* 只是不参与匹配。
|
|
99
|
+
*/
|
|
100
|
+
function matchesKeyword(event, keyword) {
|
|
101
|
+
return `${event.title}\n${event.slug}`.toLowerCase().includes(keyword);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 对**一页**上游事件做本地关键词过滤(上游没有检索参数,见本模块头注)。
|
|
105
|
+
*
|
|
106
|
+
* 非数组体抛 TypeError 而不返回空结果:上游把「参数错」和「真的没有数据」都表现成
|
|
107
|
+
* 200(证据 Q5.2),这里只能在体形状上守边界。
|
|
108
|
+
*/
|
|
109
|
+
function searchEventsInPage(payload, keyword, limit) {
|
|
110
|
+
if (!Array.isArray(payload)) throw new TypeError("polymarket gamma /events: expected a JSON array of events");
|
|
111
|
+
const needle = keyword.toLowerCase();
|
|
112
|
+
const summaries = [];
|
|
113
|
+
for (const event of payload) {
|
|
114
|
+
const summary = parseEventSummary(event);
|
|
115
|
+
if (summary !== void 0) summaries.push(summary);
|
|
116
|
+
}
|
|
117
|
+
const matched = summaries.filter((event) => matchesKeyword(event, needle));
|
|
118
|
+
return {
|
|
119
|
+
scanned: summaries.length,
|
|
120
|
+
matched: matched.length,
|
|
121
|
+
events: matched.slice(0, limit)
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function parseEventRecord(event) {
|
|
125
|
+
if (!isRecord(event)) return void 0;
|
|
126
|
+
const id = text(String(event.id ?? ""));
|
|
127
|
+
const title = text(event.title);
|
|
128
|
+
const slug = text(event.slug);
|
|
129
|
+
if (id === void 0 || title === void 0 || slug === void 0) return void 0;
|
|
130
|
+
const endDate = text(event.endDate);
|
|
131
|
+
const volume = num(event.volume);
|
|
132
|
+
const liquidity = num(event.liquidity);
|
|
133
|
+
const markets = [];
|
|
134
|
+
if (Array.isArray(event.markets)) for (const market of event.markets) {
|
|
135
|
+
const quote = parseMarketQuote(market);
|
|
136
|
+
if (quote !== void 0) markets.push(quote);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
id,
|
|
140
|
+
slug,
|
|
141
|
+
title,
|
|
142
|
+
closed: event.closed === true,
|
|
143
|
+
...endDate !== void 0 ? { endDate } : {},
|
|
144
|
+
negRisk: event.negRisk === true,
|
|
145
|
+
...volume !== void 0 ? { volume } : {},
|
|
146
|
+
...liquidity !== void 0 ? { liquidity } : {},
|
|
147
|
+
markets
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 单事件响应体 → 详情。
|
|
152
|
+
*
|
|
153
|
+
* `id=` / `slug=` 是精确匹配(实测最多一条),所以取首条可解析记录;
|
|
154
|
+
* 返回 undefined = 上游 200 但空集(**没有这个事件**,调用方抛 TRADING_SYMBOL_NOT_FOUND),
|
|
155
|
+
* 与「上游故障」是两件事,调用方不得把前者读成后者或反过来。
|
|
156
|
+
*/
|
|
157
|
+
function parseEventDetail(payload) {
|
|
158
|
+
if (!Array.isArray(payload)) throw new TypeError("polymarket gamma /events: expected a JSON array of events");
|
|
159
|
+
for (const event of payload) {
|
|
160
|
+
const detail = parseEventRecord(event);
|
|
161
|
+
if (detail !== void 0) return detail;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
export { matchesKeyword, parseEventDetail, parseEventSummary, searchEventsInPage };
|
package/lib/gamma.d.ts
CHANGED
|
@@ -18,12 +18,25 @@ interface GammaTransportOptions {
|
|
|
18
18
|
/** UA 覆盖。 */
|
|
19
19
|
readonly userAgent?: string;
|
|
20
20
|
}
|
|
21
|
-
/** 一次 GET 的结果:状态码 + 解析后的 JSON 体 +
|
|
21
|
+
/** 一次 GET 的结果:状态码 + 解析后的 JSON 体 + 原始文本 + 响应头(证据/新鲜度用)。 */
|
|
22
22
|
interface GammaResponse {
|
|
23
23
|
readonly status: number;
|
|
24
24
|
readonly body: unknown;
|
|
25
25
|
readonly text: string;
|
|
26
|
+
readonly headers: Headers;
|
|
26
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* 本次响应是否由 Cloudflare **共享缓存直接命中**(`cf-cache-status: HIT`)。
|
|
30
|
+
*
|
|
31
|
+
* 用途:工具面 `stale` 字段的**唯一判据**。`/events` 回
|
|
32
|
+
* `cache-control: public, max-age=300`,HIT 意味着拿到的是副本(实测 `age: 224`
|
|
33
|
+
* 的副本,见 spikes/impl-polymarket/agent-tools-ACCEPTANCE.md),最多可能比源站旧
|
|
34
|
+
* 5 分钟;MISS/DYNAMIC = 源站当次现算。
|
|
35
|
+
*
|
|
36
|
+
* 头缺失(本地夹具、非 Cloudflare 部署)返回 false——false 的语义是「没有观察到
|
|
37
|
+
* 共享缓存命中」,不是「已证明为源站实时值」,两者不可混为一谈(不猜)。
|
|
38
|
+
*/
|
|
39
|
+
declare function isCdnCached(response: GammaResponse): boolean;
|
|
27
40
|
declare class GammaClient {
|
|
28
41
|
private readonly timeoutMs;
|
|
29
42
|
private readonly fetchImpl;
|
|
@@ -42,4 +55,4 @@ declare class GammaClient {
|
|
|
42
55
|
get(path: string, query?: Readonly<Record<string, string>>): Promise<GammaResponse>;
|
|
43
56
|
}
|
|
44
57
|
//#endregion
|
|
45
|
-
export { DEFAULT_TIMEOUT_MS, GAMMA_BASE_URL, GammaClient, GammaResponse, GammaTransportOptions, USER_AGENT };
|
|
58
|
+
export { DEFAULT_TIMEOUT_MS, GAMMA_BASE_URL, GammaClient, GammaResponse, GammaTransportOptions, USER_AGENT, isCdnCached };
|