@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,435 @@
1
+ // The bench report: deterministic JSON over replayed cycles.
2
+ //
3
+ // Determinism rules: no wall-clock field, cassettes in chronological order,
4
+ // every map key sorted, every number rounded to 6 decimals, and the bootstrap
5
+ // driven by a SEEDED PRNG (never Math.random). The same corpus and the same
6
+ // decisions therefore produce the same JSON and the same contentHash (sha256
7
+ // of the canonical JSON, the scorecard.ts approach). With a real model the
8
+ // decisions themselves vary run to run; that is what --repeats measures.
9
+ //
10
+ // Statistics, stated plainly:
11
+ // - Paired comparison: for every cassette both variants saw, the per-cassette
12
+ // difference of a metric (each side averaged over its repeats). The report
13
+ // gives the mean difference and a percentile bootstrap 95% CI over
14
+ // cassettes. A CI that excludes 0 is evidence of a difference on THIS
15
+ // corpus, not a guarantee; few cassettes give wide intervals.
16
+ // - Chronological split: cassettes sorted by asOf; the first 70% are "tune",
17
+ // the last 30% "holdout". Choose between variants on tune, report holdout.
18
+ // - Failures are results: model failures, runtime errors and validator
19
+ // rejections are counted per variant and never dropped from a denominator.
20
+ // - Calibrated null: a single A/A comparison can exclude 0 by chance (about
21
+ // 1 time in 20 at 95%). So the report does not demand that every A/A CI
22
+ // covers 0. Instead, per variant with 2+ repeats, it builds many seeded
23
+ // A/A comparisons from the variant's OWN repeats (two different repeats
24
+ // per cassette) and reports how often the 95% CI test fires. That
25
+ // false-positive rate should sit near 5%; far above means the CI is too
26
+ // narrow for this corpus and its differences must not be trusted.
27
+ import { canonicalJson, sha256Hex } from "./cassette.js";
28
+ export const REPORT_SCHEMA = "coinrithm.bench.report.v1";
29
+ export const TUNE_FRACTION = 0.7;
30
+ export const NULL_CALIBRATION_RESAMPLINGS = 200;
31
+ export const NULL_CALIBRATION_BOOTSTRAP = 500;
32
+ export const NULL_CALIBRATION_TARGET = 0.05;
33
+ /** Deterministic 32-bit PRNG (mulberry32). Same seed, same sequence. */
34
+ export function mulberry32(seed) {
35
+ let a = seed >>> 0;
36
+ return () => {
37
+ a = (a + 0x6d2b79f5) >>> 0;
38
+ let t = a;
39
+ t = Math.imul(t ^ (t >>> 15), t | 1);
40
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
41
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
42
+ };
43
+ }
44
+ function fnv1a32(s) {
45
+ let h = 0x811c9dc5;
46
+ for (let i = 0; i < s.length; i++) {
47
+ h ^= s.charCodeAt(i);
48
+ h = Math.imul(h, 0x01000193);
49
+ }
50
+ return h >>> 0;
51
+ }
52
+ const mean = (xs) => xs.length === 0 ? null : xs.reduce((s, x) => s + x, 0) / xs.length;
53
+ /**
54
+ * Percentile bootstrap CI of the mean of `diffs`. Deterministic for a given
55
+ * seed. n = 0 gives null; n = 1 gives a degenerate [d, d] interval.
56
+ */
57
+ export function bootstrapMeanCi(diffs, seed, resamples) {
58
+ const n = diffs.length;
59
+ if (n === 0)
60
+ return null;
61
+ const rand = mulberry32(seed);
62
+ const means = [];
63
+ for (let b = 0; b < resamples; b++) {
64
+ let s = 0;
65
+ for (let i = 0; i < n; i++)
66
+ s += diffs[Math.floor(rand() * n)];
67
+ means.push(s / n);
68
+ }
69
+ means.sort((x, y) => x - y);
70
+ const at = (q) => means[Math.min(means.length - 1, Math.max(0, Math.floor(q * means.length)))];
71
+ return [at(0.025), at(0.975)];
72
+ }
73
+ /**
74
+ * Calibrated-null check of the paired bootstrap test. `repeatValues` holds,
75
+ * per cassette, one metric value per repeat of the SAME variant. Each of the
76
+ * `resamplings` draws two different repeats per cassette (seeded), treats them
77
+ * as A and B, and runs the same 95% CI test the comparisons use. Under this
78
+ * construction there is no true difference, so the share of draws whose CI
79
+ * excludes 0 estimates the test's false-positive rate. A draw where every
80
+ * difference is exactly 0 has CI [0, 0] and counts as a non-rejection.
81
+ */
82
+ export function calibrateNull(repeatValues, seed, resamplings = NULL_CALIBRATION_RESAMPLINGS, bootstrapResamples = NULL_CALIBRATION_BOOTSTRAP) {
83
+ const usable = repeatValues.filter((v) => v.length >= 2);
84
+ if (usable.length < 2)
85
+ return {
86
+ resamplings: 0,
87
+ usableCassettes: usable.length,
88
+ falsePositives: 0,
89
+ falsePositiveRate: null,
90
+ };
91
+ const rand = mulberry32(seed);
92
+ let falsePositives = 0;
93
+ for (let s = 0; s < resamplings; s++) {
94
+ const diffs = usable.map((v) => {
95
+ const i = Math.floor(rand() * v.length);
96
+ let j = Math.floor(rand() * (v.length - 1));
97
+ if (j >= i)
98
+ j += 1;
99
+ return v[i] - v[j];
100
+ });
101
+ const innerSeed = Math.floor(rand() * 4294967296) >>> 0;
102
+ if (diffs.every((d) => d === 0))
103
+ continue;
104
+ const ci = bootstrapMeanCi(diffs, innerSeed, bootstrapResamples);
105
+ if (ci && (ci[0] > 0 || ci[1] < 0))
106
+ falsePositives += 1;
107
+ }
108
+ return {
109
+ resamplings,
110
+ usableCassettes: usable.length,
111
+ falsePositives,
112
+ falsePositiveRate: falsePositives / resamplings,
113
+ };
114
+ }
115
+ /** Jaccard similarity of two action-key sets; two empty sets count as 1. */
116
+ export function jaccard(a, b) {
117
+ const sa = new Set(a);
118
+ const sb = new Set(b);
119
+ if (sa.size === 0 && sb.size === 0)
120
+ return 1;
121
+ let inter = 0;
122
+ for (const k of sa)
123
+ if (sb.has(k))
124
+ inter += 1;
125
+ return inter / (sa.size + sb.size - inter);
126
+ }
127
+ /** Round every finite number in a JSON-like value (deterministic output). */
128
+ export function roundDeep(value, digits = 6) {
129
+ if (typeof value === "number") {
130
+ if (!Number.isFinite(value))
131
+ return null;
132
+ const f = 10 ** digits;
133
+ const r = Math.round(value * f) / f;
134
+ return Object.is(r, -0) ? 0 : r;
135
+ }
136
+ if (Array.isArray(value))
137
+ return value.map((v) => roundDeep(v, digits));
138
+ if (value && typeof value === "object") {
139
+ const out = {};
140
+ for (const [k, v] of Object.entries(value)) {
141
+ if (v !== undefined)
142
+ out[k] = roundDeep(v, digits);
143
+ }
144
+ return out;
145
+ }
146
+ return value;
147
+ }
148
+ // ── per-cassette summaries ──────────────────────────────────────────────────
149
+ const PAIRED_METRICS = [
150
+ "acceptedActions",
151
+ "rejectedActions",
152
+ "actRate",
153
+ "modelFailureRate",
154
+ "labelledPnlMusd",
155
+ "pmBrier",
156
+ "pmReturnOnStake",
157
+ "futuresReturnOnMargin",
158
+ ];
159
+ function acceptedKeys(row) {
160
+ return [
161
+ ...new Set(row.actions.filter((a) => a.accepted).map((a) => a.key)),
162
+ ].sort();
163
+ }
164
+ function rowValues(row) {
165
+ const accepted = row.actions.filter((a) => a.accepted);
166
+ const labelled = accepted
167
+ .map((a) => a.score)
168
+ .filter((s) => s?.status === "labelled");
169
+ const pm = labelled.filter((s) => s.venue === "pm");
170
+ const futures = labelled.filter((s) => s.venue === "futures");
171
+ const briers = pm
172
+ .map((s) => s.brier)
173
+ .filter((b) => b !== undefined);
174
+ const out = {
175
+ acceptedActions: accepted.length,
176
+ rejectedActions: row.actions.length - accepted.length,
177
+ actRate: row.decisionType === "act" ? 1 : 0,
178
+ modelFailureRate: row.modelFailed || row.runtimeError ? 1 : 0,
179
+ };
180
+ // A genuine no-action cycle earns zero. Missing outcomes/inputs are unknown,
181
+ // not cash: zero-filling them biases comparisons toward unlabelled trades.
182
+ if (!row.runtimeError &&
183
+ row.missingInputs.length === 0 &&
184
+ accepted.every((a) => a.score?.status === "labelled"))
185
+ out.labelledPnlMusd = labelled.reduce((s, x) => s + x.pnlMusd, 0);
186
+ const pmBrier = mean(briers);
187
+ if (pmBrier !== null)
188
+ out.pmBrier = pmBrier;
189
+ const pmRet = mean(pm.map((s) => s.returnOnStake));
190
+ if (pmRet !== null)
191
+ out.pmReturnOnStake = pmRet;
192
+ const futRet = mean(futures.map((s) => s.returnOnMargin));
193
+ if (futRet !== null)
194
+ out.futuresReturnOnMargin = futRet;
195
+ return out;
196
+ }
197
+ function summarize(rows) {
198
+ const perRepeat = rows.map(rowValues);
199
+ const values = {};
200
+ for (const m of PAIRED_METRICS) {
201
+ // Compare equal repeat sets, never just whichever repeats got labels.
202
+ if (m === "labelledPnlMusd" && perRepeat.some((r) => r[m] === undefined))
203
+ continue;
204
+ const v = mean(perRepeat.map((r) => r[m]).filter((x) => x !== undefined));
205
+ if (v !== null)
206
+ values[m] = v;
207
+ }
208
+ return { values, perRepeat, actionSets: rows.map(acceptedKeys) };
209
+ }
210
+ // ── variant aggregates ──────────────────────────────────────────────────────
211
+ function variantMetrics(rows, summaries) {
212
+ const decisionMix = {
213
+ act: 0,
214
+ skip: 0,
215
+ gate_skip: 0,
216
+ model_error: 0,
217
+ runtime_error: 0,
218
+ };
219
+ // Trigger code occurrences across rows (a row can carry several codes), not
220
+ // model calls: llmCallMade/decisionMix count those.
221
+ const triggerMix = {};
222
+ const rejectCodes = {};
223
+ let proposed = 0;
224
+ let accepted = 0;
225
+ const missingKeys = new Set();
226
+ const scores = [];
227
+ for (const row of rows) {
228
+ decisionMix[row.decisionType] = (decisionMix[row.decisionType] ?? 0) + 1;
229
+ for (const code of row.triggerCodes ?? [])
230
+ triggerMix[code] = (triggerMix[code] ?? 0) + 1;
231
+ for (const a of row.actions) {
232
+ proposed += 1;
233
+ if (a.accepted) {
234
+ accepted += 1;
235
+ if (a.score)
236
+ scores.push(a.score);
237
+ }
238
+ else {
239
+ const code = a.code ?? "unknown";
240
+ rejectCodes[code] = (rejectCodes[code] ?? 0) + 1;
241
+ }
242
+ }
243
+ for (const k of row.missingInputs)
244
+ missingKeys.add(k);
245
+ }
246
+ const pm = scores.filter((s) => s.status === "labelled" && s.venue === "pm");
247
+ const futures = scores.filter((s) => s.status === "labelled" && s.venue === "futures");
248
+ const consistent = summaries.filter((s) => s.actionSets.every((set) => set.join("|") === s.actionSets[0].join("|"))).length;
249
+ return {
250
+ cassettes: summaries.length,
251
+ cycles: rows.length,
252
+ modelFailures: rows.filter((r) => r.modelFailed).length,
253
+ runtimeErrors: rows.filter((r) => r.runtimeError).length,
254
+ decisionMix,
255
+ triggerMix,
256
+ actions: {
257
+ proposed,
258
+ accepted,
259
+ rejected: proposed - accepted,
260
+ rejectCodes,
261
+ },
262
+ repeatConsistency: summaries.length === 0 ? null : consistent / summaries.length,
263
+ synthesizedQuotes: rows.reduce((s, r) => s + r.synthesizedQuotes, 0),
264
+ cyclesWithMissingInputs: rows.filter((r) => r.missingInputs.length > 0)
265
+ .length,
266
+ missingInputKeys: [...missingKeys].sort(),
267
+ refusedWrites: rows.reduce((s, r) => s + r.refusedWrites.length, 0),
268
+ pnlComparableCycles: rows.filter((r) => rowValues(r).labelledPnlMusd !== undefined).length,
269
+ pnlExcludedCycles: rows.filter((r) => rowValues(r).labelledPnlMusd === undefined).length,
270
+ labelled: {
271
+ pmOpens: pm.length,
272
+ pmBrierMean: mean(pm.map((s) => s.brier).filter((b) => b !== undefined)),
273
+ pmMarketBrierMean: mean(pm
274
+ .map((s) => s.marketBrier)
275
+ .filter((b) => b !== undefined)),
276
+ pmReturnOnStakeMean: mean(pm.map((s) => s.returnOnStake)),
277
+ futuresOpens: futures.length,
278
+ futuresReturnOnMarginMean: mean(futures.map((s) => s.returnOnMargin)),
279
+ futuresSameBarStopFirst: futures.filter((s) => s.sameBarStopAndTarget)
280
+ .length,
281
+ futuresFundingIncluded: futures.filter((s) => s.fundingIncluded).length,
282
+ unlabelledOpens: scores.filter((s) => s.status === "unlabelled").length,
283
+ unlabelledGapOpens: scores.filter((s) => s.status === "unlabelled_gap")
284
+ .length,
285
+ labelledPnlMusd: [...pm, ...futures].reduce((s, x) => s + x.pnlMusd, 0),
286
+ },
287
+ };
288
+ }
289
+ /** Chronological order: asOf (parsed when possible), then id. */
290
+ export function chronological(cassettes) {
291
+ const t = (c) => {
292
+ const ms = Date.parse(c.asOf);
293
+ return Number.isFinite(ms) ? ms : Number.POSITIVE_INFINITY;
294
+ };
295
+ return [...cassettes].sort((a, b) => t(a) - t(b) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
296
+ }
297
+ export function buildReport(input) {
298
+ const ordered = chronological(input.cassettes);
299
+ const ids = ordered.map((c) => c.id);
300
+ const tuneCount = Math.floor(ids.length * TUNE_FRACTION);
301
+ const splits = {
302
+ all: ids,
303
+ tune: ids.slice(0, tuneCount),
304
+ holdout: ids.slice(tuneCount),
305
+ };
306
+ const byKey = new Map();
307
+ for (const row of input.rows) {
308
+ const k = `${row.variant}\u0000${row.cassetteId}`;
309
+ const list = byKey.get(k) ?? [];
310
+ list.push(row);
311
+ byKey.set(k, list);
312
+ }
313
+ const rowsOf = (variant, id) => (byKey.get(`${variant}\u0000${id}`) ?? []).sort((a, b) => a.repeat - b.repeat);
314
+ const summaryCache = new Map();
315
+ const summaryOf = (variant, id) => {
316
+ const k = `${variant}\u0000${id}`;
317
+ if (!summaryCache.has(k)) {
318
+ const rows = rowsOf(variant, id);
319
+ summaryCache.set(k, rows.length > 0 ? summarize(rows) : undefined);
320
+ }
321
+ return summaryCache.get(k);
322
+ };
323
+ const variants = {};
324
+ for (const v of input.variants) {
325
+ const bySplit = {};
326
+ for (const [split, splitIds] of Object.entries(splits)) {
327
+ const rows = splitIds.flatMap((id) => rowsOf(v.name, id));
328
+ const summaries = splitIds
329
+ .map((id) => summaryOf(v.name, id))
330
+ .filter((s) => s !== undefined);
331
+ bySplit[split] = variantMetrics(rows, summaries);
332
+ }
333
+ const allSummaries = ids
334
+ .map((id) => summaryOf(v.name, id))
335
+ .filter((s) => s !== undefined);
336
+ const nullMetrics = {};
337
+ if (v.kind === "variant" && input.repeats >= 2) {
338
+ for (const m of PAIRED_METRICS) {
339
+ nullMetrics[m] = calibrateNull(allSummaries.map((s) => s.perRepeat
340
+ .map((r) => r[m])
341
+ .filter((x) => x !== undefined)), (input.seed ^ fnv1a32(`null|${v.name}|${m}`)) >>> 0);
342
+ }
343
+ }
344
+ variants[v.name] = {
345
+ kind: v.kind,
346
+ ...(v.definitionHash ? { definitionHash: v.definitionHash } : {}),
347
+ ...bySplit,
348
+ nullCalibration: Object.keys(nullMetrics).length === 0
349
+ ? null
350
+ : {
351
+ method: "seeded A/A from this variant's own repeats (two different repeats per cassette), paired bootstrap 95% CI per resampling, all cassettes",
352
+ target: NULL_CALIBRATION_TARGET,
353
+ resamplings: NULL_CALIBRATION_RESAMPLINGS,
354
+ bootstrapResamples: NULL_CALIBRATION_BOOTSTRAP,
355
+ seed: input.seed,
356
+ metrics: nullMetrics,
357
+ },
358
+ };
359
+ }
360
+ // Every variant against every later variant and every baseline; baselines
361
+ // are not compared with each other.
362
+ const comparisons = [];
363
+ for (let i = 0; i < input.variants.length; i++) {
364
+ for (let j = i + 1; j < input.variants.length; j++) {
365
+ const a = input.variants[i];
366
+ const b = input.variants[j];
367
+ if (a.kind === "baseline" && b.kind === "baseline")
368
+ continue;
369
+ const bySplit = {};
370
+ for (const [split, splitIds] of Object.entries(splits)) {
371
+ const pairs = splitIds
372
+ .map((id) => [summaryOf(a.name, id), summaryOf(b.name, id)])
373
+ .filter((p) => p[0] !== undefined && p[1] !== undefined);
374
+ const overlaps = pairs.map(([sa, sb]) => {
375
+ const scores = [];
376
+ for (const x of sa.actionSets)
377
+ for (const y of sb.actionSets)
378
+ scores.push(jaccard(x, y));
379
+ return mean(scores) ?? 1;
380
+ });
381
+ const metrics = {};
382
+ for (const m of PAIRED_METRICS) {
383
+ const both = pairs.filter(([sa, sb]) => sa.values[m] !== undefined && sb.values[m] !== undefined);
384
+ const va = both.map(([sa]) => sa.values[m]);
385
+ const vb = both.map(([, sb]) => sb.values[m]);
386
+ const diffs = va.map((x, k) => x - vb[k]);
387
+ const ci = bootstrapMeanCi(diffs, (input.seed ^ fnv1a32(`${a.name}|${b.name}|${split}|${m}`)) >>> 0, input.bootstrapResamples);
388
+ metrics[m] = {
389
+ n: diffs.length,
390
+ meanA: mean(va),
391
+ meanB: mean(vb),
392
+ meanDiff: mean(diffs),
393
+ ci95: ci,
394
+ // One paired cassette is an anecdote, not evidence.
395
+ excludesZero: ci !== null && diffs.length >= 2 && (ci[0] > 0 || ci[1] < 0),
396
+ };
397
+ }
398
+ bySplit[split] = {
399
+ pairedCassettes: pairs.length,
400
+ actionOverlapJaccard: mean(overlaps),
401
+ metrics,
402
+ };
403
+ }
404
+ comparisons.push({ a: a.name, b: b.name, ...bySplit });
405
+ }
406
+ }
407
+ const cycles = [...input.rows].sort((x, y) => ids.indexOf(x.cassetteId) - ids.indexOf(y.cassetteId) ||
408
+ (x.variant < y.variant ? -1 : x.variant > y.variant ? 1 : 0) ||
409
+ x.repeat - y.repeat);
410
+ const body = roundDeep({
411
+ schema: REPORT_SCHEMA,
412
+ corpus: {
413
+ cassettes: ids.length,
414
+ ids,
415
+ split: {
416
+ tuneFraction: TUNE_FRACTION,
417
+ tune: splits.tune,
418
+ holdout: splits.holdout,
419
+ },
420
+ },
421
+ settings: {
422
+ repeats: input.repeats,
423
+ seed: input.seed,
424
+ bootstrapResamples: input.bootstrapResamples,
425
+ pairedMetrics: [...PAIRED_METRICS],
426
+ },
427
+ assumptions: input.assumptions,
428
+ fidelity: input.fidelity,
429
+ variants,
430
+ comparisons,
431
+ cycles,
432
+ });
433
+ const contentHash = `sha256:${sha256Hex(canonicalJson(body))}`;
434
+ return { ...body, schema: REPORT_SCHEMA, contentHash };
435
+ }
@@ -1,6 +1,15 @@
1
- import type { AgentSpec, CapitalBook, CapitalSizingAdjustment, Observation, ProposedAction, QuoteEvidence } from "./types.js";
1
+ import type { AgentSpec, CapitalBook, CapitalSizingPolicy, CapitalSizingAdjustment, Observation, ProposedAction, QuoteEvidence } from "./types.js";
2
2
  export declare const CAPITAL_VALUATION_BASIS = "wallet_assets_spot_marked_futures_pm_at_collateral";
3
3
  export declare const CAPITAL_FEE_BUFFER_BPS = 10;
4
+ /** The ticket and stop-risk limits for one action. Owner 2026-10-08: a
5
+ * risk-taker may size a futures entry it rates at or above
6
+ * highConviction.minConfidence far above its base ticket. Only futures_open
7
+ * qualifies; PM and spot sizing never read confidence. */
8
+ export declare function capitalLimitsFor(policy: CapitalSizingPolicy, action: ProposedAction): {
9
+ perTicketCapitalPct: number;
10
+ futuresRiskPct: number;
11
+ highConviction: boolean;
12
+ };
4
13
  /** Reconcile the bounded open-position reads against the wallet's frozen
5
14
  * buckets. A full page is not assumed complete: the collateral checksums must
6
15
  * agree. No held mark, unknown status, missing bucket, or mismatched book can
@@ -16,6 +16,28 @@ const FROZEN_RESIDUE_TOLERANCE = 1e-8;
16
16
  const centsDown = (n) => Math.floor(n * 100) / 100;
17
17
  const positive = (n) => asNum(n) !== undefined && n > 0;
18
18
  const nonnegative = (n) => asNum(n) !== undefined && n >= 0;
19
+ /** The ticket and stop-risk limits for one action. Owner 2026-10-08: a
20
+ * risk-taker may size a futures entry it rates at or above
21
+ * highConviction.minConfidence far above its base ticket. Only futures_open
22
+ * qualifies; PM and spot sizing never read confidence. */
23
+ export function capitalLimitsFor(policy, action) {
24
+ const hc = policy.highConviction;
25
+ if (hc &&
26
+ action.type === "futures_open" &&
27
+ typeof action.confidence === "number" &&
28
+ Number.isFinite(action.confidence) &&
29
+ action.confidence >= hc.minConfidence)
30
+ return {
31
+ perTicketCapitalPct: hc.perTicketCapitalPct,
32
+ futuresRiskPct: hc.futuresRiskPct,
33
+ highConviction: true,
34
+ };
35
+ return {
36
+ perTicketCapitalPct: policy.perTicketCapitalPct,
37
+ futuresRiskPct: policy.futuresRiskPct,
38
+ highConviction: false,
39
+ };
40
+ }
19
41
  /** Reconcile the bounded open-position reads against the wallet's frozen
20
42
  * buckets. A full page is not assumed complete: the collateral checksums must
21
43
  * agree. No held mark, unknown status, missing bucket, or mismatched book can
@@ -119,6 +141,7 @@ export function prepareCapitalAction(action, spec, observation, budget) {
119
141
  if (validateCapitalSizingPolicy(spec.capitalSizing).length > 0)
120
142
  return { action, rejection: "capital_policy_invalid" };
121
143
  const policy = spec.capitalSizing;
144
+ const limits = capitalLimitsFor(policy, action);
122
145
  const adjustment = {
123
146
  version: policy.version,
124
147
  basis: "owned_collateral_spot_marked_negative_position_marks_only",
@@ -134,7 +157,7 @@ export function prepareCapitalAction(action, spec, observation, budget) {
134
157
  !nonnegative(budget.openMarginMusd))
135
158
  return reject("capital_budget_unavailable");
136
159
  adjustment.conservativeEquityMusd = equity;
137
- const ticket = Math.min(spec.risk.perTradeMarginMusd, (equity * policy.perTicketCapitalPct) / 100);
160
+ const ticket = Math.min(spec.risk.perTradeMarginMusd, (equity * limits.perTicketCapitalPct) / 100);
138
161
  const room = Math.min(ticket, (equity * policy.totalCapitalPct) / 100 - budget.committedCapitalMusd, budget.cashAvailableMusd - (equity * policy.cashReservePct) / 100);
139
162
  if (!positive(room))
140
163
  return reject("capital_allocation_or_reserve_exhausted");
@@ -161,7 +184,7 @@ export function prepareCapitalAction(action, spec, observation, budget) {
161
184
  const distance = Math.abs(mark - stop) / mark;
162
185
  const fee = CAPITAL_FEE_BUFFER_BPS / 10_000;
163
186
  const riskPerMargin = action.leverage * (distance + fee * (1 + stop / mark));
164
- const riskBudget = (equity * policy.futuresRiskPct) / 100;
187
+ const riskBudget = (equity * limits.futuresRiskPct) / 100;
165
188
  const margin = centsDown(Math.min(ticket, riskBudget / riskPerMargin, spec.limits.maxOpenMarginMusd - budget.openMarginMusd, room / (1 + action.leverage * fee)));
166
189
  Object.assign(adjustment, {
167
190
  proposedAmountMusd: action.marginMusd,
@@ -188,6 +211,7 @@ export function validateCapitalAction(action, spec, observation, budget, quote)
188
211
  !nonnegative(budget.committedCapitalMusd) ||
189
212
  !nonnegative(budget.openMarginMusd))
190
213
  return "capital_budget_unavailable";
214
+ const limits = capitalLimitsFor(p, action);
191
215
  let cost, allocated;
192
216
  if (action.type === "pm_open") {
193
217
  cost = allocated = action.stakeMusd;
@@ -227,7 +251,7 @@ export function validateCapitalAction(action, spec, observation, budget, quote)
227
251
  const reward = (notional * favorable) / entry -
228
252
  entryFee -
229
253
  ((notional * target) / entry) * feeRate;
230
- if (risk > (equity * p.futuresRiskPct) / 100 + 1e-8)
254
+ if (risk > (equity * limits.futuresRiskPct) / 100 + 1e-8)
231
255
  return "capital_quote_stop_risk_exceeded";
232
256
  if (reward / risk + 1e-8 < p.minRewardRisk)
233
257
  return "capital_quote_reward_risk_too_low";
@@ -235,7 +259,7 @@ export function validateCapitalAction(action, spec, observation, budget, quote)
235
259
  if (!positive(cost) || !positive(allocated))
236
260
  return "capital_quote_cost_evidence_missing";
237
261
  if (cost >
238
- Math.min(spec.risk.perTradeMarginMusd, (equity * p.perTicketCapitalPct) / 100) +
262
+ Math.min(spec.risk.perTradeMarginMusd, (equity * limits.perTicketCapitalPct) / 100) +
239
263
  1e-8)
240
264
  return "capital_ticket_cap_exceeded";
241
265
  if (budget.committedCapitalMusd + cost >
@@ -1,4 +1,6 @@
1
1
  import { SkillValidationMode } from "./skillValidator.js";
2
+ import { AgentSpec } from "./types.js";
3
+ import { Provider } from "./providers.js";
2
4
  export interface CmdResult {
3
5
  ok: boolean;
4
6
  code: number;
@@ -20,4 +22,27 @@ export declare function cmdRun(path: string, opts?: {
20
22
  stateFile?: string;
21
23
  expectDefinition?: string;
22
24
  }): Promise<CmdResult>;
25
+ export declare function cmdRecord(path: string, opts?: {
26
+ out?: string;
27
+ cycles?: number;
28
+ every?: string;
29
+ fetchFn?: typeof fetch;
30
+ sleepFn?: (ms: number) => Promise<void>;
31
+ }): Promise<CmdResult>;
32
+ export declare function cmdLabel(opts?: {
33
+ corpus?: string;
34
+ horizonHours?: number;
35
+ overwrite?: boolean;
36
+ fetchFn?: typeof fetch;
37
+ nowMs?: number;
38
+ }): Promise<CmdResult>;
39
+ export declare function cmdBench(opts?: {
40
+ corpus?: string;
41
+ variants?: string[];
42
+ repeats?: number;
43
+ seed?: number;
44
+ out?: string;
45
+ baselines?: boolean;
46
+ providerFor?: (spec: AgentSpec) => Provider;
47
+ }): Promise<CmdResult>;
23
48
  export declare function main(argv: string[]): Promise<number>;