@coinrithm/mcp-trading 0.7.15 → 0.7.17

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.
Files changed (76) hide show
  1. package/CHANGELOG.md +885 -692
  2. package/README.md +486 -400
  3. package/dist/agent/bench/bench.d.ts +59 -0
  4. package/dist/agent/bench/bench.js +319 -0
  5. package/dist/agent/bench/cassette.d.ts +97 -0
  6. package/dist/agent/bench/cassette.js +181 -0
  7. package/dist/agent/bench/costs.d.ts +28 -0
  8. package/dist/agent/bench/costs.js +60 -0
  9. package/dist/agent/bench/labelBuilder.d.ts +123 -0
  10. package/dist/agent/bench/labelBuilder.js +282 -0
  11. package/dist/agent/bench/labels.d.ts +75 -0
  12. package/dist/agent/bench/labels.js +259 -0
  13. package/dist/agent/bench/recordingClient.d.ts +51 -0
  14. package/dist/agent/bench/recordingClient.js +193 -0
  15. package/dist/agent/bench/replayClient.d.ts +41 -0
  16. package/dist/agent/bench/replayClient.js +262 -0
  17. package/dist/agent/bench/report.d.ts +115 -0
  18. package/dist/agent/bench/report.js +435 -0
  19. package/dist/agent/capitalSizing.d.ts +10 -1
  20. package/dist/agent/capitalSizing.js +28 -4
  21. package/dist/agent/cli.d.ts +25 -0
  22. package/dist/agent/cli.js +300 -2
  23. package/dist/agent/client.d.ts +14 -0
  24. package/dist/agent/client.js +23 -6
  25. package/dist/agent/configurationWarnings.js +11 -0
  26. package/dist/agent/decision.d.ts +24 -16
  27. package/dist/agent/decision.js +92 -3
  28. package/dist/agent/decisionValidator.d.ts +31 -0
  29. package/dist/agent/decisionValidator.js +148 -3
  30. package/dist/agent/engine.d.ts +5 -3
  31. package/dist/agent/engine.js +5 -3
  32. package/dist/agent/futuresCloseOutcome.d.ts +19 -0
  33. package/dist/agent/futuresCloseOutcome.js +54 -0
  34. package/dist/agent/futuresEligibility.d.ts +13 -0
  35. package/dist/agent/futuresEligibility.js +43 -5
  36. package/dist/agent/observe.d.ts +28 -2
  37. package/dist/agent/observe.js +530 -90
  38. package/dist/agent/pmPaperCandidates.d.ts +17 -0
  39. package/dist/agent/pmPaperCandidates.js +125 -0
  40. package/dist/agent/pmPaperCycle.d.ts +16 -0
  41. package/dist/agent/pmPaperCycle.js +372 -0
  42. package/dist/agent/pmPaperDecision.d.ts +187 -0
  43. package/dist/agent/pmPaperDecision.js +101 -0
  44. package/dist/agent/pmPaperPolicy.d.ts +71 -0
  45. package/dist/agent/pmPaperPolicy.js +46 -0
  46. package/dist/agent/prompt.d.ts +15 -1
  47. package/dist/agent/prompt.js +187 -9
  48. package/dist/agent/promptTables.d.ts +1 -0
  49. package/dist/agent/promptTables.js +33 -0
  50. package/dist/agent/providerCapabilities.d.ts +7 -0
  51. package/dist/agent/providerCapabilities.js +27 -1
  52. package/dist/agent/providers.d.ts +26 -7
  53. package/dist/agent/providers.js +193 -48
  54. package/dist/agent/resolve.js +3 -0
  55. package/dist/agent/runner.d.ts +17 -1
  56. package/dist/agent/runner.js +210 -38
  57. package/dist/agent/setups.d.ts +2 -1
  58. package/dist/agent/setups.js +17 -16
  59. package/dist/agent/signals.d.ts +33 -0
  60. package/dist/agent/signals.js +89 -0
  61. package/dist/agent/skill.js +46 -0
  62. package/dist/agent/skillValidator.js +56 -0
  63. package/dist/agent/strictLint.js +30 -0
  64. package/dist/agent/templates.js +30 -30
  65. package/dist/agent/types.d.ts +180 -12
  66. package/dist/agent/universe.d.ts +78 -0
  67. package/dist/agent/universe.js +215 -0
  68. package/dist/agent/version.d.ts +1 -1
  69. package/dist/agent/version.js +1 -1
  70. package/dist/client.d.ts +14 -0
  71. package/dist/client.js +31 -1
  72. package/dist/http.js +1 -1
  73. package/dist/pmPaperV2.d.ts +81 -0
  74. package/dist/pmPaperV2.js +53 -0
  75. package/dist/tools.js +84 -6
  76. package/package.json +100 -97
