outcometick 1.5.2 → 1.6.1

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.
@@ -8,16 +8,6 @@
8
8
  // Nothing in this module can see the strategy. It reads the trade and fill logs
9
9
  // the engine produced, so a report cannot be tuned by the thing it is judging.
10
10
 
11
- /** Delays the latency panel re-prices every fill at. */
12
- export const LATENCY_STEPS = Object.freeze([
13
- { label: 'as captured (0 ms)', ms: 0 },
14
- { label: '+100 ms', ms: 100 },
15
- { label: '+250 ms', ms: 250 },
16
- { label: '+500 ms', ms: 500 },
17
- { label: '+1 s', ms: 1000 },
18
- { label: '+2 s', ms: 2000 },
19
- ]);
20
-
21
11
  /** Entry-price buckets for the calibration panel. */
22
12
  export const CALIBRATION_BUCKETS = Object.freeze([
23
13
  [0.0, 0.1], [0.1, 0.2], [0.2, 0.3], [0.3, 0.4], [0.4, 0.5],
@@ -96,6 +86,11 @@ export function metrics(trades, { feesPaid = 0, days = 1 } = {}) {
96
86
  max_drawdown_abs: r2(dd.abs),
97
87
  sharpe: sharpe == null ? null : r2(sharpe),
98
88
  trades: closed.length,
89
+ // Distinct markets the strategy actually took a position in. The engine
90
+ // does not stop a strategy trading a market twice, so `trades / markets`
91
+ // is only an entry rate for strategies that enter once — this one is an
92
+ // entry rate for all of them, and equals `trades` in the common case.
93
+ markets_traded: new Set(closed.map((t) => t.market_id)).size,
99
94
  return_on_collateral: collateral > 0 ? r4(netPnl / collateral) : null,
100
95
  // Cents of edge per contract: what the outcome was worth minus what was
101
96
  // paid, averaged. This is the number that says whether there was an edge
@@ -202,7 +197,7 @@ export function calibration(trades) {
202
197
  const implied = mean(inBucket.map((t) => t.entry_px));
203
198
  const realized = mean(inBucket.map((t) => (t.outcome === t.side ? 1 : 0)));
204
199
  return {
205
- bucket: `${lo.toFixed(2)} – ${hi.toFixed(2)}`,
200
+ bucket: `${lo.toFixed(2)}-${hi.toFixed(2)}`,
206
201
  lo,
207
202
  hi,
208
203
  implied: r4(implied),
@@ -308,26 +303,6 @@ export function splitByMarket(trades, marketMeta = new Map()) {
308
303
  return rows;
309
304
  }
310
305
 
311
- /**
312
- * The latency panel: net PnL if every fill had landed later.
313
- *
314
- * The rows come from re-running the replay at each delay, which the caller
315
- * does — this only shapes the result. We keep event time, upstream server time
316
- * and our receive time separate on every row, which is what makes re-pricing at
317
- * an arbitrary delay meaningful rather than a guess.
318
- */
319
- export function latencyPanel(resultsByDelay) {
320
- const base = resultsByDelay.find((r) => r.delayMs === 0)?.netPnl ?? 0;
321
- return resultsByDelay.map((r) => ({
322
- label: LATENCY_STEPS.find((s) => s.ms === r.delayMs)?.label ?? `+${r.delayMs} ms`,
323
- delay_ms: r.delayMs,
324
- net_pnl: r2(r.netPnl),
325
- // Relative to the as-captured run, so the shape of the decay is readable
326
- // without dividing in your head.
327
- ratio: base === 0 ? null : r4(r.netPnl / base),
328
- unprofitable: r.netPnl < 0,
329
- }));
330
- }
331
306
 
332
307
  /**
333
308
  * The parameter sweep grid.
@@ -365,9 +340,9 @@ export function sweepPanel(cells, { xParam, yParam, metric = 'sharpe' }) {
365
340
  * makes the rest of it checkable, so it is never summarised away.
366
341
  */
367
342
  export function buildReport({
368
- runId, submittedAt, manifest, scope,
343
+ runId, submittedAt, manifest, scope, sourceSha256 = null,
369
344
  trades, fills, marketSummaries, marketMeta,
370
- feesPaid = 0, latency = [], sweep = null, coverage = null,
345
+ feesPaid = 0, fillDelayMs = 0, sweep = null, coverage = null,
371
346
  crosschecks = [], budget = null, seed = null, scanned = {},
372
347
  }) {
373
348
  const closed = trades.filter((t) => Number.isFinite(t.pnl));
@@ -376,6 +351,10 @@ export function buildReport({
376
351
 
377
352
  return {
378
353
  run_id: runId,
354
+ // WHICH CODE PRODUCED THIS. The source itself is no longer in the archive
355
+ // — a report is a thing you forward to someone and the strategy is not —
356
+ // so this is what answers "which version of my strategy was this?".
357
+ source_sha256: sourceSha256,
379
358
  generated_ms: submittedAt,
380
359
  sdk_schema: manifest?.schema ?? null,
381
360
  language: manifest?.language ?? null,
@@ -409,7 +388,17 @@ export function buildReport({
409
388
  }),
410
389
  split: splitByMarket(closed, marketMeta ?? new Map()),
411
390
  slippage: slippage(fills ?? []),
412
- latency: latencyPanel(latency),
391
+ // THE DELAY THIS RUN WAS PRICED AT, not a comparison table.
392
+ //
393
+ // There used to be five extra replays at 100ms..2s, then one, and the
394
+ // panel that compared them. It is gone: a run now replays ONCE, at
395
+ // whatever delay the submitter asked for, which is both the fastest answer
396
+ // and the only one that is a measurement rather than an extrapolation.
397
+ //
398
+ // It has to be IN the report, because it changes every number in it and
399
+ // nothing else in here would tell a reader whether they are looking at a
400
+ // zero-latency run or a 250ms one.
401
+ fill_delay_ms: fillDelayMs,
413
402
  sweep,
414
403
  coverage,
415
404
  budget,