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.
- package/README.md +8 -1
- package/api/lib/backtest-contract.mjs +265 -15
- package/api/lib/backtest-datasets.mjs +125 -1
- package/api/lib/backtest-manifest.mjs +52 -4
- package/api/lib/coverage-window.mjs +63 -1
- package/api/lib/data-taxonomy.mjs +14 -0
- package/cli/commands/run.mjs +196 -37
- package/cli/commands/submit.mjs +30 -2
- package/cli/local-data.mjs +81 -15
- package/cli/ot.mjs +22 -3
- package/index.d.ts +31 -4
- package/package.json +1 -1
- package/runner/archive.mjs +24 -13
- package/runner/engine/book.mjs +12 -1
- package/runner/engine/feed.mjs +116 -0
- package/runner/engine/portfolio.mjs +77 -5
- package/runner/engine/replay.mjs +136 -14
- package/runner/engine/report.mjs +23 -34
- package/runner/events.mjs +713 -55
- package/runner/harness/node/harness.mjs +138 -12
- package/runner/harness/node/sdk/index.d.ts +31 -4
- package/runner/harness/node/sdk/index.mjs +43 -2
- package/runner/harness/protocol.mjs +31 -3
- package/runner/harness/python/harness.py +123 -18
- package/runner/harness/python/otengine.py +116 -9
- package/runner/harness/python/otfeed.py +109 -0
- package/runner/harness/python/otreplay.py +90 -8
- package/runner/harness/python/outcometick.py +44 -4
- package/runner/series-data.mjs +220 -0
package/runner/engine/report.mjs
CHANGED
|
@@ -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)}
|
|
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,
|
|
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
|
-
|
|
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,
|