@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.
- package/CHANGELOG.md +885 -692
- package/README.md +486 -400
- package/dist/agent/bench/bench.d.ts +59 -0
- package/dist/agent/bench/bench.js +319 -0
- package/dist/agent/bench/cassette.d.ts +97 -0
- package/dist/agent/bench/cassette.js +181 -0
- package/dist/agent/bench/costs.d.ts +28 -0
- package/dist/agent/bench/costs.js +60 -0
- package/dist/agent/bench/labelBuilder.d.ts +123 -0
- package/dist/agent/bench/labelBuilder.js +282 -0
- package/dist/agent/bench/labels.d.ts +75 -0
- package/dist/agent/bench/labels.js +259 -0
- package/dist/agent/bench/recordingClient.d.ts +51 -0
- package/dist/agent/bench/recordingClient.js +193 -0
- package/dist/agent/bench/replayClient.d.ts +41 -0
- package/dist/agent/bench/replayClient.js +262 -0
- package/dist/agent/bench/report.d.ts +115 -0
- package/dist/agent/bench/report.js +435 -0
- package/dist/agent/capitalSizing.d.ts +10 -1
- package/dist/agent/capitalSizing.js +28 -4
- package/dist/agent/cli.d.ts +25 -0
- package/dist/agent/cli.js +300 -2
- package/dist/agent/client.d.ts +14 -0
- package/dist/agent/client.js +23 -6
- package/dist/agent/configurationWarnings.js +11 -0
- package/dist/agent/decision.d.ts +24 -16
- package/dist/agent/decision.js +92 -3
- package/dist/agent/decisionValidator.d.ts +31 -0
- package/dist/agent/decisionValidator.js +148 -3
- package/dist/agent/engine.d.ts +5 -3
- package/dist/agent/engine.js +5 -3
- package/dist/agent/futuresCloseOutcome.d.ts +19 -0
- package/dist/agent/futuresCloseOutcome.js +54 -0
- package/dist/agent/futuresEligibility.d.ts +13 -0
- package/dist/agent/futuresEligibility.js +43 -5
- package/dist/agent/observe.d.ts +28 -2
- package/dist/agent/observe.js +530 -90
- package/dist/agent/pmPaperCandidates.d.ts +17 -0
- package/dist/agent/pmPaperCandidates.js +125 -0
- package/dist/agent/pmPaperCycle.d.ts +16 -0
- package/dist/agent/pmPaperCycle.js +372 -0
- package/dist/agent/pmPaperDecision.d.ts +187 -0
- package/dist/agent/pmPaperDecision.js +101 -0
- package/dist/agent/pmPaperPolicy.d.ts +71 -0
- package/dist/agent/pmPaperPolicy.js +46 -0
- package/dist/agent/prompt.d.ts +15 -1
- package/dist/agent/prompt.js +187 -9
- package/dist/agent/promptTables.d.ts +1 -0
- package/dist/agent/promptTables.js +33 -0
- package/dist/agent/providerCapabilities.d.ts +7 -0
- package/dist/agent/providerCapabilities.js +27 -1
- package/dist/agent/providers.d.ts +26 -7
- package/dist/agent/providers.js +193 -48
- package/dist/agent/resolve.js +3 -0
- package/dist/agent/runner.d.ts +17 -1
- package/dist/agent/runner.js +210 -38
- package/dist/agent/setups.d.ts +2 -1
- package/dist/agent/setups.js +17 -16
- package/dist/agent/signals.d.ts +33 -0
- package/dist/agent/signals.js +89 -0
- package/dist/agent/skill.js +46 -0
- package/dist/agent/skillValidator.js +56 -0
- package/dist/agent/strictLint.js +30 -0
- package/dist/agent/templates.js +30 -30
- package/dist/agent/types.d.ts +180 -12
- package/dist/agent/universe.d.ts +78 -0
- package/dist/agent/universe.js +215 -0
- package/dist/agent/version.d.ts +1 -1
- package/dist/agent/version.js +1 -1
- package/dist/client.d.ts +14 -0
- package/dist/client.js +31 -1
- package/dist/http.js +1 -1
- package/dist/pmPaperV2.d.ts +81 -0
- package/dist/pmPaperV2.js +53 -0
- package/dist/tools.js +84 -6
- 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;
|