@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,262 @@
1
+ // Replay side of the bench: a real CoinRithmClient whose transport serves a
2
+ // cassette instead of the network.
3
+ //
4
+ // Reads are served byte-for-byte from the cassette by canonical request key.
5
+ // A read that is NOT in the cassette is never filled in: it answers 599
6
+ // not_recorded and is counted as a missing input, so a variant that needs
7
+ // data the recording never fetched (a different watchlist, a capability the
8
+ // recorded agent lacked) shows up in the report instead of silently seeing
9
+ // an empty market.
10
+ //
11
+ // Quotes are the one thing a cassette cannot hold, because they depend on the
12
+ // action the variant proposes. They are synthesized deterministically from
13
+ // recorded evidence (costs.ts) and every one is counted. Any other non-GET is
14
+ // refused with 599 bench_write_refused: replay runs dry-run, so a write
15
+ // attempt means a runner change the bench must surface, not forward.
16
+ import { CoinRithmClient } from "../client.js";
17
+ import { asArr, asNum, asObj, asStr } from "../extract.js";
18
+ import { freshnessOf } from "../pmContext.js";
19
+ import { canonicalRequest } from "./cassette.js";
20
+ import { FUTURES_SYNTHETIC_FEE_BPS, SPOT_SYNTHETIC_FEE_BPS, synthesizePmQuote, } from "./costs.js";
21
+ import { jsonResponse, urlOf, writeRefusedResponse, } from "./recordingClient.js";
22
+ export const REPLAY_BASE_URL = "https://bench-replay.invalid";
23
+ export const NOT_RECORDED_STATUS = 599;
24
+ const NULL_BODY_STATUS = new Set([204, 205, 304]);
25
+ // Each client gets its own credential, so observe()'s per-credential
26
+ // calibration cache can never hand one cassette's read to another.
27
+ let replayNonce = 0;
28
+ /**
29
+ * The chosen PM outcome as the recording's discover reads reported it. Every
30
+ * recorded discover page is searched in key order (deterministic); the first
31
+ * match wins. Mirrors observe()'s expandPmMarkets field fallbacks.
32
+ */
33
+ export function findRecordedPmOutcome(cassette, source, slug, outcomeExternalMarketId) {
34
+ for (const r of cassette.responses) {
35
+ if (r.path !== "/api/agent/pm/discover" || !r.ok)
36
+ continue;
37
+ const page = asObj(r.data);
38
+ for (const event of asArr(page.data ?? page.markets ?? page.results)) {
39
+ const ev = asObj(event);
40
+ if ((asStr(ev.source) ?? "").toLowerCase() !== source)
41
+ continue;
42
+ if ((asStr(ev.slug) ?? "").toLowerCase() !== slug)
43
+ continue;
44
+ const outcomes = Object.hasOwn(ev, "outcomes")
45
+ ? asArr(ev.outcomes)
46
+ : [ev];
47
+ for (const outcome of outcomes) {
48
+ const o = asObj(outcome);
49
+ const id = asStr(o.externalMarketId) ?? asStr(o.outcomeExternalMarketId);
50
+ if (id !== outcomeExternalMarketId)
51
+ continue;
52
+ const p = asNum(o.probability);
53
+ return {
54
+ probability: p !== undefined && p >= 0 && p <= 100 ? p : undefined,
55
+ freshness: freshnessOf(ev)?.status ?? "fresh",
56
+ eligible: ev.eligible !== false && o.eligible !== false,
57
+ };
58
+ }
59
+ }
60
+ }
61
+ return undefined;
62
+ }
63
+ /** The recorded /market price and freshness for a coin, if it was read. */
64
+ export function findRecordedPrice(cassette, coinId) {
65
+ const path = canonicalRequest("GET", `${REPLAY_BASE_URL}/api/agent/market/${encodeURIComponent(coinId)}`).path;
66
+ const hit = cassette.responses.find((r) => r.path === path && r.ok);
67
+ if (!hit)
68
+ return undefined;
69
+ const m = asObj(hit.data);
70
+ const price = asNum(asObj(m.price).usd);
71
+ return {
72
+ priceUsd: price !== undefined && price > 0 ? price : undefined,
73
+ freshness: freshnessOf(asObj(m.observation))?.status ?? "fresh",
74
+ };
75
+ }
76
+ function ineligible(venue, subject, reason, freshness = "fresh") {
77
+ return {
78
+ response: {
79
+ eligible: false,
80
+ blockReasons: [reason],
81
+ observation: { freshness: { status: freshness } },
82
+ synthetic: true,
83
+ },
84
+ evidence: { venue, subject, eligible: false, basis: reason },
85
+ };
86
+ }
87
+ function synthesizePm(cassette, body) {
88
+ const source = (asStr(body.source) ?? "").toLowerCase();
89
+ const slug = (asStr(body.slug) ?? "").toLowerCase();
90
+ const outcome = asStr(body.outcomeExternalMarketId) ?? "";
91
+ const subject = `${source}/${slug}/${outcome}`;
92
+ const found = findRecordedPmOutcome(cassette, source, slug, outcome);
93
+ if (!found || found.probability === undefined)
94
+ return ineligible("pm", subject, "entry_price_unavailable");
95
+ if (!found.eligible)
96
+ return ineligible("pm", subject, "recorded_outcome_ineligible");
97
+ // NOTE: the server also applies minEntryProbabilityPct inside the quote
98
+ // (blockReasons entry_below_floor). The synthesized quote does not, so the
99
+ // runner's own validator applies the same floor and reports
100
+ // pm_entry_below_floor where production reports quote_ineligible. Both
101
+ // reject the same action.
102
+ const quote = synthesizePmQuote(found.probability, asNum(body.stakeMusd) ?? 0);
103
+ if (!quote)
104
+ return ineligible("pm", subject, "synthetic_unfillable", found.freshness);
105
+ const freshness = { status: found.freshness };
106
+ return {
107
+ response: {
108
+ eligible: true,
109
+ blockReasons: [],
110
+ fillBasis: "outcome_probability",
111
+ side: "yes",
112
+ entryProbability: quote.entryProbability,
113
+ stakeMusd: quote.stakeMusd,
114
+ sharesEstimate: quote.sharesEstimate,
115
+ maxPayout: quote.sharesEstimate,
116
+ minStake: 10,
117
+ freshness,
118
+ observation: { freshness },
119
+ openBlocked: false,
120
+ openBlockReasons: [],
121
+ synthetic: true,
122
+ },
123
+ evidence: {
124
+ venue: "pm",
125
+ subject,
126
+ eligible: true,
127
+ basis: "recorded_discover_probability",
128
+ },
129
+ };
130
+ }
131
+ function synthesizeCoin(venue, cassette, body) {
132
+ const coinId = asStr(body.coinId) ?? "";
133
+ const found = findRecordedPrice(cassette, coinId);
134
+ if (!found || found.priceUsd === undefined)
135
+ return ineligible(venue, coinId, "entry_price_unavailable");
136
+ const freshness = { status: found.freshness };
137
+ const evidence = {
138
+ venue,
139
+ subject: coinId,
140
+ eligible: true,
141
+ basis: "recorded_market_price",
142
+ };
143
+ const price = found.priceUsd;
144
+ if (venue === "spot") {
145
+ const quantity = asNum(body.quantity);
146
+ if (quantity === undefined || quantity <= 0)
147
+ return ineligible(venue, coinId, "invalid_size", found.freshness);
148
+ const cost = price * quantity;
149
+ return {
150
+ response: {
151
+ eligible: true,
152
+ blockReasons: [],
153
+ executionPrice: price,
154
+ estimatedCostMusd: cost,
155
+ estimatedFeeMusd: (cost * SPOT_SYNTHETIC_FEE_BPS) / 10_000,
156
+ observation: { freshness },
157
+ synthetic: true,
158
+ },
159
+ evidence,
160
+ };
161
+ }
162
+ const margin = asNum(body.marginMusd);
163
+ const leverage = asNum(body.leverage);
164
+ if (margin === undefined ||
165
+ leverage === undefined ||
166
+ margin <= 0 ||
167
+ leverage <= 0)
168
+ return ineligible(venue, coinId, "invalid_size", found.freshness);
169
+ const entryFee = (margin * leverage * FUTURES_SYNTHETIC_FEE_BPS) / 10_000;
170
+ return {
171
+ response: {
172
+ eligible: true,
173
+ blockReasons: [],
174
+ entryPrice: price,
175
+ executionModel: {
176
+ version: "bench_synthetic_v1",
177
+ feeBps: FUTURES_SYNTHETIC_FEE_BPS,
178
+ estimatedEntryFeeMusd: entryFee,
179
+ },
180
+ cashRequiredMusd: margin + entryFee,
181
+ observation: { freshness },
182
+ synthetic: true,
183
+ },
184
+ evidence,
185
+ };
186
+ }
187
+ function bodyOf(init) {
188
+ const body = init?.body;
189
+ if (typeof body !== "string")
190
+ return {};
191
+ try {
192
+ return asObj(JSON.parse(body));
193
+ }
194
+ catch {
195
+ return {};
196
+ }
197
+ }
198
+ function replayResponse(hit) {
199
+ const text = typeof hit.data === "string" ? hit.data : JSON.stringify(hit.data);
200
+ return new Response(NULL_BODY_STATUS.has(hit.status) ? null : text, {
201
+ status: hit.status,
202
+ headers: { "content-type": "application/json" },
203
+ });
204
+ }
205
+ /** A production CoinRithmClient that answers from one cassette. */
206
+ export class ReplayClient extends CoinRithmClient {
207
+ stats;
208
+ constructor(cassette) {
209
+ const stats = {
210
+ served: 0,
211
+ missing: [],
212
+ synthesizedQuotes: [],
213
+ refusedWrites: [],
214
+ };
215
+ const index = new Map(cassette.responses.map((r) => [r.key, r]));
216
+ const replay = async (input, init) => {
217
+ const req = canonicalRequest(init?.method ?? "GET", urlOf(input));
218
+ if (req.method === "GET") {
219
+ const hit = index.get(req.key);
220
+ if (!hit) {
221
+ stats.missing.push(req.key);
222
+ return jsonResponse(NOT_RECORDED_STATUS, {
223
+ error: "not_recorded",
224
+ key: req.key,
225
+ });
226
+ }
227
+ // Reproduce a recorded transport failure as one (the client maps it
228
+ // to status 0 network_error, exactly as it did while recording).
229
+ if (hit.transportError)
230
+ throw new TypeError("recorded transport error");
231
+ stats.served += 1;
232
+ return replayResponse(hit);
233
+ }
234
+ const venue = req.path === "/api/agent/pm/quote"
235
+ ? "pm"
236
+ : req.path === "/api/agent/futures/quote"
237
+ ? "futures"
238
+ : req.path === "/api/agent/spot/quote"
239
+ ? "spot"
240
+ : undefined;
241
+ if (!venue) {
242
+ stats.refusedWrites.push(req.key);
243
+ return writeRefusedResponse();
244
+ }
245
+ const synthesized = venue === "pm"
246
+ ? synthesizePm(cassette, bodyOf(init))
247
+ : synthesizeCoin(venue, cassette, bodyOf(init));
248
+ stats.synthesizedQuotes.push(synthesized.evidence);
249
+ return jsonResponse(200, synthesized.response);
250
+ };
251
+ replayNonce += 1;
252
+ super({
253
+ apiKey: `bench-replay-${replayNonce}`,
254
+ baseUrl: REPLAY_BASE_URL,
255
+ fetchFn: replay,
256
+ // A recorded 429 is replayed once, never retried with real waits.
257
+ maxRetries: 0,
258
+ sleepFn: async () => { },
259
+ });
260
+ this.stats = stats;
261
+ }
262
+ }
@@ -0,0 +1,115 @@
1
+ import { ActionScore } from "./labels.js";
2
+ import type { DecideRouteAttempt } from "../providers.js";
3
+ export declare const REPORT_SCHEMA = "coinrithm.bench.report.v1";
4
+ export declare const TUNE_FRACTION = 0.7;
5
+ export declare const NULL_CALIBRATION_RESAMPLINGS = 200;
6
+ export declare const NULL_CALIBRATION_BOOTSTRAP = 500;
7
+ export declare const NULL_CALIBRATION_TARGET = 0.05;
8
+ export interface ActionRow {
9
+ /** Identity of the move without its size (see actionKey). */
10
+ key: string;
11
+ type: string;
12
+ accepted: boolean;
13
+ code?: string;
14
+ /** Present when the action's quote was synthesized by the replay client. */
15
+ quote?: "synthesized";
16
+ forecastProbability?: number;
17
+ entryProbability?: number;
18
+ sizeMusd?: number;
19
+ score?: ActionScore;
20
+ }
21
+ export interface CycleRow {
22
+ cassetteId: string;
23
+ asOf: string;
24
+ variant: string;
25
+ repeat: number;
26
+ decision: "act" | "skip";
27
+ decisionType: string;
28
+ modelFailed: boolean;
29
+ llmCallMade: boolean;
30
+ /** Safe router provenance, at most two attempts. Omitted on legacy rows or
31
+ * providers without route metadata; no inference from its absence. Error
32
+ * text, latency, prompts and model output are deliberately not copied. */
33
+ routeAttempts?: Array<Pick<DecideRouteAttempt, "provider" | "model" | "outcome" | "failureClass" | "status" | "responseSource" | "actionsStringDiagnostic" | "admissionReasons">>;
34
+ /** A nonstandard provider exceeded the shipped two-entry route contract. */
35
+ routeAttemptsTruncated?: true;
36
+ /**
37
+ * Gate trigger codes for the cycle. Tells a real setup (PRICE_BREAKOUT,
38
+ * MOMENTUM_TREND, ...) apart from the periodic PM wake (PM_PERIODIC). Each
39
+ * cycle starts from a fresh state, so a cassette with PM markets and no
40
+ * setup or open position CAN wake periodically.
41
+ */
42
+ triggerCodes?: string[];
43
+ skipReason?: string;
44
+ runtimeError?: string;
45
+ actions: ActionRow[];
46
+ missingInputs: string[];
47
+ synthesizedQuotes: number;
48
+ refusedWrites: string[];
49
+ }
50
+ export interface VariantInfo {
51
+ name: string;
52
+ kind: "variant" | "baseline";
53
+ definitionHash?: string;
54
+ }
55
+ export interface CiResult {
56
+ n: number;
57
+ meanA: number | null;
58
+ meanB: number | null;
59
+ meanDiff: number | null;
60
+ ci95: [number, number] | null;
61
+ excludesZero: boolean;
62
+ }
63
+ /** Deterministic 32-bit PRNG (mulberry32). Same seed, same sequence. */
64
+ export declare function mulberry32(seed: number): () => number;
65
+ /**
66
+ * Percentile bootstrap CI of the mean of `diffs`. Deterministic for a given
67
+ * seed. n = 0 gives null; n = 1 gives a degenerate [d, d] interval.
68
+ */
69
+ export declare function bootstrapMeanCi(diffs: number[], seed: number, resamples: number): [number, number] | null;
70
+ export interface NullCalibration {
71
+ resamplings: number;
72
+ /** Cassettes with at least two repeat values for this metric. */
73
+ usableCassettes: number;
74
+ falsePositives: number;
75
+ /** Share of seeded A/A resamplings whose 95% CI excluded 0. */
76
+ falsePositiveRate: number | null;
77
+ }
78
+ /**
79
+ * Calibrated-null check of the paired bootstrap test. `repeatValues` holds,
80
+ * per cassette, one metric value per repeat of the SAME variant. Each of the
81
+ * `resamplings` draws two different repeats per cassette (seeded), treats them
82
+ * as A and B, and runs the same 95% CI test the comparisons use. Under this
83
+ * construction there is no true difference, so the share of draws whose CI
84
+ * excludes 0 estimates the test's false-positive rate. A draw where every
85
+ * difference is exactly 0 has CI [0, 0] and counts as a non-rejection.
86
+ */
87
+ export declare function calibrateNull(repeatValues: number[][], seed: number, resamplings?: number, bootstrapResamples?: number): NullCalibration;
88
+ /** Jaccard similarity of two action-key sets; two empty sets count as 1. */
89
+ export declare function jaccard(a: string[], b: string[]): number;
90
+ /** Round every finite number in a JSON-like value (deterministic output). */
91
+ export declare function roundDeep(value: unknown, digits?: number): unknown;
92
+ export interface BuildReportInput {
93
+ cassettes: Array<{
94
+ id: string;
95
+ asOf: string;
96
+ }>;
97
+ rows: CycleRow[];
98
+ variants: VariantInfo[];
99
+ repeats: number;
100
+ seed: number;
101
+ bootstrapResamples: number;
102
+ fidelity: Record<string, unknown>;
103
+ assumptions: Record<string, unknown>;
104
+ }
105
+ export interface BenchReport {
106
+ schema: typeof REPORT_SCHEMA;
107
+ contentHash: string;
108
+ [key: string]: unknown;
109
+ }
110
+ /** Chronological order: asOf (parsed when possible), then id. */
111
+ export declare function chronological<T extends {
112
+ id: string;
113
+ asOf: string;
114
+ }>(cassettes: T[]): T[];
115
+ export declare function buildReport(input: BuildReportInput): BenchReport;