@dshtrading/connector-polymarket 0.0.0-stage → 0.6.2
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/LICENSE +75 -0
- package/README.md +35 -2
- package/lib/discovery.d.ts +56 -0
- package/lib/discovery.js +238 -0
- package/lib/errors.d.ts +22 -0
- package/lib/errors.js +29 -0
- package/lib/gamma.d.ts +45 -0
- package/lib/gamma.js +93 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +7 -0
- package/lib/plugin.d.ts +37 -0
- package/lib/plugin.js +41 -0
- package/lib/service.d.ts +52 -0
- package/lib/service.js +73 -0
- package/lib/snapshot.d.ts +72 -0
- package/lib/snapshot.js +165 -0
- package/package.json +32 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Copyright (c) 2026 zhu1090093659 (dsh-trading)
|
|
2
|
+
|
|
3
|
+
# PolyForm Noncommercial License 1.0.0
|
|
4
|
+
|
|
5
|
+
<https://polyformproject.org/licenses/noncommercial/1.0.0>
|
|
6
|
+
|
|
7
|
+
## Acceptance
|
|
8
|
+
|
|
9
|
+
In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
|
|
10
|
+
|
|
11
|
+
## Copyright License
|
|
12
|
+
|
|
13
|
+
The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
|
|
14
|
+
|
|
15
|
+
## Distribution License
|
|
16
|
+
|
|
17
|
+
The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
|
|
18
|
+
|
|
19
|
+
## Notices
|
|
20
|
+
|
|
21
|
+
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
|
|
22
|
+
|
|
23
|
+
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
|
|
24
|
+
|
|
25
|
+
## Changes and New Works License
|
|
26
|
+
|
|
27
|
+
The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
|
|
28
|
+
|
|
29
|
+
## Patent License
|
|
30
|
+
|
|
31
|
+
The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
|
|
32
|
+
|
|
33
|
+
## Noncommercial Purposes
|
|
34
|
+
|
|
35
|
+
Any noncommercial purpose is a permitted purpose.
|
|
36
|
+
|
|
37
|
+
## Personal Uses
|
|
38
|
+
|
|
39
|
+
Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
|
|
40
|
+
|
|
41
|
+
## Noncommercial Organizations
|
|
42
|
+
|
|
43
|
+
Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
|
|
44
|
+
|
|
45
|
+
## Fair Use
|
|
46
|
+
|
|
47
|
+
You may have "fair use" rights for the software under the law. These terms do not limit them.
|
|
48
|
+
|
|
49
|
+
## No Other Rights
|
|
50
|
+
|
|
51
|
+
These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
|
|
52
|
+
|
|
53
|
+
## Patent Defense
|
|
54
|
+
|
|
55
|
+
If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
|
|
56
|
+
|
|
57
|
+
## Violations
|
|
58
|
+
|
|
59
|
+
The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
|
|
60
|
+
|
|
61
|
+
## No Liability
|
|
62
|
+
|
|
63
|
+
***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
|
|
64
|
+
|
|
65
|
+
## Definitions
|
|
66
|
+
|
|
67
|
+
The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
|
|
68
|
+
|
|
69
|
+
**You** refers to the individual or entity agreeing to these terms.
|
|
70
|
+
|
|
71
|
+
**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
|
72
|
+
|
|
73
|
+
**Your licenses** are all the licenses granted to you for the software under these terms.
|
|
74
|
+
|
|
75
|
+
**Use** means anything you do with the software requiring one of your licenses.
|
package/README.md
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @dshtrading/connector-polymarket
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Polymarket 预测市场连接器(**只读**公开 API,无密钥)。v1 只覆盖 **FOMC 决议路径**:
|
|
4
|
+
给出一场会议的各档位标签与隐含概率,供右侧栏预测市场面板使用。
|
|
5
|
+
|
|
6
|
+
判据与口径的唯一来源是 [spikes/impl-polymarket/EVIDENCE.md](../../spikes/impl-polymarket/EVIDENCE.md)
|
|
7
|
+
(P0 spike 裁决)。**不要另起一套猜法**——发现规则与概率口径都在那份证据里钉死了。
|
|
8
|
+
|
|
9
|
+
## 结构
|
|
10
|
+
|
|
11
|
+
- `src/gamma.ts` —— gamma HTTP 传输层。固定自有 UA(实测 200;`Python-urllib` 会触发
|
|
12
|
+
Cloudflare 1010 → 403 且响应体是 text/plain)、显式超时、三层失败语义
|
|
13
|
+
(传输 → `TRADING_NETWORK`;4xx/5xx → `TRADING_UPSTREAM_ERROR`;200 非 JSON → 契约破损)。
|
|
14
|
+
- `src/discovery.ts` —— 发现规则 L0..L5(`series_id=35` → `series[].id` 校验 → 标题正则
|
|
15
|
+
→ `negRisk` + 档位数 4..6 → 噪声排除 → 按 id 去重/`endDate` 升序)与档位归一化。
|
|
16
|
+
纯函数,可离线复核历史证据。
|
|
17
|
+
- `src/snapshot.ts` —— 本地快照(存 trading home)。只回答「自上次观察变了多少」,
|
|
18
|
+
缺失时 `changeSincePrevious` **缺省而不是 0**。
|
|
19
|
+
- `src/service.ts` —— 取数服务 + host 面服务对象(`tradingPredictionFeed`)。
|
|
20
|
+
- `src/plugin.ts` —— 插件入口。
|
|
21
|
+
|
|
22
|
+
## 只读边界(硬约束)
|
|
23
|
+
|
|
24
|
+
只访问 `gamma-api.polymarket.com/events` 读取面。**不触达任何下单/交易端点**,
|
|
25
|
+
不引入任何密钥。
|
|
26
|
+
|
|
27
|
+
## 数据落盘
|
|
28
|
+
|
|
29
|
+
快照写 trading home(经 `@dshtrading/dsh-home` 解析,
|
|
30
|
+
缺省 `$DSH_HOME/polymarket/snapshots.json`),绝不写进包内或仓库数据。
|
|
31
|
+
|
|
32
|
+
## 接线
|
|
33
|
+
|
|
34
|
+
跨市场信息源(FOMC 路径影响全部市场会话),故按 connector-jin10 先例由
|
|
35
|
+
`@dshtrading/base` bundle 认领:`packages/base/cordis.patch.yml` 的 insert 行 +
|
|
36
|
+
`packages/base/package.json` 的 dependencies。
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { PredictionMeeting } from "@dshtrading/api";
|
|
2
|
+
//#region src/discovery.d.ts
|
|
3
|
+
/** 判据来源系列:FOMC 决议(证据 Q1.2,39/39 命中、噪声 0)。 */
|
|
4
|
+
declare const FOMC_SERIES_ID = "35";
|
|
5
|
+
/**
|
|
6
|
+
* `tag_slug=fomc` 上的已知噪声(L1 已全部排除,本表是纵深防御并记录失效模式)。
|
|
7
|
+
* 来源:证据 Q1.1 的 6 条噪声 + probe-discovery.mjs 的全历史复核清单。
|
|
8
|
+
*/
|
|
9
|
+
declare const NOISE_SLUGS: readonly string[];
|
|
10
|
+
/** 档位形状边界(证据 Q1.6:历史上有 4/5/6 三档形态,超出即非决议档位集合)。 */
|
|
11
|
+
declare const MIN_BUCKETS = 4;
|
|
12
|
+
declare const MAX_BUCKETS = 6;
|
|
13
|
+
/**
|
|
14
|
+
* 各档概率之和的正常区间(证据 Q4.2:实测偏离 −0.0015 ~ +0.0155)。
|
|
15
|
+
* **不假设严格等于 1**——2026-12 的 `sum(bid)` 实测为 1.0040 > 1,硬约束会假红。
|
|
16
|
+
*/
|
|
17
|
+
declare const PROBABILITY_SUM_MIN = 0.98;
|
|
18
|
+
declare const PROBABILITY_SUM_MAX = 1.03;
|
|
19
|
+
/** 档位标签 → 稳定键与利率变动(证据 Q1.7:标签在大小写与 `+` 上会漂移)。 */
|
|
20
|
+
declare function canonicalBucket(label: string): {
|
|
21
|
+
key: string;
|
|
22
|
+
changeBps: number | undefined;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* 单场会议 → 契约形状(含健康标记)。
|
|
26
|
+
*
|
|
27
|
+
* 返回 undefined = 该事件形状不足以构成一次决议(L3 之外的残形),由调用方丢弃。
|
|
28
|
+
*/
|
|
29
|
+
declare function parseMeeting(event: Record<string, unknown>): PredictionMeeting | undefined;
|
|
30
|
+
interface DiscoveryTrace {
|
|
31
|
+
/** 上游返回的候选事件数。 */
|
|
32
|
+
readonly candidates: number;
|
|
33
|
+
/** 各层后剩余条数(L1..L4)。 */
|
|
34
|
+
readonly layers: {
|
|
35
|
+
readonly series: number;
|
|
36
|
+
readonly title: number;
|
|
37
|
+
readonly shape: number;
|
|
38
|
+
readonly noise: number;
|
|
39
|
+
};
|
|
40
|
+
/** 各层丢弃的标题(诊断用,不参与业务)。 */
|
|
41
|
+
readonly dropped: readonly string[];
|
|
42
|
+
}
|
|
43
|
+
interface DiscoveryResult {
|
|
44
|
+
readonly meetings: readonly PredictionMeeting[];
|
|
45
|
+
readonly trace: DiscoveryTrace;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 应用 L1..L5。输入为**已解析的 JSON 体**(不是 HTTP 层),便于离线复核历史证据。
|
|
49
|
+
*
|
|
50
|
+
* 形状错误一律抛错而**不返回空数组**:上游把「参数/路径错」和「真的没有数据」
|
|
51
|
+
* 都表现成 200(`tag_slug` 不存在 → 200 `[]`,证据 Q5.2),这里只能在体形状上守住
|
|
52
|
+
* 边界——非数组体 = 契约破损,必须响亮。
|
|
53
|
+
*/
|
|
54
|
+
declare function discoverFomcMeetings(payload: unknown): DiscoveryResult;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { DiscoveryResult, DiscoveryTrace, FOMC_SERIES_ID, MAX_BUCKETS, MIN_BUCKETS, NOISE_SLUGS, PROBABILITY_SUM_MAX, PROBABILITY_SUM_MIN, canonicalBucket, discoverFomcMeetings, parseMeeting };
|
package/lib/discovery.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
//#region src/discovery.ts
|
|
2
|
+
/** 判据来源系列:FOMC 决议(证据 Q1.2,39/39 命中、噪声 0)。 */
|
|
3
|
+
const FOMC_SERIES_ID = "35";
|
|
4
|
+
/**
|
|
5
|
+
* `tag_slug=fomc` 上的已知噪声(L1 已全部排除,本表是纵深防御并记录失效模式)。
|
|
6
|
+
* 来源:证据 Q1.1 的 6 条噪声 + probe-discovery.mjs 的全历史复核清单。
|
|
7
|
+
*/
|
|
8
|
+
const NOISE_SLUGS = [
|
|
9
|
+
"how-many-fed-rate-hikes-in-2026",
|
|
10
|
+
"warsh-out-as-fed-chair-by",
|
|
11
|
+
"trump-tries-to-fire-warsh-in-2026",
|
|
12
|
+
"fed-decisions-",
|
|
13
|
+
"no-change-or-hike-25-bps-favored-on-october-27",
|
|
14
|
+
"fed-rate-hike-by",
|
|
15
|
+
"what-will-be-the-next-fed-rate-change",
|
|
16
|
+
"dissent-at-fed-meeting",
|
|
17
|
+
"powell-bingo",
|
|
18
|
+
"what-will-powell-say",
|
|
19
|
+
"what-will-kevin-warsh-say",
|
|
20
|
+
"powell-says-"
|
|
21
|
+
];
|
|
22
|
+
const MONTHS = [
|
|
23
|
+
"January",
|
|
24
|
+
"February",
|
|
25
|
+
"March",
|
|
26
|
+
"April",
|
|
27
|
+
"May",
|
|
28
|
+
"June",
|
|
29
|
+
"July",
|
|
30
|
+
"August",
|
|
31
|
+
"September",
|
|
32
|
+
"October",
|
|
33
|
+
"November",
|
|
34
|
+
"December"
|
|
35
|
+
];
|
|
36
|
+
/** 现代标题形态(2024-12 起):`Fed decision in October?`。 */
|
|
37
|
+
const TITLE_MODERN = new RegExp(`^Fed decision in (${MONTHS.join("|")})\\?$`, "i");
|
|
38
|
+
/** 历史标题形态(2022-03 起):`Fed Interest Rates: June 2024`。 */
|
|
39
|
+
const TITLE_LEGACY = new RegExp(`^Fed Interest Rates:? (${MONTHS.join("|")}) \\d{4}$`, "i");
|
|
40
|
+
/** 档位形状边界(证据 Q1.6:历史上有 4/5/6 三档形态,超出即非决议档位集合)。 */
|
|
41
|
+
const MIN_BUCKETS = 4;
|
|
42
|
+
const MAX_BUCKETS = 6;
|
|
43
|
+
/**
|
|
44
|
+
* 各档概率之和的正常区间(证据 Q4.2:实测偏离 −0.0015 ~ +0.0155)。
|
|
45
|
+
* **不假设严格等于 1**——2026-12 的 `sum(bid)` 实测为 1.0040 > 1,硬约束会假红。
|
|
46
|
+
*/
|
|
47
|
+
const PROBABILITY_SUM_MIN = .98;
|
|
48
|
+
const PROBABILITY_SUM_MAX = 1.03;
|
|
49
|
+
/** 会议日程解析(上游 description 原文:`its meeting scheduled for October 27-28, 2026`)。 */
|
|
50
|
+
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
|
+
/** 档位标签 → 稳定键与利率变动(证据 Q1.7:标签在大小写与 `+` 上会漂移)。 */
|
|
67
|
+
function canonicalBucket(label) {
|
|
68
|
+
const value = label.trim();
|
|
69
|
+
if (/^no change$/i.test(value)) return {
|
|
70
|
+
key: "no_change",
|
|
71
|
+
changeBps: 0
|
|
72
|
+
};
|
|
73
|
+
const match = /^(\d+)\+? bps (decrease|increase)$/i.exec(value);
|
|
74
|
+
if (match) {
|
|
75
|
+
const magnitude = Number(match[1]);
|
|
76
|
+
const up = /increase/i.test(match[2] ?? "");
|
|
77
|
+
return {
|
|
78
|
+
key: `${up ? "inc" : "dec"}_${match[1]}${value.includes("+") ? "plus" : ""}`,
|
|
79
|
+
changeBps: up ? magnitude : -magnitude
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
key: `raw:${value}`,
|
|
84
|
+
changeBps: void 0
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function titleMatches(title) {
|
|
88
|
+
return TITLE_MODERN.test(title) || TITLE_LEGACY.test(title);
|
|
89
|
+
}
|
|
90
|
+
function hasSeries(event) {
|
|
91
|
+
if (!Array.isArray(event.series)) return false;
|
|
92
|
+
return event.series.some((entry) => isRecord(entry) && String(entry.id) === "35");
|
|
93
|
+
}
|
|
94
|
+
/** `outcomePrices` 是**字符串化的 JSON 数组**(如 `"[\"0.0035\",\"0.9965\"]"`)。 */
|
|
95
|
+
function parseOutcomePrices(value) {
|
|
96
|
+
if (typeof value !== "string") return void 0;
|
|
97
|
+
let parsed;
|
|
98
|
+
try {
|
|
99
|
+
parsed = JSON.parse(value);
|
|
100
|
+
} catch {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (!Array.isArray(parsed) || parsed.length === 0) return void 0;
|
|
104
|
+
return num(parsed[0]);
|
|
105
|
+
}
|
|
106
|
+
function parseBucket(market) {
|
|
107
|
+
const label = text(market.groupItemTitle);
|
|
108
|
+
const probability = parseOutcomePrices(market.outcomePrices);
|
|
109
|
+
if (label === void 0 || probability === void 0) return void 0;
|
|
110
|
+
const { key, changeBps } = canonicalBucket(label);
|
|
111
|
+
const bestBid = num(market.bestBid);
|
|
112
|
+
const bestAsk = num(market.bestAsk);
|
|
113
|
+
const liquidity = num(market.liquidityNum);
|
|
114
|
+
const volume = num(market.volumeNum);
|
|
115
|
+
const volume24h = num(market.volume24hr);
|
|
116
|
+
const tickSize = num(market.orderPriceMinTickSize);
|
|
117
|
+
return {
|
|
118
|
+
key,
|
|
119
|
+
label,
|
|
120
|
+
...changeBps !== void 0 ? { changeBps } : {},
|
|
121
|
+
probability,
|
|
122
|
+
...bestBid !== void 0 ? { bestBid } : {},
|
|
123
|
+
...bestAsk !== void 0 ? { bestAsk } : {},
|
|
124
|
+
...bestBid !== void 0 && bestAsk !== void 0 ? { spread: Number((bestAsk - bestBid).toFixed(6)) } : {},
|
|
125
|
+
...liquidity !== void 0 ? { liquidity } : {},
|
|
126
|
+
...volume !== void 0 ? { volume } : {},
|
|
127
|
+
...volume24h !== void 0 ? { volume24h } : {},
|
|
128
|
+
...tickSize !== void 0 ? { tickSize } : {},
|
|
129
|
+
acceptingOrders: market.acceptingOrders === true,
|
|
130
|
+
closed: market.closed === true
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** 档位排序:按利率变动从低到高,未识别形态排末位(用 key 兜底保证确定性)。 */
|
|
134
|
+
function compareBuckets(a, b) {
|
|
135
|
+
const left = a.changeBps;
|
|
136
|
+
const right = b.changeBps;
|
|
137
|
+
if (left === void 0 && right === void 0) return a.key.localeCompare(b.key);
|
|
138
|
+
if (left === void 0) return 1;
|
|
139
|
+
if (right === void 0) return -1;
|
|
140
|
+
return left - right;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* 单场会议 → 契约形状(含健康标记)。
|
|
144
|
+
*
|
|
145
|
+
* 返回 undefined = 该事件形状不足以构成一次决议(L3 之外的残形),由调用方丢弃。
|
|
146
|
+
*/
|
|
147
|
+
function parseMeeting(event) {
|
|
148
|
+
const id = text(String(event.id ?? ""));
|
|
149
|
+
const slug = text(event.slug);
|
|
150
|
+
const title = text(event.title);
|
|
151
|
+
const endDate = text(event.endDate);
|
|
152
|
+
if (id === void 0 || slug === void 0 || title === void 0 || endDate === void 0) return void 0;
|
|
153
|
+
if (!Array.isArray(event.markets)) return void 0;
|
|
154
|
+
const buckets = [];
|
|
155
|
+
for (const raw of event.markets) {
|
|
156
|
+
if (!isRecord(raw)) continue;
|
|
157
|
+
const bucket = parseBucket(raw);
|
|
158
|
+
if (bucket !== void 0) buckets.push(bucket);
|
|
159
|
+
}
|
|
160
|
+
if (buckets.length === 0) return void 0;
|
|
161
|
+
buckets.sort(compareBuckets);
|
|
162
|
+
const sum = Number(buckets.reduce((acc, bucket) => acc + bucket.probability, 0).toFixed(6));
|
|
163
|
+
const degraded = [];
|
|
164
|
+
if (sum < .98 || sum > 1.03) degraded.push("probability-sum-out-of-band");
|
|
165
|
+
if (buckets.some((bucket) => bucket.probability < 0 || bucket.probability > 1)) degraded.push("probability-out-of-range");
|
|
166
|
+
if (buckets.some((bucket) => !bucket.closed && (bucket.bestBid === void 0 || bucket.bestAsk === void 0))) degraded.push("bucket-quote-missing");
|
|
167
|
+
if (buckets.some((bucket) => bucket.key.startsWith("raw:"))) degraded.push("bucket-unrecognized-label");
|
|
168
|
+
const description = text(event.description);
|
|
169
|
+
const window = description === void 0 ? void 0 : MEETING_WINDOW_RE.exec(description);
|
|
170
|
+
return {
|
|
171
|
+
id,
|
|
172
|
+
slug,
|
|
173
|
+
title,
|
|
174
|
+
endDate,
|
|
175
|
+
...window?.[1] !== void 0 ? { meetingWindow: window[1] } : {},
|
|
176
|
+
closed: event.closed === true,
|
|
177
|
+
buckets,
|
|
178
|
+
probabilitySum: sum,
|
|
179
|
+
degraded
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* 应用 L1..L5。输入为**已解析的 JSON 体**(不是 HTTP 层),便于离线复核历史证据。
|
|
184
|
+
*
|
|
185
|
+
* 形状错误一律抛错而**不返回空数组**:上游把「参数/路径错」和「真的没有数据」
|
|
186
|
+
* 都表现成 200(`tag_slug` 不存在 → 200 `[]`,证据 Q5.2),这里只能在体形状上守住
|
|
187
|
+
* 边界——非数组体 = 契约破损,必须响亮。
|
|
188
|
+
*/
|
|
189
|
+
function discoverFomcMeetings(payload) {
|
|
190
|
+
if (!Array.isArray(payload)) throw new TypeError("polymarket gamma /events: expected a JSON array of events");
|
|
191
|
+
const candidates = payload.filter(isRecord);
|
|
192
|
+
const dropped = [];
|
|
193
|
+
const bySeries = candidates.filter((event) => {
|
|
194
|
+
const ok = hasSeries(event);
|
|
195
|
+
if (!ok) dropped.push(`L1:${String(event.title ?? event.slug ?? "(untitled)")}`);
|
|
196
|
+
return ok;
|
|
197
|
+
});
|
|
198
|
+
const byTitle = bySeries.filter((event) => {
|
|
199
|
+
const ok = titleMatches(String(event.title ?? ""));
|
|
200
|
+
if (!ok) dropped.push(`L2:${String(event.title ?? "(untitled)")}`);
|
|
201
|
+
return ok;
|
|
202
|
+
});
|
|
203
|
+
const byShape = byTitle.filter((event) => {
|
|
204
|
+
const count = Array.isArray(event.markets) ? event.markets.length : 0;
|
|
205
|
+
const ok = event.negRisk === true && count >= 4 && count <= 6;
|
|
206
|
+
if (!ok) dropped.push(`L3:${String(event.title ?? "(untitled)")}`);
|
|
207
|
+
return ok;
|
|
208
|
+
});
|
|
209
|
+
const byNoise = byShape.filter((event) => {
|
|
210
|
+
const haystack = `${String(event.slug ?? "")} ${String(event.ticker ?? "")}`;
|
|
211
|
+
const hit = NOISE_SLUGS.find((noise) => haystack.includes(noise));
|
|
212
|
+
if (hit !== void 0) dropped.push(`L4:${String(event.title ?? "(untitled)")}<-${hit}`);
|
|
213
|
+
return hit === void 0;
|
|
214
|
+
});
|
|
215
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
216
|
+
for (const event of byNoise) {
|
|
217
|
+
const meeting = parseMeeting(event);
|
|
218
|
+
if (meeting !== void 0) deduped.set(meeting.id, meeting);
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
meetings: [...deduped.values()].sort((a, b) => {
|
|
222
|
+
const diff = Date.parse(a.endDate) - Date.parse(b.endDate);
|
|
223
|
+
return diff !== 0 ? diff : a.id.localeCompare(b.id);
|
|
224
|
+
}),
|
|
225
|
+
trace: {
|
|
226
|
+
candidates: candidates.length,
|
|
227
|
+
layers: {
|
|
228
|
+
series: bySeries.length,
|
|
229
|
+
title: byTitle.length,
|
|
230
|
+
shape: byShape.length,
|
|
231
|
+
noise: byNoise.length
|
|
232
|
+
},
|
|
233
|
+
dropped
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
//#endregion
|
|
238
|
+
export { FOMC_SERIES_ID, MAX_BUCKETS, MIN_BUCKETS, NOISE_SLUGS, PROBABILITY_SUM_MAX, PROBABILITY_SUM_MIN, canonicalBucket, discoverFomcMeetings, parseMeeting };
|
package/lib/errors.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { TradingErrorCode } from "@dshtrading/api";
|
|
2
|
+
//#region src/errors.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* 错误词汇对齐 @dshtrading/api(本包不做跨包 Error 类基座,与 connector-jin10 同款)。
|
|
5
|
+
*
|
|
6
|
+
* 本连接器**无凭证**(Polymarket gamma 为公开只读 API),故不存在
|
|
7
|
+
* TRADING_CREDENTIALS_MISSING 路径;失败一律落在网络层、准入层或上游契约层。
|
|
8
|
+
*/
|
|
9
|
+
declare class PolymarketError extends Error {
|
|
10
|
+
readonly code: TradingErrorCode;
|
|
11
|
+
constructor(code: TradingErrorCode, message: string, cause?: unknown);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 上游响应体 → 可读片段(截断到 200 字符,绝不落盘整包)。
|
|
15
|
+
*
|
|
16
|
+
* 403 的响应体是 **text/plain 的 `error code: 1010`**(Cloudflare 准入拦截),
|
|
17
|
+
* 不是 JSON——解析层必须把它读成准入失败而不是「没有数据」,这是验收要求
|
|
18
|
+
* 「失败路径不产生空数据假象」的第一道闸门。
|
|
19
|
+
*/
|
|
20
|
+
declare function bodySnippet(text: string): string;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { PolymarketError, bodySnippet };
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* 错误词汇对齐 @dshtrading/api(本包不做跨包 Error 类基座,与 connector-jin10 同款)。
|
|
4
|
+
*
|
|
5
|
+
* 本连接器**无凭证**(Polymarket gamma 为公开只读 API),故不存在
|
|
6
|
+
* TRADING_CREDENTIALS_MISSING 路径;失败一律落在网络层、准入层或上游契约层。
|
|
7
|
+
*/
|
|
8
|
+
var PolymarketError = class extends Error {
|
|
9
|
+
code;
|
|
10
|
+
constructor(code, message, cause) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "PolymarketError";
|
|
13
|
+
this.code = code;
|
|
14
|
+
if (cause !== void 0) this.cause = cause;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* 上游响应体 → 可读片段(截断到 200 字符,绝不落盘整包)。
|
|
19
|
+
*
|
|
20
|
+
* 403 的响应体是 **text/plain 的 `error code: 1010`**(Cloudflare 准入拦截),
|
|
21
|
+
* 不是 JSON——解析层必须把它读成准入失败而不是「没有数据」,这是验收要求
|
|
22
|
+
* 「失败路径不产生空数据假象」的第一道闸门。
|
|
23
|
+
*/
|
|
24
|
+
function bodySnippet(text) {
|
|
25
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
26
|
+
return collapsed.length > 200 ? `${collapsed.slice(0, 200)}…` : collapsed;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { PolymarketError, bodySnippet };
|
package/lib/gamma.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/gamma.d.ts
|
|
2
|
+
declare const GAMMA_BASE_URL = "https://gamma-api.polymarket.com";
|
|
3
|
+
declare const DEFAULT_TIMEOUT_MS = 10000;
|
|
4
|
+
/**
|
|
5
|
+
* 自有 UA。实测矩阵(raw/connector-live-ua-matrix.txt,2026-10-11T02:00Z):
|
|
6
|
+
* `Mozilla/5.0` 200、`Mozilla/5.0 (compatible; <name>)` 200、`<name>/<ver>` 200、
|
|
7
|
+
* `Python-urllib/3.11` 403。取自有标识而非伪造浏览器 UA——请求语义诚实,
|
|
8
|
+
* 且仍落在 200 的集合内。
|
|
9
|
+
*/
|
|
10
|
+
declare const USER_AGENT = "dsh-trading-connector-polymarket/0.6.1";
|
|
11
|
+
interface GammaTransportOptions {
|
|
12
|
+
/** 单次请求超时(ms),缺省 10s。 */
|
|
13
|
+
readonly timeoutMs?: number;
|
|
14
|
+
/** 注入的 fetch(测试用契约化替身;缺省 globalThis.fetch)。 */
|
|
15
|
+
readonly fetchImpl?: typeof globalThis.fetch;
|
|
16
|
+
/** 基址覆盖(测试指向本地夹具服务用)。 */
|
|
17
|
+
readonly baseUrl?: string;
|
|
18
|
+
/** UA 覆盖。 */
|
|
19
|
+
readonly userAgent?: string;
|
|
20
|
+
}
|
|
21
|
+
/** 一次 GET 的结果:状态码 + 解析后的 JSON 体 + 原始文本(证据用)。 */
|
|
22
|
+
interface GammaResponse {
|
|
23
|
+
readonly status: number;
|
|
24
|
+
readonly body: unknown;
|
|
25
|
+
readonly text: string;
|
|
26
|
+
}
|
|
27
|
+
declare class GammaClient {
|
|
28
|
+
private readonly timeoutMs;
|
|
29
|
+
private readonly fetchImpl;
|
|
30
|
+
private readonly baseUrl;
|
|
31
|
+
private readonly userAgent;
|
|
32
|
+
constructor(options?: GammaTransportOptions);
|
|
33
|
+
/**
|
|
34
|
+
* GET `path?query`。
|
|
35
|
+
*
|
|
36
|
+
* 失败语义(三层,全部抛结构化错误,**不返回空数组冒充无数据**):
|
|
37
|
+
* - 传输层(DNS/TLS/超时)→ `TRADING_NETWORK`;
|
|
38
|
+
* - HTTP 403/4xx/5xx → `TRADING_UPSTREAM_ERROR`,消息带状态码与响应片段
|
|
39
|
+
* (403 的 text/plain 体就是 Cloudflare 1010,直接可读);
|
|
40
|
+
* - 200 但体不是 JSON → `TRADING_UPSTREAM_ERROR`(契约破损,不静默吞)。
|
|
41
|
+
*/
|
|
42
|
+
get(path: string, query?: Readonly<Record<string, string>>): Promise<GammaResponse>;
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { DEFAULT_TIMEOUT_MS, GAMMA_BASE_URL, GammaClient, GammaResponse, GammaTransportOptions, USER_AGENT };
|
package/lib/gamma.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { PolymarketError, bodySnippet } from "./errors.js";
|
|
2
|
+
//#region src/gamma.ts
|
|
3
|
+
/**
|
|
4
|
+
* Polymarket gamma 公开只读 API 的传输层。
|
|
5
|
+
*
|
|
6
|
+
* 只读边界(硬约束):本模块只访问 `gamma-api.polymarket.com` 的 `/events`
|
|
7
|
+
* 读取面;**不触达任何下单/交易端点**(无 CLOB 鉴权头、无 /order、无 API key)。
|
|
8
|
+
*
|
|
9
|
+
* 三条实测约束(全部来自 spikes/impl-polymarket/EVIDENCE.md,不另起猜法):
|
|
10
|
+
*
|
|
11
|
+
* 1. **必须带 UA**:`curl` 默认 UA 与 `Mozilla/5.0` 均 200;`Python-urllib/3.11`
|
|
12
|
+
* 触发 Cloudflare 1010 → 403,响应体是 **text/plain**(不是 JSON)。本模块固定
|
|
13
|
+
* 发送可识别的自有 UA(实测 200),并在 403/非 JSON 响应上抛结构化错误——
|
|
14
|
+
* 绝不把准入失败读成「没有数据」。
|
|
15
|
+
* 2. **CDN 5 分钟缓存**:`/events` 回 `cache-control: public, max-age=300`,
|
|
16
|
+
* 快照轮询快于 5 分钟只会拿到同一份副本。需要实时值时由调用方传
|
|
17
|
+
* `cacheBust: true`(任意 query 变化即 `cf-cache-status: MISS`)。
|
|
18
|
+
* 3. **超时要显式**:上游无 429 证据,但存在 TLS `SSL_ERROR_SYSCALL`(curl 35)
|
|
19
|
+
* 与 DNS/连接超时;用 AbortSignal 超时把它变成 TRADING_NETWORK 而不是悬挂。
|
|
20
|
+
*/
|
|
21
|
+
const GAMMA_BASE_URL = "https://gamma-api.polymarket.com";
|
|
22
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
23
|
+
/**
|
|
24
|
+
* 自有 UA。实测矩阵(raw/connector-live-ua-matrix.txt,2026-10-11T02:00Z):
|
|
25
|
+
* `Mozilla/5.0` 200、`Mozilla/5.0 (compatible; <name>)` 200、`<name>/<ver>` 200、
|
|
26
|
+
* `Python-urllib/3.11` 403。取自有标识而非伪造浏览器 UA——请求语义诚实,
|
|
27
|
+
* 且仍落在 200 的集合内。
|
|
28
|
+
*/
|
|
29
|
+
const USER_AGENT = "dsh-trading-connector-polymarket/0.6.1";
|
|
30
|
+
var GammaClient = class {
|
|
31
|
+
timeoutMs;
|
|
32
|
+
fetchImpl;
|
|
33
|
+
baseUrl;
|
|
34
|
+
userAgent;
|
|
35
|
+
constructor(options = {}) {
|
|
36
|
+
this.timeoutMs = options.timeoutMs ?? 1e4;
|
|
37
|
+
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
38
|
+
this.baseUrl = options.baseUrl ?? "https://gamma-api.polymarket.com";
|
|
39
|
+
this.userAgent = options.userAgent ?? "dsh-trading-connector-polymarket/0.6.1";
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* GET `path?query`。
|
|
43
|
+
*
|
|
44
|
+
* 失败语义(三层,全部抛结构化错误,**不返回空数组冒充无数据**):
|
|
45
|
+
* - 传输层(DNS/TLS/超时)→ `TRADING_NETWORK`;
|
|
46
|
+
* - HTTP 403/4xx/5xx → `TRADING_UPSTREAM_ERROR`,消息带状态码与响应片段
|
|
47
|
+
* (403 的 text/plain 体就是 Cloudflare 1010,直接可读);
|
|
48
|
+
* - 200 但体不是 JSON → `TRADING_UPSTREAM_ERROR`(契约破损,不静默吞)。
|
|
49
|
+
*/
|
|
50
|
+
async get(path, query = {}) {
|
|
51
|
+
const url = new URL(path, this.baseUrl);
|
|
52
|
+
for (const [key, value] of Object.entries(query)) url.searchParams.set(key, value);
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
controller.abort();
|
|
56
|
+
}, this.timeoutMs);
|
|
57
|
+
let response;
|
|
58
|
+
try {
|
|
59
|
+
response = await this.fetchImpl(url, {
|
|
60
|
+
method: "GET",
|
|
61
|
+
headers: {
|
|
62
|
+
"user-agent": this.userAgent,
|
|
63
|
+
accept: "application/json"
|
|
64
|
+
},
|
|
65
|
+
signal: controller.signal
|
|
66
|
+
});
|
|
67
|
+
} catch (cause) {
|
|
68
|
+
throw networkError(url.pathname, cause);
|
|
69
|
+
} finally {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
}
|
|
72
|
+
const text = await response.text();
|
|
73
|
+
if (!response.ok) throw new PolymarketError("TRADING_UPSTREAM_ERROR", `polymarket gamma ${url.pathname}: HTTP ${response.status} — ${bodySnippet(text)}`);
|
|
74
|
+
let body;
|
|
75
|
+
try {
|
|
76
|
+
body = JSON.parse(text);
|
|
77
|
+
} catch {
|
|
78
|
+
throw new PolymarketError("TRADING_UPSTREAM_ERROR", `polymarket gamma ${url.pathname}: HTTP ${response.status} but the body is not JSON — ${bodySnippet(text)}`);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
status: response.status,
|
|
82
|
+
body,
|
|
83
|
+
text
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
function networkError(path, cause) {
|
|
88
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
89
|
+
const timedOut = cause instanceof Error && (cause.name === "TimeoutError" || cause.name === "AbortError");
|
|
90
|
+
return new PolymarketError("TRADING_NETWORK", `polymarket gamma ${path}: ${timedOut ? "request timed out" : "network failure"} — ${reason}`, cause);
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
export { DEFAULT_TIMEOUT_MS, GAMMA_BASE_URL, GammaClient, USER_AGENT };
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { DiscoveryResult, DiscoveryTrace, FOMC_SERIES_ID, MAX_BUCKETS, MIN_BUCKETS, NOISE_SLUGS, PROBABILITY_SUM_MAX, PROBABILITY_SUM_MIN, canonicalBucket, discoverFomcMeetings, parseMeeting } from "./discovery.js";
|
|
2
|
+
import { PolymarketError, bodySnippet } from "./errors.js";
|
|
3
|
+
import { DEFAULT_TIMEOUT_MS, GAMMA_BASE_URL, GammaClient, GammaResponse, GammaTransportOptions, USER_AGENT } from "./gamma.js";
|
|
4
|
+
import { SNAPSHOT_DIR, SNAPSHOT_FILE, SNAPSHOT_VERSION, SnapshotEntry, SnapshotStore, SnapshotStoreOptions, SnapshotTable, applySnapshot, defaultSnapshotPath, emptySnapshotTable, parseSnapshotTable } from "./snapshot.js";
|
|
5
|
+
import { PREDICTION_EVENTS_LIMIT, PolymarketService, PolymarketServiceOptions, TRADING_PREDICTION_FEED_KEY, createPolymarketPredictionFeed } from "./service.js";
|
|
6
|
+
import { ApplyOverrides, Config, apply, createPolymarketService, inject, name } from "./plugin.js";
|
|
7
|
+
export { type ApplyOverrides, Config, DEFAULT_TIMEOUT_MS, DiscoveryResult, DiscoveryTrace, FOMC_SERIES_ID, GAMMA_BASE_URL, GammaClient, GammaResponse, GammaTransportOptions, MAX_BUCKETS, MIN_BUCKETS, NOISE_SLUGS, PREDICTION_EVENTS_LIMIT, PROBABILITY_SUM_MAX, PROBABILITY_SUM_MIN, PolymarketError, PolymarketService, PolymarketServiceOptions, SNAPSHOT_DIR, SNAPSHOT_FILE, SNAPSHOT_VERSION, SnapshotEntry, SnapshotStore, SnapshotStoreOptions, SnapshotTable, TRADING_PREDICTION_FEED_KEY, USER_AGENT, apply, applySnapshot, bodySnippet, canonicalBucket, createPolymarketPredictionFeed, createPolymarketService, defaultSnapshotPath, discoverFomcMeetings, emptySnapshotTable, inject, name, parseMeeting, parseSnapshotTable };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { FOMC_SERIES_ID, MAX_BUCKETS, MIN_BUCKETS, NOISE_SLUGS, PROBABILITY_SUM_MAX, PROBABILITY_SUM_MIN, canonicalBucket, discoverFomcMeetings, parseMeeting } from "./discovery.js";
|
|
2
|
+
import { PolymarketError, bodySnippet } from "./errors.js";
|
|
3
|
+
import { DEFAULT_TIMEOUT_MS, GAMMA_BASE_URL, GammaClient, USER_AGENT } from "./gamma.js";
|
|
4
|
+
import { PREDICTION_EVENTS_LIMIT, PolymarketService, TRADING_PREDICTION_FEED_KEY, createPolymarketPredictionFeed } from "./service.js";
|
|
5
|
+
import { SNAPSHOT_DIR, SNAPSHOT_FILE, SNAPSHOT_VERSION, SnapshotStore, applySnapshot, defaultSnapshotPath, emptySnapshotTable, parseSnapshotTable } from "./snapshot.js";
|
|
6
|
+
import { Config, apply, createPolymarketService, inject, name } from "./plugin.js";
|
|
7
|
+
export { Config, DEFAULT_TIMEOUT_MS, FOMC_SERIES_ID, GAMMA_BASE_URL, GammaClient, MAX_BUCKETS, MIN_BUCKETS, NOISE_SLUGS, PREDICTION_EVENTS_LIMIT, PROBABILITY_SUM_MAX, PROBABILITY_SUM_MIN, PolymarketError, PolymarketService, SNAPSHOT_DIR, SNAPSHOT_FILE, SNAPSHOT_VERSION, SnapshotStore, TRADING_PREDICTION_FEED_KEY, USER_AGENT, apply, applySnapshot, bodySnippet, canonicalBucket, createPolymarketPredictionFeed, createPolymarketService, defaultSnapshotPath, discoverFomcMeetings, emptySnapshotTable, inject, name, parseMeeting, parseSnapshotTable };
|
package/lib/plugin.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { PolymarketService } from "./service.js";
|
|
2
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
3
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
4
|
+
//#region src/plugin.d.ts
|
|
5
|
+
/** Cordis 插件名 = patch 行 id(TEMPLATES §8)。 */
|
|
6
|
+
declare const name = "dsh-trading-connector-polymarket";
|
|
7
|
+
/** 本插件不注册工具、不依赖任何宿主服务(纯 provide)。 */
|
|
8
|
+
declare const inject: string[];
|
|
9
|
+
interface Config {
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
/** 单次 gamma 请求超时(ms)。 */
|
|
12
|
+
timeoutMs: number;
|
|
13
|
+
/** 是否启用本地快照(Δ 标注)。关闭后 `changeSincePrevious` 一律缺省。 */
|
|
14
|
+
snapshots: boolean;
|
|
15
|
+
/** 快照文件覆盖(绝对路径;缺省 `$DSH_HOME/polymarket/snapshots.json`)。 */
|
|
16
|
+
snapshotPath: string;
|
|
17
|
+
}
|
|
18
|
+
declare const Config: Schema<Config>;
|
|
19
|
+
interface ApplyOverrides {
|
|
20
|
+
/** 注入的 fetch(单测用契约化替身)。 */
|
|
21
|
+
readonly fetchImpl?: typeof globalThis.fetch;
|
|
22
|
+
/** 基址覆盖(单测指向本地夹具服务)。 */
|
|
23
|
+
readonly baseUrl?: string;
|
|
24
|
+
/** 时钟注入(单测固定 fetchedAt)。 */
|
|
25
|
+
readonly now?: (() => number) | undefined;
|
|
26
|
+
}
|
|
27
|
+
/** 组装取数服务(测试可直接注入 fetchImpl,无需 mock 全局)。 */
|
|
28
|
+
declare function createPolymarketService(config: Config, overrides?: ApplyOverrides): PolymarketService;
|
|
29
|
+
/**
|
|
30
|
+
* 插件钩子(宿主签名 `apply(ctx, config)`)。
|
|
31
|
+
*
|
|
32
|
+
* 不接收注入参数:测试用的 fetch/时钟替身走 {@link createPolymarketService},不往
|
|
33
|
+
* 生产钩子的签名里加测试专用面(connector-jin10 同款纪律)。
|
|
34
|
+
*/
|
|
35
|
+
declare function apply(ctx: Context, config: Config): void;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { ApplyOverrides, Config, apply, createPolymarketService, inject, name };
|
package/lib/plugin.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { DEFAULT_TIMEOUT_MS, GammaClient } from "./gamma.js";
|
|
2
|
+
import { PolymarketService, TRADING_PREDICTION_FEED_KEY, createPolymarketPredictionFeed } from "./service.js";
|
|
3
|
+
import { SnapshotStore } from "./snapshot.js";
|
|
4
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
5
|
+
//#region src/plugin.ts
|
|
6
|
+
/** Cordis 插件名 = patch 行 id(TEMPLATES §8)。 */
|
|
7
|
+
const name = "dsh-trading-connector-polymarket";
|
|
8
|
+
/** 本插件不注册工具、不依赖任何宿主服务(纯 provide)。 */
|
|
9
|
+
const inject = [];
|
|
10
|
+
const Config = Schema.object({
|
|
11
|
+
enabled: Schema.boolean().default(true).description("是否启用 Polymarket 预测市场数据面"),
|
|
12
|
+
timeoutMs: Schema.number().default(DEFAULT_TIMEOUT_MS).description("单次 gamma-api 请求超时(ms)"),
|
|
13
|
+
snapshots: Schema.boolean().default(true).description("是否用本地快照标注概率变动(存 trading home)"),
|
|
14
|
+
snapshotPath: Schema.string().default("").description("快照文件绝对路径(留空 = $DSH_HOME/polymarket/snapshots.json)")
|
|
15
|
+
});
|
|
16
|
+
/** 组装取数服务(测试可直接注入 fetchImpl,无需 mock 全局)。 */
|
|
17
|
+
function createPolymarketService(config, overrides = {}) {
|
|
18
|
+
const gamma = new GammaClient({
|
|
19
|
+
timeoutMs: config.timeoutMs,
|
|
20
|
+
...overrides.fetchImpl !== void 0 ? { fetchImpl: overrides.fetchImpl } : {},
|
|
21
|
+
...overrides.baseUrl !== void 0 ? { baseUrl: overrides.baseUrl } : {}
|
|
22
|
+
});
|
|
23
|
+
const snapshots = config.snapshots ? new SnapshotStore(config.snapshotPath === "" ? {} : { filePath: config.snapshotPath }) : void 0;
|
|
24
|
+
return new PolymarketService({
|
|
25
|
+
gamma,
|
|
26
|
+
...snapshots !== void 0 ? { snapshots } : {},
|
|
27
|
+
...overrides.now !== void 0 ? { now: overrides.now } : {}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 插件钩子(宿主签名 `apply(ctx, config)`)。
|
|
32
|
+
*
|
|
33
|
+
* 不接收注入参数:测试用的 fetch/时钟替身走 {@link createPolymarketService},不往
|
|
34
|
+
* 生产钩子的签名里加测试专用面(connector-jin10 同款纪律)。
|
|
35
|
+
*/
|
|
36
|
+
function apply(ctx, config) {
|
|
37
|
+
if (!config.enabled) return;
|
|
38
|
+
ctx.reflect.provide(TRADING_PREDICTION_FEED_KEY, createPolymarketPredictionFeed(createPolymarketService(config)));
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { Config, apply, createPolymarketService, inject, name };
|
package/lib/service.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { GammaClient } from "./gamma.js";
|
|
2
|
+
import { SnapshotStore } from "./snapshot.js";
|
|
3
|
+
import { PredictionFeed, PredictionFeedService } from "@dshtrading/api";
|
|
4
|
+
//#region src/service.d.ts
|
|
5
|
+
declare const TRADING_PREDICTION_FEED_KEY = "tradingPredictionFeed";
|
|
6
|
+
/** 单次取数的候选上限(上游 limit 上限 100,证据 Q1.4)。 */
|
|
7
|
+
declare const PREDICTION_EVENTS_LIMIT = 100;
|
|
8
|
+
interface PolymarketServiceOptions {
|
|
9
|
+
readonly gamma: GammaClient;
|
|
10
|
+
/** 快照存储;缺省 = 不做 Δ 标注(服务仍可用)。 */
|
|
11
|
+
readonly snapshots?: SnapshotStore | undefined;
|
|
12
|
+
/** 时钟注入(测试固定 fetchedAt 用;缺省 Date.now)。 */
|
|
13
|
+
readonly now?: (() => number) | undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 取数实现:**一次 GET 拿全量**(证据 Q2.3 的 v1 裁决理由)。
|
|
17
|
+
*
|
|
18
|
+
* `cacheBust` 的用途(证据 Q3.4):`/events` 有 `max-age=300` CDN 缓存,同一请求
|
|
19
|
+
* 5 分钟内只会拿到同一份副本。默认**不加**(面板轮询间隔离不开 5 分钟时缓存是
|
|
20
|
+
* 有益的去重);需要真实时值时可显式打开。
|
|
21
|
+
*/
|
|
22
|
+
declare class PolymarketService {
|
|
23
|
+
private readonly gamma;
|
|
24
|
+
private readonly snapshots;
|
|
25
|
+
private readonly now;
|
|
26
|
+
constructor(options: PolymarketServiceOptions);
|
|
27
|
+
/**
|
|
28
|
+
* FOMC 决议路径全量会议(含已结算;按 endDate 升序)。
|
|
29
|
+
*
|
|
30
|
+
* **失败必须响亮**:传输失败、HTTP 403(Cloudflare 1010 的 text/plain 体)、
|
|
31
|
+
* 非 JSON 体、非数组体一律抛结构化错误——绝不返回 `{meetings: []}`。空数组
|
|
32
|
+
* 只代表「上游确实返回了空集合」这一种真实语义。
|
|
33
|
+
*/
|
|
34
|
+
listFomcMeetings(options?: {
|
|
35
|
+
cacheBust?: boolean | undefined;
|
|
36
|
+
}): Promise<PredictionFeed>;
|
|
37
|
+
/**
|
|
38
|
+
* Δ 标注:**快照失败不阻断取数**。
|
|
39
|
+
*
|
|
40
|
+
* 为什么与上游失败区别对待:上游失败时我们没有任何可信数据,必须响亮抛错;
|
|
41
|
+
* 快照失败时概率本身依然完整正确,缺的只是「与上次差多少」这一项增益标注。
|
|
42
|
+
* 此时若整块失败,会把一次磁盘/权限问题放大成整个面板黑屏。
|
|
43
|
+
*
|
|
44
|
+
* 但降级必须**可见**:log 到 stderr,并且返回的档位**不带** `changeSincePrevious`
|
|
45
|
+
* (缺省 = 不知道),绝不填 0——0 表示「确知没变」,那是编造。
|
|
46
|
+
*/
|
|
47
|
+
private annotate;
|
|
48
|
+
}
|
|
49
|
+
/** host 平面服务面:桥与工具面共用的最小接口(@dshtrading/api 的 PredictionFeedService)。 */
|
|
50
|
+
declare function createPolymarketPredictionFeed(service: PolymarketService): PredictionFeedService;
|
|
51
|
+
//#endregion
|
|
52
|
+
export { PREDICTION_EVENTS_LIMIT, PolymarketService, PolymarketServiceOptions, TRADING_PREDICTION_FEED_KEY, createPolymarketPredictionFeed };
|
package/lib/service.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { discoverFomcMeetings } from "./discovery.js";
|
|
2
|
+
//#region src/service.ts
|
|
3
|
+
const TRADING_PREDICTION_FEED_KEY = "tradingPredictionFeed";
|
|
4
|
+
/** 单次取数的候选上限(上游 limit 上限 100,证据 Q1.4)。 */
|
|
5
|
+
const PREDICTION_EVENTS_LIMIT = 100;
|
|
6
|
+
/**
|
|
7
|
+
* 取数实现:**一次 GET 拿全量**(证据 Q2.3 的 v1 裁决理由)。
|
|
8
|
+
*
|
|
9
|
+
* `cacheBust` 的用途(证据 Q3.4):`/events` 有 `max-age=300` CDN 缓存,同一请求
|
|
10
|
+
* 5 分钟内只会拿到同一份副本。默认**不加**(面板轮询间隔离不开 5 分钟时缓存是
|
|
11
|
+
* 有益的去重);需要真实时值时可显式打开。
|
|
12
|
+
*/
|
|
13
|
+
var PolymarketService = class {
|
|
14
|
+
gamma;
|
|
15
|
+
snapshots;
|
|
16
|
+
now;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.gamma = options.gamma;
|
|
19
|
+
this.snapshots = options.snapshots;
|
|
20
|
+
this.now = options.now ?? Date.now;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* FOMC 决议路径全量会议(含已结算;按 endDate 升序)。
|
|
24
|
+
*
|
|
25
|
+
* **失败必须响亮**:传输失败、HTTP 403(Cloudflare 1010 的 text/plain 体)、
|
|
26
|
+
* 非 JSON 体、非数组体一律抛结构化错误——绝不返回 `{meetings: []}`。空数组
|
|
27
|
+
* 只代表「上游确实返回了空集合」这一种真实语义。
|
|
28
|
+
*/
|
|
29
|
+
async listFomcMeetings(options = {}) {
|
|
30
|
+
const query = {
|
|
31
|
+
series_id: "35",
|
|
32
|
+
limit: String(100),
|
|
33
|
+
closed: "false"
|
|
34
|
+
};
|
|
35
|
+
if (options.cacheBust === true) query._cb = String(this.now());
|
|
36
|
+
const response = await this.gamma.get("/events", query);
|
|
37
|
+
const { meetings } = discoverFomcMeetings(response.body);
|
|
38
|
+
const fetchedAt = new Date(this.now()).toISOString();
|
|
39
|
+
return {
|
|
40
|
+
fetchedAt,
|
|
41
|
+
meetings: await this.annotate(meetings, fetchedAt)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Δ 标注:**快照失败不阻断取数**。
|
|
46
|
+
*
|
|
47
|
+
* 为什么与上游失败区别对待:上游失败时我们没有任何可信数据,必须响亮抛错;
|
|
48
|
+
* 快照失败时概率本身依然完整正确,缺的只是「与上次差多少」这一项增益标注。
|
|
49
|
+
* 此时若整块失败,会把一次磁盘/权限问题放大成整个面板黑屏。
|
|
50
|
+
*
|
|
51
|
+
* 但降级必须**可见**:log 到 stderr,并且返回的档位**不带** `changeSincePrevious`
|
|
52
|
+
* (缺省 = 不知道),绝不填 0——0 表示「确知没变」,那是编造。
|
|
53
|
+
*/
|
|
54
|
+
async annotate(meetings, observedAt) {
|
|
55
|
+
if (this.snapshots === void 0) return meetings;
|
|
56
|
+
try {
|
|
57
|
+
return await this.snapshots.annotate(meetings, observedAt);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error("[dsh-trading/connector-polymarket] snapshot annotation unavailable, continuing without Δ:", error);
|
|
60
|
+
return meetings;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
/** host 平面服务面:桥与工具面共用的最小接口(@dshtrading/api 的 PredictionFeedService)。 */
|
|
65
|
+
function createPolymarketPredictionFeed(service) {
|
|
66
|
+
return {
|
|
67
|
+
/** 当前 FOMC 决议路径全部会议(含已结算)。 */
|
|
68
|
+
async listFomcMeetings() {
|
|
69
|
+
return service.listFomcMeetings();
|
|
70
|
+
} };
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
export { PREDICTION_EVENTS_LIMIT, PolymarketService, TRADING_PREDICTION_FEED_KEY, createPolymarketPredictionFeed };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { PredictionMeeting } from "@dshtrading/api";
|
|
2
|
+
//#region src/snapshot.d.ts
|
|
3
|
+
/** 快照文件相对 trading home 的路径(POSIX 分隔符,join 时再拆)。 */
|
|
4
|
+
declare const SNAPSHOT_DIR = "polymarket";
|
|
5
|
+
declare const SNAPSHOT_FILE = "snapshots.json";
|
|
6
|
+
/** 快照表版本(结构变更时递增,旧版按空表处理而不是崩)。 */
|
|
7
|
+
declare const SNAPSHOT_VERSION = 1;
|
|
8
|
+
/** 一条档位读数。 */
|
|
9
|
+
interface SnapshotEntry {
|
|
10
|
+
/** 该档最近一次观察到的隐含概率。 */
|
|
11
|
+
readonly probability: number;
|
|
12
|
+
/** 观察时刻(ISO 8601 UTC)。 */
|
|
13
|
+
readonly observedAt: string;
|
|
14
|
+
}
|
|
15
|
+
/** 整张快照表:`eventId -> bucketKey -> entry`。 */
|
|
16
|
+
interface SnapshotTable {
|
|
17
|
+
readonly version: number;
|
|
18
|
+
readonly meetings: Record<string, Record<string, SnapshotEntry>>;
|
|
19
|
+
}
|
|
20
|
+
interface SnapshotStoreOptions {
|
|
21
|
+
/** 文件绝对路径覆盖(测试注入临时目录用)。 */
|
|
22
|
+
readonly filePath?: string;
|
|
23
|
+
/** 环境变量映射(缺省 process.env;测试固定 home 用)。 */
|
|
24
|
+
readonly env?: Record<string, string | undefined>;
|
|
25
|
+
}
|
|
26
|
+
/** 默认快照文件路径:`$DSH_HOME/polymarket/snapshots.json`。 */
|
|
27
|
+
declare function defaultSnapshotPath(env?: Record<string, string | undefined>): string;
|
|
28
|
+
/** 空表(首次运行 / 结构版本不符 / 坏 JSON 的降级值——快照缺失不是错误)。 */
|
|
29
|
+
declare function emptySnapshotTable(): SnapshotTable;
|
|
30
|
+
/**
|
|
31
|
+
* 解析磁盘上的快照表。
|
|
32
|
+
*
|
|
33
|
+
* **降级语义**:快照是纯增益信息(有没有它都不影响概率正确性),所以坏形状一律
|
|
34
|
+
* 降级为空表**不抛错**——与「上游取数失败必须响亮」刻意相反。取数失败会掩盖成
|
|
35
|
+
* 「没有数据」,快照失败则只是少一个 Δ 标注,两者不可混为一谈。
|
|
36
|
+
*/
|
|
37
|
+
declare function parseSnapshotTable(raw: unknown): SnapshotTable;
|
|
38
|
+
/**
|
|
39
|
+
* 给每档补 `changeSincePrevious`(与**上一次**读数的差),然后写入本次读数。
|
|
40
|
+
*
|
|
41
|
+
* 关键纪律(对应验收「失败路径不产生空数据假象」):
|
|
42
|
+
* - 首次采样 = 无历史 → `changeSincePrevious` **缺省**,绝不填 0(0 是「确知没变」,
|
|
43
|
+
* 与「不知道」是两件事);
|
|
44
|
+
* - 已结算档位同样写入(它们在页面上显示最终 0/1),但不再参与后续比较;
|
|
45
|
+
* - 写盘失败**不阻断取数**(调用方拿到的是已经算好的 Δ)。
|
|
46
|
+
*/
|
|
47
|
+
declare function applySnapshot(table: SnapshotTable, meetings: readonly PredictionMeeting[], observedAt: string): {
|
|
48
|
+
readonly meetings: readonly PredictionMeeting[];
|
|
49
|
+
readonly next: SnapshotTable;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* 快照表的持久化句柄。
|
|
53
|
+
*
|
|
54
|
+
* `annotate()` 是唯一业务入口:**在锁内**读旧表、算 Δ、写新表——三个动作在同一
|
|
55
|
+
* 临界区里,两个宿主进程并发取数不会出现「A 读旧表、B 也读旧表、B 后写覆盖 A」。
|
|
56
|
+
* 锁超时抛 HomeFileLockTimeoutError(响亮),不静默按陈旧值覆盖。
|
|
57
|
+
*/
|
|
58
|
+
declare class SnapshotStore {
|
|
59
|
+
private readonly filePath;
|
|
60
|
+
constructor(options?: SnapshotStoreOptions);
|
|
61
|
+
path(): string;
|
|
62
|
+
/** 读当前表(只读;不改盘,锁内新鲜读)。 */
|
|
63
|
+
read(): Promise<SnapshotTable>;
|
|
64
|
+
/**
|
|
65
|
+
* 在锁内给本次会议补 Δ 并落盘下一次读数。
|
|
66
|
+
*
|
|
67
|
+
* 返回标注后的会议(调用方直接用它构造响应,Δ 一定与本次落盘同源)。
|
|
68
|
+
*/
|
|
69
|
+
annotate(meetings: readonly PredictionMeeting[], observedAt: string): Promise<readonly PredictionMeeting[]>;
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
export { SNAPSHOT_DIR, SNAPSHOT_FILE, SNAPSHOT_VERSION, SnapshotEntry, SnapshotStore, SnapshotStoreOptions, SnapshotTable, applySnapshot, defaultSnapshotPath, emptySnapshotTable, parseSnapshotTable };
|
package/lib/snapshot.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { SKIP_WRITE, dshHomeDir, transactStore } from "@dshtrading/dsh-home";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
//#region src/snapshot.ts
|
|
5
|
+
/**
|
|
6
|
+
* 本地价格快照(证据 Q3 的裁决产物):只为回答「这条概率自上次观察变了多少」。
|
|
7
|
+
*
|
|
8
|
+
* 为什么仍要本地快照(证据 Q3.1/Q3.2):上游 `clob.polymarket.com/prices-history`
|
|
9
|
+
* 虽然有服务端历史,但**只保留约 30 天**(显式 startTs/endTs 窗口 336h 可用、
|
|
10
|
+
* 720h 报 `interval is too long`),且需要按 token 扇出。本地快照是「近期 tick」,
|
|
11
|
+
* 与上游历史互补,不是替代。
|
|
12
|
+
*
|
|
13
|
+
* 存储位置(硬约束):trading home 下,经 `@dshtrading/dsh-home` 的 `dshHomeDir()`
|
|
14
|
+
* 解析——绝不写进包内或仓库数据。默认 `$DSH_HOME/polymarket/snapshots.json`。
|
|
15
|
+
*
|
|
16
|
+
* 体积(证据 Q3.5 实测 237.6 bytes/row):本实现只存**每档的最新一次读数**
|
|
17
|
+
* (不是 append-only 流水),所以文件大小与「会议数 × 档位数」成正比(约 10 条
|
|
18
|
+
* 会议 × 5 档 × ~200B ≈ 10 KiB),与采样频率无关。这避开了 append-only 在
|
|
19
|
+
* 1/分钟频率下 119 MiB/年/会议的量级。
|
|
20
|
+
*
|
|
21
|
+
* 跨进程:走 `transactStore`(全仓唯一临界区实现),两个宿主共用同一 home 时
|
|
22
|
+
* 不会 last-writer-wins 静默丢数据。
|
|
23
|
+
*/
|
|
24
|
+
/** 快照文件相对 trading home 的路径(POSIX 分隔符,join 时再拆)。 */
|
|
25
|
+
const SNAPSHOT_DIR = "polymarket";
|
|
26
|
+
const SNAPSHOT_FILE = "snapshots.json";
|
|
27
|
+
/** 快照表版本(结构变更时递增,旧版按空表处理而不是崩)。 */
|
|
28
|
+
const SNAPSHOT_VERSION = 1;
|
|
29
|
+
/** 默认快照文件路径:`$DSH_HOME/polymarket/snapshots.json`。 */
|
|
30
|
+
function defaultSnapshotPath(env = process.env) {
|
|
31
|
+
return join(dshHomeDir(env), SNAPSHOT_DIR, SNAPSHOT_FILE);
|
|
32
|
+
}
|
|
33
|
+
/** 空表(首次运行 / 结构版本不符 / 坏 JSON 的降级值——快照缺失不是错误)。 */
|
|
34
|
+
function emptySnapshotTable() {
|
|
35
|
+
return {
|
|
36
|
+
version: 1,
|
|
37
|
+
meetings: {}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function isRecord(value) {
|
|
41
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 解析磁盘上的快照表。
|
|
45
|
+
*
|
|
46
|
+
* **降级语义**:快照是纯增益信息(有没有它都不影响概率正确性),所以坏形状一律
|
|
47
|
+
* 降级为空表**不抛错**——与「上游取数失败必须响亮」刻意相反。取数失败会掩盖成
|
|
48
|
+
* 「没有数据」,快照失败则只是少一个 Δ 标注,两者不可混为一谈。
|
|
49
|
+
*/
|
|
50
|
+
function parseSnapshotTable(raw) {
|
|
51
|
+
if (!isRecord(raw) || raw.version !== 1 || !isRecord(raw.meetings)) return emptySnapshotTable();
|
|
52
|
+
const meetings = {};
|
|
53
|
+
for (const [eventId, buckets] of Object.entries(raw.meetings)) {
|
|
54
|
+
if (!isRecord(buckets)) continue;
|
|
55
|
+
const parsed = {};
|
|
56
|
+
for (const [key, entry] of Object.entries(buckets)) {
|
|
57
|
+
if (!isRecord(entry)) continue;
|
|
58
|
+
const probability = entry.probability;
|
|
59
|
+
const observedAt = entry.observedAt;
|
|
60
|
+
if (typeof probability !== "number" || !Number.isFinite(probability)) continue;
|
|
61
|
+
if (typeof observedAt !== "string" || observedAt.trim() === "") continue;
|
|
62
|
+
parsed[key] = {
|
|
63
|
+
probability,
|
|
64
|
+
observedAt
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
meetings[eventId] = parsed;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
version: 1,
|
|
71
|
+
meetings
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 给每档补 `changeSincePrevious`(与**上一次**读数的差),然后写入本次读数。
|
|
76
|
+
*
|
|
77
|
+
* 关键纪律(对应验收「失败路径不产生空数据假象」):
|
|
78
|
+
* - 首次采样 = 无历史 → `changeSincePrevious` **缺省**,绝不填 0(0 是「确知没变」,
|
|
79
|
+
* 与「不知道」是两件事);
|
|
80
|
+
* - 已结算档位同样写入(它们在页面上显示最终 0/1),但不再参与后续比较;
|
|
81
|
+
* - 写盘失败**不阻断取数**(调用方拿到的是已经算好的 Δ)。
|
|
82
|
+
*/
|
|
83
|
+
function applySnapshot(table, meetings, observedAt) {
|
|
84
|
+
const nextMeetings = { ...table.meetings };
|
|
85
|
+
return {
|
|
86
|
+
meetings: meetings.map((meeting) => {
|
|
87
|
+
const previous = table.meetings[meeting.id];
|
|
88
|
+
const buckets = meeting.buckets.map((bucket) => {
|
|
89
|
+
const before = previous?.[bucket.key];
|
|
90
|
+
return before === void 0 ? bucket : {
|
|
91
|
+
...bucket,
|
|
92
|
+
changeSincePrevious: Number((bucket.probability - before.probability).toFixed(6))
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
nextMeetings[meeting.id] = Object.fromEntries(buckets.map((bucket) => [bucket.key, {
|
|
96
|
+
probability: bucket.probability,
|
|
97
|
+
observedAt
|
|
98
|
+
}]));
|
|
99
|
+
return {
|
|
100
|
+
...meeting,
|
|
101
|
+
buckets
|
|
102
|
+
};
|
|
103
|
+
}),
|
|
104
|
+
next: {
|
|
105
|
+
version: 1,
|
|
106
|
+
meetings: nextMeetings
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const LOG_PREFIX = "[dsh-trading/connector-polymarket] failed to atomic flush snapshots to";
|
|
111
|
+
/** 新鲜读盘(锁内调用;坏内容降级为空表,见 {@link parseSnapshotTable})。 */
|
|
112
|
+
async function readTableFromDisk(filePath) {
|
|
113
|
+
let text;
|
|
114
|
+
try {
|
|
115
|
+
text = await readFile(filePath, "utf8");
|
|
116
|
+
} catch {
|
|
117
|
+
return emptySnapshotTable();
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
return parseSnapshotTable(JSON.parse(text));
|
|
121
|
+
} catch {
|
|
122
|
+
return emptySnapshotTable();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 快照表的持久化句柄。
|
|
127
|
+
*
|
|
128
|
+
* `annotate()` 是唯一业务入口:**在锁内**读旧表、算 Δ、写新表——三个动作在同一
|
|
129
|
+
* 临界区里,两个宿主进程并发取数不会出现「A 读旧表、B 也读旧表、B 后写覆盖 A」。
|
|
130
|
+
* 锁超时抛 HomeFileLockTimeoutError(响亮),不静默按陈旧值覆盖。
|
|
131
|
+
*/
|
|
132
|
+
var SnapshotStore = class {
|
|
133
|
+
filePath;
|
|
134
|
+
constructor(options = {}) {
|
|
135
|
+
this.filePath = options.filePath ?? defaultSnapshotPath(options.env ?? process.env);
|
|
136
|
+
}
|
|
137
|
+
path() {
|
|
138
|
+
return this.filePath;
|
|
139
|
+
}
|
|
140
|
+
/** 读当前表(只读;不改盘,锁内新鲜读)。 */
|
|
141
|
+
async read() {
|
|
142
|
+
let table = emptySnapshotTable();
|
|
143
|
+
await transactStore(this.filePath, () => readTableFromDisk(this.filePath), (onDisk) => {
|
|
144
|
+
table = onDisk;
|
|
145
|
+
return SKIP_WRITE;
|
|
146
|
+
}, (value) => value, LOG_PREFIX);
|
|
147
|
+
return table;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 在锁内给本次会议补 Δ 并落盘下一次读数。
|
|
151
|
+
*
|
|
152
|
+
* 返回标注后的会议(调用方直接用它构造响应,Δ 一定与本次落盘同源)。
|
|
153
|
+
*/
|
|
154
|
+
async annotate(meetings, observedAt) {
|
|
155
|
+
let annotated = meetings;
|
|
156
|
+
await transactStore(this.filePath, () => readTableFromDisk(this.filePath), (onDisk) => {
|
|
157
|
+
const applied = applySnapshot(onDisk, meetings, observedAt);
|
|
158
|
+
annotated = applied.meetings;
|
|
159
|
+
return applied.next;
|
|
160
|
+
}, (value) => value, LOG_PREFIX);
|
|
161
|
+
return annotated;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
//#endregion
|
|
165
|
+
export { SNAPSHOT_DIR, SNAPSHOT_FILE, SNAPSHOT_VERSION, SnapshotStore, applySnapshot, defaultSnapshotPath, emptySnapshotTable, parseSnapshotTable };
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dshtrading/connector-polymarket",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"description": "Polymarket 预测市场连接器(只读公开 API,无密钥):FOMC 决议档位与隐含概率,host 面 provide tradingPredictionFeed(跨市场信息源,归 base bundle)",
|
|
4
|
+
"version": "0.6.2",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "./lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"lib"
|
|
17
|
+
],
|
|
18
|
+
"license": "PolyForm-Noncommercial-1.0.0",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@dshtrading/api": "^0.6.2",
|
|
21
|
+
"@dshtrading/dsh-home": "^0.6.2"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@deepseek-ai/cordis": ">=4.0.0",
|
|
25
|
+
"@deepseek-ai/schemastery": ">=3.18.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"tsdown": "^0.22.0",
|
|
29
|
+
"vitest": "^3.0.0"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsdown",
|
|
33
|
+
"test": "vitest run"
|
|
34
|
+
}
|
|
6
35
|
}
|