@@ -0,0 +1,28 @@
1
+ /** Fee fraction of the price paid at a 50% PM price (openapi.yaml: ~1.8%). */
2
+ export declare const PM_SYNTHETIC_FEE_RATE_AT_MID = 0.018;
3
+ /** Flat futures taker fee (bps of notional) used for synthesized quotes. */
4
+ export declare const FUTURES_SYNTHETIC_FEE_BPS = 10;
5
+ /** Flat spot taker fee (bps of notional) used for synthesized quotes. */
6
+ export declare const SPOT_SYNTHETIC_FEE_BPS = 10;
7
+ /**
8
+ * Synthetic PM fee in probability POINTS for an outcome priced at
9
+ * `probabilityPoints` (0..100). Zero at the extremes, largest at 50.
10
+ */
11
+ export declare function pmSyntheticFeePoints(probabilityPoints: number): number;
12
+ export interface SyntheticPmQuote {
13
+ /** Raw chosen-outcome probability in points: the value the floor checks. */
14
+ entryProbability: number;
15
+ feePoints: number;
16
+ /** Fee-inclusive break-even cost in points (entry + fee). */
17
+ costPoints: number;
18
+ stakeMusd: number;
19
+ /** Net shares: stake / (cost / 100). One share pays 1 mUSD on a win. */
20
+ sharesEstimate: number;
21
+ }
22
+ /**
23
+ * The PM quote the bench serves for a recorded outcome probability. Returns
24
+ * undefined for a probability the paper engine could not fill (0, 100, or not
25
+ * a finite number in range) or a non-positive stake, so the caller reports an
26
+ * ineligible quote instead of dividing by zero.
27
+ */
28
+ export declare function synthesizePmQuote(probabilityPoints: number, stakeMusd: number): SyntheticPmQuote | undefined;
@@ -0,0 +1,60 @@
1
+ // Synthetic execution costs for the bench's replayed quotes.
2
+ //
3
+ // A quote depends on the action the agent proposes (which outcome, which stake,
4
+ // which leverage), so it can never be recorded ahead of time: a cassette only
5
+ // holds the READ inputs of a cycle. On replay the bench therefore synthesizes
6
+ // every quote from recorded evidence (the discover probability of the chosen PM
7
+ // outcome, the recorded market price of a coin) plus the cost model below. Every
8
+ // synthesized quote is counted in the report; it is a fidelity limit, not a
9
+ // silent substitute for the paper engine.
10
+ //
11
+ // What the cost model covers and what it leaves out, stated plainly:
12
+ // - PM: the published fee SHAPE only. openapi.yaml ("Paper Execution Realism
13
+ // v1") documents a Polymarket-shaped taker fee of about 1.8% near a 50%
14
+ // price and about 0 at the extremes. That is modelled as
15
+ // PM_SYNTHETIC_FEE_RATE_AT_MID * 4p(1-p) of the price paid. The ask spread
16
+ // and the size-scaled slippage the paper engine adds are NOT modelled, so a
17
+ // synthesized PM cost is optimistic by an unmeasured amount.
18
+ // - Futures and spot: a flat taker fee on notional. The paper engine discloses
19
+ // its own feeBps per quote, which a cassette cannot contain, so the bench
20
+ // uses the runner's own conservative pre-quote estimate
21
+ // (CAPITAL_FEE_BUFFER_BPS). Funding, spread and slippage are not modelled.
22
+ import { CAPITAL_FEE_BUFFER_BPS } from "../capitalSizing.js";
23
+ /** Fee fraction of the price paid at a 50% PM price (openapi.yaml: ~1.8%). */
24
+ export const PM_SYNTHETIC_FEE_RATE_AT_MID = 0.018;
25
+ /** Flat futures taker fee (bps of notional) used for synthesized quotes. */
26
+ export const FUTURES_SYNTHETIC_FEE_BPS = CAPITAL_FEE_BUFFER_BPS;
27
+ /** Flat spot taker fee (bps of notional) used for synthesized quotes. */
28
+ export const SPOT_SYNTHETIC_FEE_BPS = CAPITAL_FEE_BUFFER_BPS;
29
+ /**
30
+ * Synthetic PM fee in probability POINTS for an outcome priced at
31
+ * `probabilityPoints` (0..100). Zero at the extremes, largest at 50.
32
+ */
33
+ export function pmSyntheticFeePoints(probabilityPoints) {
34
+ const p = probabilityPoints / 100;
35
+ return probabilityPoints * PM_SYNTHETIC_FEE_RATE_AT_MID * 4 * p * (1 - p);
36
+ }
37
+ /**
38
+ * The PM quote the bench serves for a recorded outcome probability. Returns
39
+ * undefined for a probability the paper engine could not fill (0, 100, or not
40
+ * a finite number in range) or a non-positive stake, so the caller reports an
41
+ * ineligible quote instead of dividing by zero.
42
+ */
43
+ export function synthesizePmQuote(probabilityPoints, stakeMusd) {
44
+ if (!Number.isFinite(probabilityPoints) ||
45
+ probabilityPoints <= 0 ||
46
+ probabilityPoints >= 100 ||
47
+ !Number.isFinite(stakeMusd) ||
48
+ stakeMusd <= 0) {
49
+ return undefined;
50
+ }
51
+ const feePoints = pmSyntheticFeePoints(probabilityPoints);
52
+ const costPoints = probabilityPoints + feePoints;
53
+ return {
54
+ entryProbability: probabilityPoints,
55
+ feePoints,
56
+ costPoints,
57
+ stakeMusd,
58
+ sharesEstimate: stakeMusd / (costPoints / 100),
59
+ };
60
+ }
@@ -0,0 +1,123 @@
1
+ import { ApiResult } from "../types.js";
2
+ import { Cassette } from "./cassette.js";
3
+ import { LabelFile, PriceBar } from "./labels.js";
4
+ export declare const LABEL_RANGES: readonly [{
5
+ readonly range: "1D";
6
+ readonly spanSeconds: 86400;
7
+ readonly barSeconds: 300;
8
+ }, {
9
+ readonly range: "1W";
10
+ readonly spanSeconds: number;
11
+ readonly barSeconds: 900;
12
+ }, {
13
+ readonly range: "1M";
14
+ readonly spanSeconds: number;
15
+ readonly barSeconds: 3600;
16
+ }, {
17
+ readonly range: "3M";
18
+ readonly spanSeconds: number;
19
+ readonly barSeconds: 14400;
20
+ }];
21
+ export declare const DEFAULT_LABEL_HORIZON_HOURS = 24;
22
+ export type PriceLabelPlan = {
23
+ status: "ready";
24
+ range: string;
25
+ barSeconds: number;
26
+ fromSec: number;
27
+ toSec: number;
28
+ } | {
29
+ status: "not_yet";
30
+ labelableAfter: string;
31
+ } | {
32
+ status: "too_old";
33
+ };
34
+ /**
35
+ * Which candle range can label [asOf, asOf + horizon] right now. The horizon
36
+ * must have fully elapsed, and the range window (ending now) must still hold
37
+ * the bar that contains asOf.
38
+ */
39
+ export declare function planPriceLabels(asOf: string, horizonHours: number, nowMs: number): PriceLabelPlan;
40
+ /** The coins a cassette observed: its recorded GET /api/agent/market/:id reads. */
41
+ export declare function cassetteCoins(cassette: Pick<Cassette, "responses">): Array<{
42
+ coinId: string;
43
+ symbol: string;
44
+ }>;
45
+ /**
46
+ * Bars from a candles response that can matter for [fromSec, toSec]: the bar
47
+ * containing asOf onward (the scorer itself ignores every bar at or before
48
+ * asOf) through the last bar that CLOSES by toSec, finite OHLC only, oldest
49
+ * first. A bar straddling toSec (it may hold prices after the horizon) or
50
+ * still open when labelling runs is never stored.
51
+ */
52
+ export declare function priceBarsFor(data: unknown, fromSec: number, toSec: number, barSeconds: number): PriceBar[];
53
+ export type CassetteLabelResult = {
54
+ id: string;
55
+ status: "not_yet";
56
+ labelableAfter: string;
57
+ } | {
58
+ id: string;
59
+ status: "too_old";
60
+ } | {
61
+ id: string;
62
+ status: "kept";
63
+ } | {
64
+ id: string;
65
+ status: "horizon_mismatch";
66
+ existingHorizonHours: number | null;
67
+ } | {
68
+ id: string;
69
+ status: "no_coins";
70
+ } | {
71
+ id: string;
72
+ status: "built";
73
+ file: LabelFile;
74
+ range: string;
75
+ symbols: string[];
76
+ missing: string[];
77
+ };
78
+ /**
79
+ * Price labels for each cassette. `fetchCandles` is the agent API candles
80
+ * read; one fetch per coin and range serves every cassette. An existing label
81
+ * file keeps its PM and funding labels; its prices are replaced only with
82
+ * `overwrite`. A symbol whose read fails or returns no bar in the window is
83
+ * reported missing and stays unlabelled.
84
+ */
85
+ export declare function buildPriceLabels(cassettes: ReadonlyArray<Pick<Cassette, "id" | "asOf" | "responses">>, opts: {
86
+ fetchCandles: (coinId: string, range: string) => Promise<ApiResult>;
87
+ horizonHours?: number;
88
+ nowMs: number;
89
+ existing?: Record<string, LabelFile>;
90
+ overwrite?: boolean;
91
+ }): Promise<CassetteLabelResult[]>;
92
+ /** The PM events a cassette discovered (its recorded /pm/discover reads). */
93
+ export declare function cassettePmEvents(cassette: Pick<Cassette, "responses">): Array<{
94
+ source: string;
95
+ slug: string;
96
+ }>;
97
+ /**
98
+ * Pure: settled labels from one public event-detail body, keyed
99
+ * `source/slug/outcomeExternalMarketId`. Empty unless the platform's own
100
+ * verdict says the event settles and the outcome has a provider result.
101
+ */
102
+ export declare function pmVerdictLabels(source: string, slug: string, body: unknown): Record<string, {
103
+ settled: 0 | 1;
104
+ }>;
105
+ export type CassettePmResult = {
106
+ id: string;
107
+ events: number;
108
+ labelled: number;
109
+ failed: number;
110
+ pm: Record<string, {
111
+ settled: 0 | 1;
112
+ }>;
113
+ };
114
+ /**
115
+ * Settled PM labels per cassette from the public event verdict. One read per
116
+ * event serves every cassette; a failed or unusable read leaves that event
117
+ * unlabelled and is counted. Existing PM labels are kept unless `overwrite`.
118
+ */
119
+ export declare function buildPmLabels(cassettes: ReadonlyArray<Pick<Cassette, "id" | "responses">>, opts: {
120
+ fetchEvent: (source: string, slug: string) => Promise<ApiResult>;
121
+ existing?: Record<string, LabelFile>;
122
+ overwrite?: boolean;
123
+ }): Promise<CassettePmResult[]>;
@@ -0,0 +1,282 @@
1
+ // Outcome labels for bench cassettes, built from data published AFTER each
2
+ // cassette's asOf (C2 of the item-3 bench plan). Reads only; never writes to
3
+ // the API.
4
+ //
5
+ // Prices: OHLC bars from GET /api/agent/market/:coinId/candles, the same
6
+ // loader and shape as the public /api/coins/:slug/candles (probed live
7
+ // 2026-10-07: range=1D -> 288 five-minute bars {t, o, h, l, c, v}, t = bar
8
+ // open in unix seconds). Candle storage keeps fine bars only briefly (about
9
+ // 24 h of 5-minute bars, 7 days of 15-minute, 30 days of hourly), so the bar
10
+ // size follows the cassette's age: the finest range whose window still covers
11
+ // asOf. A label file has one bar size (barSeconds).
12
+ //
13
+ // PM settlement comes from the platform's own public event verdict (see the
14
+ // PM section below), never a rule rebuilt here; anything short of a settled
15
+ // provider result stays "unlabelled".
16
+ export const LABEL_RANGES = [
17
+ { range: "1D", spanSeconds: 86_400, barSeconds: 300 },
18
+ { range: "1W", spanSeconds: 7 * 86_400, barSeconds: 900 },
19
+ { range: "1M", spanSeconds: 30 * 86_400, barSeconds: 3_600 },
20
+ { range: "3M", spanSeconds: 90 * 86_400, barSeconds: 14_400 },
21
+ ];
22
+ export const DEFAULT_LABEL_HORIZON_HOURS = 24;
23
+ /**
24
+ * Which candle range can label [asOf, asOf + horizon] right now. The horizon
25
+ * must have fully elapsed, and the range window (ending now) must still hold
26
+ * the bar that contains asOf.
27
+ */
28
+ export function planPriceLabels(asOf, horizonHours, nowMs) {
29
+ const asOfMs = Date.parse(asOf);
30
+ if (!Number.isFinite(asOfMs))
31
+ throw new Error(`invalid asOf "${asOf}"`);
32
+ if (!Number.isFinite(horizonHours) || horizonHours <= 0)
33
+ throw new Error("horizon hours must be a positive number");
34
+ const fromSec = Math.floor(asOfMs / 1000);
35
+ const toSec = fromSec + Math.round(horizonHours * 3600);
36
+ const nowSec = Math.floor(nowMs / 1000);
37
+ if (toSec > nowSec)
38
+ return {
39
+ status: "not_yet",
40
+ labelableAfter: new Date(toSec * 1000).toISOString(),
41
+ };
42
+ const age = nowSec - fromSec;
43
+ const pick = LABEL_RANGES.find((r) => age + r.barSeconds <= r.spanSeconds);
44
+ return pick
45
+ ? {
46
+ status: "ready",
47
+ range: pick.range,
48
+ barSeconds: pick.barSeconds,
49
+ fromSec,
50
+ toSec,
51
+ }
52
+ : { status: "too_old" };
53
+ }
54
+ const isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
55
+ /** The coins a cassette observed: its recorded GET /api/agent/market/:id reads. */
56
+ export function cassetteCoins(cassette) {
57
+ const out = new Map();
58
+ for (const r of cassette.responses) {
59
+ const m = /^\/api\/agent\/market\/([^/]+)$/.exec(r.path);
60
+ if (!m || r.method !== "GET" || !r.ok)
61
+ continue;
62
+ const coin = isObject(r.data) ? r.data.coin : undefined;
63
+ const symbol = isObject(coin) ? coin.symbol : undefined;
64
+ if (typeof symbol === "string" && symbol.trim())
65
+ out.set(decodeURIComponent(m[1]), symbol.trim().toUpperCase());
66
+ }
67
+ return [...out].map(([coinId, symbol]) => ({ coinId, symbol }));
68
+ }
69
+ /**
70
+ * Bars from a candles response that can matter for [fromSec, toSec]: the bar
71
+ * containing asOf onward (the scorer itself ignores every bar at or before
72
+ * asOf) through the last bar that CLOSES by toSec, finite OHLC only, oldest
73
+ * first. A bar straddling toSec (it may hold prices after the horizon) or
74
+ * still open when labelling runs is never stored.
75
+ */
76
+ export function priceBarsFor(data, fromSec, toSec, barSeconds) {
77
+ const rows = isObject(data) && Array.isArray(data.candles) ? data.candles : [];
78
+ const bars = [];
79
+ for (const row of rows) {
80
+ if (!isObject(row))
81
+ continue;
82
+ const { t, h, l, c } = row;
83
+ if ([t, h, l, c].every((v) => typeof v === "number" && Number.isFinite(v)) &&
84
+ t > fromSec - barSeconds &&
85
+ t + barSeconds <= toSec)
86
+ bars.push({
87
+ t: t,
88
+ h: h,
89
+ l: l,
90
+ c: c,
91
+ });
92
+ }
93
+ return bars.sort((a, b) => a.t - b.t);
94
+ }
95
+ /**
96
+ * Price labels for each cassette. `fetchCandles` is the agent API candles
97
+ * read; one fetch per coin and range serves every cassette. An existing label
98
+ * file keeps its PM and funding labels; its prices are replaced only with
99
+ * `overwrite`. A symbol whose read fails or returns no bar in the window is
100
+ * reported missing and stays unlabelled.
101
+ */
102
+ export async function buildPriceLabels(cassettes, opts) {
103
+ const horizonHours = opts.horizonHours ?? DEFAULT_LABEL_HORIZON_HOURS;
104
+ const cache = new Map();
105
+ const read = (coinId, range) => {
106
+ const key = `${coinId}|${range}`;
107
+ let hit = cache.get(key);
108
+ if (!hit) {
109
+ hit = opts
110
+ .fetchCandles(coinId, range)
111
+ .catch(() => ({ ok: false, status: 0, data: null }));
112
+ cache.set(key, hit);
113
+ }
114
+ return hit;
115
+ };
116
+ const results = [];
117
+ for (const cassette of cassettes) {
118
+ const existing = opts.existing?.[cassette.id];
119
+ if (existing?.prices && !opts.overwrite) {
120
+ // An existing file labelled for another horizon is never reported as
121
+ // done for this one: rebuilding it needs --overwrite.
122
+ results.push(existing.horizonHours === horizonHours
123
+ ? { id: cassette.id, status: "kept" }
124
+ : {
125
+ id: cassette.id,
126
+ status: "horizon_mismatch",
127
+ existingHorizonHours: existing.horizonHours ?? null,
128
+ });
129
+ continue;
130
+ }
131
+ const plan = planPriceLabels(cassette.asOf, horizonHours, opts.nowMs);
132
+ if (plan.status !== "ready") {
133
+ results.push({ id: cassette.id, ...plan });
134
+ continue;
135
+ }
136
+ const coins = cassetteCoins(cassette);
137
+ if (coins.length === 0) {
138
+ results.push({ id: cassette.id, status: "no_coins" });
139
+ continue;
140
+ }
141
+ const prices = {};
142
+ const missing = [];
143
+ for (const { coinId, symbol } of coins) {
144
+ const res = await read(coinId, plan.range);
145
+ const bars = res.ok
146
+ ? priceBarsFor(res.data, plan.fromSec, plan.toSec, plan.barSeconds)
147
+ : [];
148
+ if (bars.length > 0)
149
+ prices[symbol] = bars;
150
+ else
151
+ missing.push(symbol);
152
+ }
153
+ const { prices: _old, barSeconds: _bar, horizonHours: _h, ...kept } = existing ?? {};
154
+ results.push({
155
+ id: cassette.id,
156
+ status: "built",
157
+ range: plan.range,
158
+ symbols: Object.keys(prices).sort(),
159
+ missing: missing.sort(),
160
+ file: {
161
+ ...kept,
162
+ prices,
163
+ barSeconds: plan.barSeconds,
164
+ horizonHours,
165
+ },
166
+ });
167
+ }
168
+ return results;
169
+ }
170
+ // ── PM settlement labels (root 57074) ──────────────────────────────────────
171
+ // From the EXISTING public event read GET /api/prediction-markets/events/
172
+ // :source/:slug, never an invented verdict: an outcome is labelled only when
173
+ // the event's settlementTrust says settlementEligible AND limboVerdict
174
+ // "settle", and the outcome's own lifecycle is a provider result (isResult,
175
+ // basis "provider", result won|lost). Probed live 2026-10-07: a Kalshi ladder
176
+ // (1 won / 74 lost, eligible, settle) and a ForecastEx event whose winner is
177
+ // known but whose time is unverified (eligible false, limbo "void"), which
178
+ // stays unlabelled. A pm_open buys its named outcome, so settled = won.
179
+ /** The PM events a cassette discovered (its recorded /pm/discover reads). */
180
+ export function cassettePmEvents(cassette) {
181
+ const out = new Map();
182
+ for (const r of cassette.responses) {
183
+ if (r.path !== "/api/agent/pm/discover" || r.method !== "GET" || !r.ok)
184
+ continue;
185
+ const rows = isObject(r.data) && Array.isArray(r.data.data) ? r.data.data : [];
186
+ for (const ev of rows) {
187
+ if (!isObject(ev))
188
+ continue;
189
+ const { source, slug } = ev;
190
+ if (typeof source !== "string" || typeof slug !== "string")
191
+ continue;
192
+ const s = source.trim().toLowerCase();
193
+ const g = slug.trim().toLowerCase();
194
+ if (s && g)
195
+ out.set(`${s}/${g}`, { source: s, slug: g });
196
+ }
197
+ }
198
+ return [...out.values()];
199
+ }
200
+ /**
201
+ * Pure: settled labels from one public event-detail body, keyed
202
+ * `source/slug/outcomeExternalMarketId`. Empty unless the platform's own
203
+ * verdict says the event settles and the outcome has a provider result.
204
+ */
205
+ export function pmVerdictLabels(source, slug, body) {
206
+ const out = {};
207
+ if (!isObject(body))
208
+ return out;
209
+ const resolution = isObject(body.resolution) ? body.resolution : undefined;
210
+ const trust = resolution && isObject(resolution.settlementTrust)
211
+ ? resolution.settlementTrust
212
+ : undefined;
213
+ if (!trust ||
214
+ trust.settlementEligible !== true ||
215
+ trust.limboVerdict !== "settle")
216
+ return out;
217
+ const event = isObject(body.event) ? body.event : undefined;
218
+ const outcomes = event && Array.isArray(event.outcomes) ? event.outcomes : [];
219
+ for (const o of outcomes) {
220
+ if (!isObject(o) || typeof o.externalMarketId !== "string")
221
+ continue;
222
+ const lc = isObject(o.lifecycle) ? o.lifecycle : undefined;
223
+ if (!lc ||
224
+ lc.isResult !== true ||
225
+ lc.basis !== "provider" ||
226
+ (lc.result !== "won" && lc.result !== "lost"))
227
+ continue;
228
+ out[`${source}/${slug}/${o.externalMarketId}`] = {
229
+ settled: lc.result === "won" ? 1 : 0,
230
+ };
231
+ }
232
+ return out;
233
+ }
234
+ /**
235
+ * Settled PM labels per cassette from the public event verdict. One read per
236
+ * event serves every cassette; a failed or unusable read leaves that event
237
+ * unlabelled and is counted. Existing PM labels are kept unless `overwrite`.
238
+ */
239
+ export async function buildPmLabels(cassettes, opts) {
240
+ const cache = new Map();
241
+ const read = (source, slug) => {
242
+ const key = `${source}/${slug}`;
243
+ let hit = cache.get(key);
244
+ if (!hit) {
245
+ hit = opts
246
+ .fetchEvent(source, slug)
247
+ .catch(() => ({ ok: false, status: 0, data: null }));
248
+ cache.set(key, hit);
249
+ }
250
+ return hit;
251
+ };
252
+ const results = [];
253
+ for (const cassette of cassettes) {
254
+ const events = cassettePmEvents(cassette);
255
+ const pm = opts.overwrite
256
+ ? {}
257
+ : { ...(opts.existing?.[cassette.id]?.pm ?? {}) };
258
+ let failed = 0;
259
+ let labelled = 0;
260
+ for (const { source, slug } of events) {
261
+ const res = await read(source, slug);
262
+ if (!res.ok) {
263
+ failed += 1;
264
+ continue;
265
+ }
266
+ for (const [key, label] of Object.entries(pmVerdictLabels(source, slug, res.data))) {
267
+ if (!opts.overwrite && pm[key] !== undefined)
268
+ continue;
269
+ pm[key] = label;
270
+ labelled += 1;
271
+ }
272
+ }
273
+ results.push({
274
+ id: cassette.id,
275
+ events: events.length,
276
+ labelled,
277
+ failed,
278
+ pm,
279
+ });
280
+ }
281
+ return results;
282
+ }
@@ -0,0 +1,75 @@
1
+ import { ProposedAction, QuoteEvidence } from "../types.js";
2
+ export declare const DEFAULT_BAR_SECONDS = 300;
3
+ export interface PriceBar {
4
+ t: number;
5
+ h: number;
6
+ l: number;
7
+ c: number;
8
+ }
9
+ export interface FundingEvent {
10
+ t: number;
11
+ rate: number;
12
+ }
13
+ export interface LabelFile {
14
+ pm?: Record<string, {
15
+ settled: 0 | 1;
16
+ }>;
17
+ prices?: Record<string, PriceBar[]>;
18
+ barSeconds?: number;
19
+ funding?: Record<string, FundingEvent[]>;
20
+ horizonHours?: number;
21
+ }
22
+ export type ActionScore = {
23
+ status: "not_scored";
24
+ reason: string;
25
+ } | {
26
+ status: "unlabelled";
27
+ reason: string;
28
+ } | {
29
+ status: "unlabelled_gap";
30
+ reason: string;
31
+ } | {
32
+ status: "labelled";
33
+ venue: "pm";
34
+ settled: 0 | 1;
35
+ /** Fee-inclusive cost per share, 0..1 (stake / net shares). */
36
+ cost: number;
37
+ /** settled - cost: P&L per share bought. */
38
+ pnlPerShare: number;
39
+ /** (settled - cost) / cost: P&L per 1 mUSD staked. */
40
+ returnOnStake: number;
41
+ pnlMusd: number;
42
+ /** Brier of the agent's own forecast; absent when it sent none. */
43
+ brier?: number;
44
+ /** Brier of the market's raw entry probability on the same outcome. */
45
+ marketBrier?: number;
46
+ } | {
47
+ status: "labelled";
48
+ venue: "futures";
49
+ basis: "ohlc_walk_model";
50
+ exit: "stop" | "take_profit" | "liquidation" | "horizon";
51
+ exitPrice: number;
52
+ /** One bar touched both stop and target; resolved as the stop. */
53
+ sameBarStopAndTarget: boolean;
54
+ /** The horizon was not on a bar boundary: the walk ended at the last
55
+ * full bar before it (never at a close after the horizon). */
56
+ horizonFlooredToBar: boolean;
57
+ fundingIncluded: boolean;
58
+ /** After fees (and funding when included), fraction of margin, >= -1. */
59
+ returnOnMargin: number;
60
+ pnlMusd: number;
61
+ };
62
+ /** Parse and validate a label file. Throws on a malformed file (fail closed). */
63
+ export declare function parseLabelFile(value: unknown, source: string): LabelFile;
64
+ /**
65
+ * Score one ACCEPTED action. Only risk-increasing PM and futures opens are
66
+ * scored in this slice; closes, protection updates, cancels and spot orders
67
+ * are "not_scored". An open with no matching label is "unlabelled"; a futures
68
+ * open whose bar path has holes is "unlabelled_gap".
69
+ */
70
+ export declare function scoreAction(input: {
71
+ action: ProposedAction;
72
+ quote?: QuoteEvidence;
73
+ asOf: string;
74
+ labels?: LabelFile;
75
+ }): ActionScore;