@tangle-network/agent-bench 0.3.7 → 0.3.8
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 +4 -0
- package/HARNESS.md +43 -0
- package/dist/adapters.js +23 -23
- package/dist/benchmarks/_harness.d.ts +1 -1
- package/dist/benchmarks/_harness.js +1 -1
- package/dist/benchmarks/aec-bench.js +2 -2
- package/dist/benchmarks/agentbench.js +2 -2
- package/dist/benchmarks/appworld.js +2 -2
- package/dist/benchmarks/bfcl.js +2 -2
- package/dist/benchmarks/commit0.js +2 -2
- package/dist/benchmarks/crag.js +2 -2
- package/dist/benchmarks/dabstep.js +2 -2
- package/dist/benchmarks/enterpriseops-gym.js +2 -2
- package/dist/benchmarks/finresearchbench.js +2 -2
- package/dist/benchmarks/nomiracl.js +2 -2
- package/dist/benchmarks/open-rag-bench.js +2 -2
- package/dist/benchmarks/programbench.js +2 -2
- package/dist/benchmarks/ragbench.js +2 -2
- package/dist/benchmarks/swe-bench.js +2 -2
- package/dist/benchmarks/t2-ragbench.js +2 -2
- package/dist/benchmarks/tau-bench-shared.js +2 -2
- package/dist/benchmarks/tau2-bench.js +3 -3
- package/dist/benchmarks/tau3-banking.js +3 -3
- package/dist/benchmarks/terminal-bench.js +2 -2
- package/dist/benchmarks/toollm.js +2 -2
- package/dist/benchmarks/webarena-verified.js +2 -2
- package/dist/{chunk-CKUVRZ2T.js → chunk-3U5TXJZS.js} +2 -2
- package/dist/{chunk-YCGY7UIZ.js → chunk-7GRVHU22.js} +2 -2
- package/dist/{chunk-Z7ML6L77.js → chunk-HWST3SED.js} +2 -2
- package/dist/{chunk-SYDW647C.js → chunk-IA2FBTWC.js} +2 -2
- package/dist/{chunk-R67DFVLO.js → chunk-IFVINJ4B.js} +2 -2
- package/dist/{chunk-R36V2VP7.js → chunk-IZ5M6OAC.js} +2 -2
- package/dist/{chunk-ODT47UAY.js → chunk-K3BQGZCT.js} +2 -2
- package/dist/{chunk-IFAV6KEM.js → chunk-KP5KD6EN.js} +2 -2
- package/dist/{chunk-ZEWMTR5M.js → chunk-MQMRLGOG.js} +2 -2
- package/dist/{chunk-TSWPNOYM.js → chunk-NQG5XDSB.js} +2 -2
- package/dist/{chunk-7WSD27QQ.js → chunk-PB64GYIG.js} +2 -2
- package/dist/{chunk-HBSWHQNJ.js → chunk-RCYQEFNX.js} +3 -3
- package/dist/{chunk-J3KDJNX2.js → chunk-RH5F53JT.js} +2 -2
- package/dist/{chunk-UAIOHCUK.js → chunk-SFLA7OH3.js} +3 -3
- package/dist/{chunk-Y6O2OCUO.js → chunk-SHM6MRRF.js} +2 -2
- package/dist/{chunk-KDIKRJGB.js → chunk-SHYIRB7I.js} +2 -2
- package/dist/{chunk-HHXFIHXC.js → chunk-SVR2LKYI.js} +2 -2
- package/dist/{chunk-PWQVGAJB.js → chunk-V7AEBY6U.js} +21 -21
- package/dist/{chunk-LRRD7NAG.js → chunk-WSKWVEQB.js} +18 -2
- package/dist/chunk-WSKWVEQB.js.map +1 -0
- package/dist/{chunk-2PVVP7GN.js → chunk-XKEFIFIC.js} +2 -2
- package/dist/{chunk-JRWWGMK7.js → chunk-XYA4XSNU.js} +2 -2
- package/dist/{chunk-X5YKXC6V.js → chunk-YSMEKBTD.js} +2 -2
- package/dist/{chunk-2XU6OGEN.js → chunk-Z4TZ76N7.js} +2 -2
- package/dist/index.js +23 -23
- package/package.json +4 -4
- package/scripts/verify-packed-consumer.mjs +1 -1
- package/src/benchmarks/_harness.ts +20 -2
- package/src/benchmarks/humaneval.test.mts +2 -2
- package/src/quant-arena/README.md +144 -0
- package/src/quant-arena/backtest.test.mts +135 -0
- package/src/quant-arena/backtest.ts +218 -0
- package/src/quant-arena/data.test.mts +44 -0
- package/src/quant-arena/data.ts +141 -0
- package/src/quant-arena/driver.test.mts +253 -0
- package/src/quant-arena/driver.ts +219 -0
- package/src/quant-arena/fixtures/data/PROVENANCE.md +26 -0
- package/src/quant-arena/fixtures/data/holdout/IDX.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S01.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S02.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S03.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S04.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S05.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S06.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S07.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S08.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S09.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S10.csv +523 -0
- package/src/quant-arena/fixtures/data/insample/IDX.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S01.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S02.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S03.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S04.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S05.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S06.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S07.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S08.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S09.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S10.csv +2087 -0
- package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +16 -0
- package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +5 -0
- package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +171 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +119 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +119 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +105 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +102 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +4 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +2 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +84 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +117 -0
- package/src/quant-arena/holdout-certify.mts +206 -0
- package/src/quant-arena/holdout-certify.test.mts +82 -0
- package/src/quant-arena/leak-audit.test.mts +79 -0
- package/src/quant-arena/leak-audit.ts +95 -0
- package/src/quant-arena/make-fixtures.mts +161 -0
- package/src/quant-arena/multiplicity.test.mts +68 -0
- package/src/quant-arena/multiplicity.ts +87 -0
- package/src/quant-arena/nautilus-certify.ts +31 -0
- package/src/quant-arena/oms.ts +90 -0
- package/src/quant-arena/profiles/quant-researcher.profile.json +7 -0
- package/src/quant-arena/python/pyproject.toml +8 -0
- package/src/quant-arena/python/uv.lock +1297 -0
- package/src/quant-arena/python/vbt-worker.py +192 -0
- package/src/quant-arena/quant-loop.mts +813 -0
- package/src/quant-arena/quant-loop.test.mts +75 -0
- package/src/quant-arena/strategies/buy-hold-index/strategy.ts +11 -0
- package/src/quant-arena/strategies/equal-weight/strategy.ts +20 -0
- package/src/quant-arena/strategies/sma-crossover/strategy.ts +42 -0
- package/src/quant-arena/types.ts +133 -0
- package/src/quant-arena/vbt-client.ts +321 -0
- package/src/quant-arena/vbt-parity.test.mts +183 -0
- package/src/quant-arena/windows.test.mts +45 -0
- package/src/quant-arena/windows.ts +54 -0
- package/src/rollout-ledger/backfill-swe-arena.mts +606 -0
- package/src/rollout-ledger/backfill-swe-arena.test.mts +338 -0
- package/src/rollout-ledger/settle-capture.mts +442 -0
- package/src/rollout-ledger/settle-capture.test.mts +270 -0
- package/src/swe-arena/activation.mts +228 -0
- package/src/swe-arena/activation.test.mts +303 -0
- package/src/swe-arena/arms.ts +30 -14
- package/src/swe-arena/briefing.mts +217 -0
- package/src/swe-arena/briefing.test.mts +178 -0
- package/src/swe-arena/calibrate.ts +102 -1
- package/src/swe-arena/cell-evidence.mts +41 -9
- package/src/swe-arena/factory-command-container.ts +284 -0
- package/src/swe-arena/factory-judge-child.mts +228 -0
- package/src/swe-arena/factory.test.mts +643 -0
- package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +51 -0
- package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +29 -0
- package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +64 -0
- package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +48 -0
- package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +29 -0
- package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +48 -0
- package/src/swe-arena/fixtures/factory/loops-28/calibration.md +47 -0
- package/src/swe-arena/fixtures/factory/loops-28/manifest.json +30 -0
- package/src/swe-arena/fixtures/factory/loops-28/spec.md +50 -0
- package/src/swe-arena/fixtures/run-report/README.md +43 -0
- package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +173 -0
- package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +100 -0
- package/src/swe-arena/fixtures/run-report/gen3-rollup.json +551 -0
- package/src/swe-arena/fixtures/run-report/gen3-rollup.md +64 -0
- package/src/swe-arena/fixtures.ts +135 -2
- package/src/swe-arena/gepa-seat.mts +583 -0
- package/src/swe-arena/gepa-seat.test.mts +635 -0
- package/src/swe-arena/ledger-orphans.mts +77 -0
- package/src/swe-arena/ledger-orphans.test.mts +147 -0
- package/src/swe-arena/lineage-record.mts +164 -0
- package/src/swe-arena/lineage-record.test.mts +115 -0
- package/src/swe-arena/outer-loop.mts +677 -27
- package/src/swe-arena/premeasured-from-cells.mts +281 -0
- package/src/swe-arena/premeasured-from-cells.test.mts +180 -0
- package/src/swe-arena/proposer-fanout.mts +295 -14
- package/src/swe-arena/proposer-fanout.test.mts +247 -0
- package/src/swe-arena/proposer-provenance.mts +177 -0
- package/src/swe-arena/proposer-provenance.test.mts +106 -0
- package/src/swe-arena/run-experiment.mts +367 -1
- package/src/swe-arena/run-report.mts +75 -0
- package/src/swe-arena/run-supervisor.test.mts +5 -3
- package/src/swe-arena/score-split.mts +140 -0
- package/src/swe-arena/score-split.test.mts +123 -0
- package/src/swe-arena/types.ts +52 -0
- package/src/swe-structural.mts +245 -837
- package/dist/chunk-LRRD7NAG.js.map +0 -1
- /package/dist/{chunk-CKUVRZ2T.js.map → chunk-3U5TXJZS.js.map} +0 -0
- /package/dist/{chunk-YCGY7UIZ.js.map → chunk-7GRVHU22.js.map} +0 -0
- /package/dist/{chunk-Z7ML6L77.js.map → chunk-HWST3SED.js.map} +0 -0
- /package/dist/{chunk-SYDW647C.js.map → chunk-IA2FBTWC.js.map} +0 -0
- /package/dist/{chunk-R67DFVLO.js.map → chunk-IFVINJ4B.js.map} +0 -0
- /package/dist/{chunk-R36V2VP7.js.map → chunk-IZ5M6OAC.js.map} +0 -0
- /package/dist/{chunk-ODT47UAY.js.map → chunk-K3BQGZCT.js.map} +0 -0
- /package/dist/{chunk-IFAV6KEM.js.map → chunk-KP5KD6EN.js.map} +0 -0
- /package/dist/{chunk-ZEWMTR5M.js.map → chunk-MQMRLGOG.js.map} +0 -0
- /package/dist/{chunk-TSWPNOYM.js.map → chunk-NQG5XDSB.js.map} +0 -0
- /package/dist/{chunk-7WSD27QQ.js.map → chunk-PB64GYIG.js.map} +0 -0
- /package/dist/{chunk-HBSWHQNJ.js.map → chunk-RCYQEFNX.js.map} +0 -0
- /package/dist/{chunk-J3KDJNX2.js.map → chunk-RH5F53JT.js.map} +0 -0
- /package/dist/{chunk-UAIOHCUK.js.map → chunk-SFLA7OH3.js.map} +0 -0
- /package/dist/{chunk-Y6O2OCUO.js.map → chunk-SHM6MRRF.js.map} +0 -0
- /package/dist/{chunk-KDIKRJGB.js.map → chunk-SHYIRB7I.js.map} +0 -0
- /package/dist/{chunk-HHXFIHXC.js.map → chunk-SVR2LKYI.js.map} +0 -0
- /package/dist/{chunk-PWQVGAJB.js.map → chunk-V7AEBY6U.js.map} +0 -0
- /package/dist/{chunk-2PVVP7GN.js.map → chunk-XKEFIFIC.js.map} +0 -0
- /package/dist/{chunk-JRWWGMK7.js.map → chunk-XYA4XSNU.js.map} +0 -0
- /package/dist/{chunk-X5YKXC6V.js.map → chunk-YSMEKBTD.js.map} +0 -0
- /package/dist/{chunk-2XU6OGEN.js.map → chunk-Z4TZ76N7.js.map} +0 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { buildAuthorPrompt, extractStrategySource, parseAuditVerdict } from './quant-loop.mts'
|
|
3
|
+
import { bootstrapWindows } from './windows.ts'
|
|
4
|
+
import type { AlignedBars } from './data.ts'
|
|
5
|
+
|
|
6
|
+
const V2_MODULE = `
|
|
7
|
+
interface Bar { date: string; open: number; high: number; low: number; close: number; volume: number }
|
|
8
|
+
interface StrategyContext { symbols: string[]; t: number; history: Bar[][]; weights: number[]; equity: number }
|
|
9
|
+
interface TargetPosition { symbol: string; weight: number }
|
|
10
|
+
export function onBar(ctx: StrategyContext): TargetPosition[] | null {
|
|
11
|
+
if (ctx.t < 20) return null
|
|
12
|
+
return [{ symbol: ctx.symbols[0], weight: 0.5 }]
|
|
13
|
+
}
|
|
14
|
+
`
|
|
15
|
+
|
|
16
|
+
describe('extractStrategySource — v2 contract', () => {
|
|
17
|
+
it('accepts a fenced module exporting onBar', () => {
|
|
18
|
+
const out = extractStrategySource('Here is my strategy:\n```ts\n' + V2_MODULE + '\n```\n')
|
|
19
|
+
expect(out.ok).toBe(true)
|
|
20
|
+
if (out.ok) expect(out.code).toContain('export function onBar')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('rejects a v1 generateSignals-only reply (the contract moved)', () => {
|
|
24
|
+
const v1 = '```ts\nexport function generateSignals(bars: unknown[]): unknown[] { return [] }\n```'
|
|
25
|
+
const out = extractStrategySource(v1)
|
|
26
|
+
expect(out.ok).toBe(false)
|
|
27
|
+
if (!out.ok) expect(out.reason).toMatch(/onBar/)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('rejects non-self-contained modules (banned identifiers)', () => {
|
|
31
|
+
const dirty = '```ts\nimport fs from "node:fs"\nexport function onBar(ctx: unknown) { return null }\n```'
|
|
32
|
+
const out = extractStrategySource(dirty)
|
|
33
|
+
expect(out.ok).toBe(false)
|
|
34
|
+
if (!out.ok) expect(out.reason).toMatch(/import/)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('takes the LAST onBar block when the author narrates with several', () => {
|
|
38
|
+
const text = '```ts\nexport function onBar(a: unknown) { return null } // draft\n```\nbetter:\n```ts\n' + V2_MODULE + '```'
|
|
39
|
+
const out = extractStrategySource(text)
|
|
40
|
+
expect(out.ok).toBe(true)
|
|
41
|
+
if (out.ok) expect(out.code).toContain('ctx.t < 20')
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('buildAuthorPrompt — authored against the v2 contract', () => {
|
|
46
|
+
it('states onBar, structural truncation, the OMS seam, and the acceptance bar', () => {
|
|
47
|
+
const universe: AlignedBars = {
|
|
48
|
+
tickers: ['IDX', 'S01'],
|
|
49
|
+
dates: Array.from({ length: 800 }, (_, i) => `d${i}`),
|
|
50
|
+
bars: [],
|
|
51
|
+
}
|
|
52
|
+
const windows = bootstrapWindows(800, { k: 4, windowDays: 200, seed: 7, warmupDays: 100 })
|
|
53
|
+
const prompt = buildAuthorPrompt({
|
|
54
|
+
universe,
|
|
55
|
+
windows,
|
|
56
|
+
baselineTable: ' (table)',
|
|
57
|
+
threshold: 0.25,
|
|
58
|
+
nTried: 3,
|
|
59
|
+
})
|
|
60
|
+
expect(prompt).toContain('export function onBar(ctx: StrategyContext)')
|
|
61
|
+
expect(prompt).toContain('ctx.history[k].length === ctx.t + 1')
|
|
62
|
+
expect(prompt).toContain('shared rebalancer')
|
|
63
|
+
expect(prompt).toContain('try #3')
|
|
64
|
+
expect(prompt).not.toContain('generateSignals')
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('parseAuditVerdict', () => {
|
|
69
|
+
it('parses clean and leak verdicts, favoring the last JSON object', () => {
|
|
70
|
+
expect(parseAuditVerdict('{"verdict":"clean"}')).toEqual({ verdict: 'clean', evidence: '' })
|
|
71
|
+
const leak = parseAuditVerdict('thinking... {"verdict":"clean"} wait no {"verdict":"leak","evidence":"Date.now()"}')
|
|
72
|
+
expect(leak).toEqual({ verdict: 'leak', evidence: 'Date.now()' })
|
|
73
|
+
expect(parseAuditVerdict('no json here')).toBeNull()
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pinned baseline: buy and hold the benchmark index (bars[0]) from day 0.
|
|
3
|
+
* One fill, then nothing — the cheapest possible exposure to the market.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Bar, Signal } from '../../types.ts'
|
|
7
|
+
|
|
8
|
+
export function generateSignals(bars: Bar[][]): Signal[] {
|
|
9
|
+
const weights = bars.map((_, k) => (k === 0 ? 1 : 0))
|
|
10
|
+
return [{ t: 0, weights }]
|
|
11
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pinned baseline: 1/N across the whole universe (index included), rebalanced
|
|
3
|
+
* every 21 trading days (~monthly). Harvests diversification with bounded
|
|
4
|
+
* turnover.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Bar, Signal } from '../../types.ts'
|
|
8
|
+
|
|
9
|
+
const REBALANCE_EVERY = 21
|
|
10
|
+
|
|
11
|
+
export function generateSignals(bars: Bar[][]): Signal[] {
|
|
12
|
+
const n = bars.length
|
|
13
|
+
const totalDays = bars[0]?.length ?? 0
|
|
14
|
+
const weights = bars.map(() => 1 / n)
|
|
15
|
+
const signals: Signal[] = []
|
|
16
|
+
for (let t = 0; t < totalDays; t += REBALANCE_EVERY) {
|
|
17
|
+
signals.push({ t, weights: [...weights] })
|
|
18
|
+
}
|
|
19
|
+
return signals
|
|
20
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pinned baseline: SMA(20/100) crossover momentum. Each day t (after warmup)
|
|
3
|
+
* a ticker is "trending" when its 20-day simple moving average of closes —
|
|
4
|
+
* computed from bars up to and including t — is above its 100-day SMA. Equity
|
|
5
|
+
* splits equally across trending tickers; no trend anywhere = 100% cash.
|
|
6
|
+
* Signals are emitted only when the trending set changes, so turnover stays
|
|
7
|
+
* proportional to regime changes rather than to calendar days.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Bar, Signal } from '../../types.ts'
|
|
11
|
+
|
|
12
|
+
const FAST = 20
|
|
13
|
+
const SLOW = 100
|
|
14
|
+
|
|
15
|
+
export function generateSignals(bars: Bar[][]): Signal[] {
|
|
16
|
+
const n = bars.length
|
|
17
|
+
const totalDays = bars[0]?.length ?? 0
|
|
18
|
+
// Prefix sums of closes per ticker: sum[t+1] = closes[0..t] summed.
|
|
19
|
+
const prefix: number[][] = bars.map((series) => {
|
|
20
|
+
const p = new Array<number>(series.length + 1).fill(0)
|
|
21
|
+
for (let t = 0; t < series.length; t++) p[t + 1] = p[t]! + series[t]!.close
|
|
22
|
+
return p
|
|
23
|
+
})
|
|
24
|
+
const sma = (k: number, t: number, len: number): number =>
|
|
25
|
+
(prefix[k]![t + 1]! - prefix[k]![t + 1 - len]!) / len
|
|
26
|
+
|
|
27
|
+
const signals: Signal[] = []
|
|
28
|
+
let lastKey = ''
|
|
29
|
+
for (let t = SLOW - 1; t < totalDays; t++) {
|
|
30
|
+
const trending: number[] = []
|
|
31
|
+
for (let k = 0; k < n; k++) {
|
|
32
|
+
if (sma(k, t, FAST) > sma(k, t, SLOW)) trending.push(k)
|
|
33
|
+
}
|
|
34
|
+
const key = trending.join(',')
|
|
35
|
+
if (key === lastKey) continue
|
|
36
|
+
lastKey = key
|
|
37
|
+
const weights = new Array<number>(n).fill(0)
|
|
38
|
+
for (const k of trending) weights[k] = 1 / Math.max(trending.length, 1)
|
|
39
|
+
signals.push({ t, weights })
|
|
40
|
+
}
|
|
41
|
+
return signals
|
|
42
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUANT-ARENA strategy contract (v2 — incremental, OMS-mediated).
|
|
3
|
+
*
|
|
4
|
+
* A strategy is ONE module exporting `onBar(ctx: StrategyContext)`. The
|
|
5
|
+
* harness calls it once per trading day, in order, and the strategy answers
|
|
6
|
+
* with either a full set of target portfolio weights or `null` ("hold").
|
|
7
|
+
*
|
|
8
|
+
* - `ctx.history[k]` is ticker k's daily bars UP TO AND INCLUDING today
|
|
9
|
+
* (`ctx.history[k].length === ctx.t + 1`, always). The harness physically
|
|
10
|
+
* slices the data before every call: bars after today DO NOT EXIST in the
|
|
11
|
+
* array, so look-ahead is structurally impossible through `history`.
|
|
12
|
+
* `history[0]` is ALWAYS the index (the buy-and-hold benchmark asset);
|
|
13
|
+
* `history[1..]` are the rest of the universe, in `ctx.symbols` order.
|
|
14
|
+
* - Returning `TargetPosition[]` requests a rebalance: each entry names a
|
|
15
|
+
* symbol and a target fraction of equity. Weights must be >= 0 and sum
|
|
16
|
+
* to <= 1 (the remainder sits in cash earning 0). Violations kill the
|
|
17
|
+
* candidate — fail-closed, not silently clamped. Symbols omitted from the
|
|
18
|
+
* list are targeted to 0 (sold).
|
|
19
|
+
* - Returning `null` means "no trade today": positions drift with prices.
|
|
20
|
+
* - FILL TIMING: a rebalance decided at the close of day `t` fills at the
|
|
21
|
+
* NEXT day's open. Every fill pays cost + slippage (basis points, one-way)
|
|
22
|
+
* on traded dollars — churn is expensive.
|
|
23
|
+
* - Strategies NEVER construct or emit `Order`s. The shared rebalancer
|
|
24
|
+
* (oms.ts) is the only component that turns target weights into orders,
|
|
25
|
+
* so sizing, side, and long-only enforcement are uniform across every
|
|
26
|
+
* candidate and baseline.
|
|
27
|
+
* - Determinism: `onBar` must be a pure function of `ctx`. No RNG, no
|
|
28
|
+
* clock, no memory of prior calls that a re-run would not rebuild. The
|
|
29
|
+
* lab re-runs candidates on truncated data; any divergence in decisions
|
|
30
|
+
* up to the cutoff kills the candidate.
|
|
31
|
+
*
|
|
32
|
+
* The v1 batch contract (`generateSignals(bars): Signal[]`) remains for the
|
|
33
|
+
* pinned baselines and old fixtures; `driver.ts` wraps it so batch strategies
|
|
34
|
+
* run unchanged under the incremental harness.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
export interface Bar {
|
|
38
|
+
/** ISO date, e.g. '2019-03-08'. */
|
|
39
|
+
date: string
|
|
40
|
+
open: number
|
|
41
|
+
high: number
|
|
42
|
+
low: number
|
|
43
|
+
close: number
|
|
44
|
+
volume: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// v2 incremental contract.
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
export interface StrategyContext {
|
|
52
|
+
/** Universe tickers; symbols[0] is the benchmark index. */
|
|
53
|
+
symbols: string[]
|
|
54
|
+
/** Today's day index into the shared date axis. */
|
|
55
|
+
t: number
|
|
56
|
+
/** history[k] = bars for symbols[k], indices 0..t ONLY (length t + 1). */
|
|
57
|
+
history: Bar[][]
|
|
58
|
+
/** Current portfolio weights per symbol (drifted, marked at today's close). */
|
|
59
|
+
weights: number[]
|
|
60
|
+
/** Current equity (starts at 1). */
|
|
61
|
+
equity: number
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface TargetPosition {
|
|
65
|
+
symbol: string
|
|
66
|
+
/** Target fraction of equity; >= 0, and the sum over all targets <= 1. */
|
|
67
|
+
weight: number
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface Strategy {
|
|
71
|
+
/** Called once per bar. Return targets to rebalance, or null to hold. */
|
|
72
|
+
onBar(ctx: StrategyContext): TargetPosition[] | null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface StrategyModuleV2 {
|
|
76
|
+
onBar: Strategy['onBar']
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// OMS layer. Orders are produced ONLY by the shared rebalancer (oms.ts) from
|
|
81
|
+
// the strategy's target weights — never by strategy code.
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
export type OrderSide = 'buy' | 'sell'
|
|
85
|
+
export type OrderType = 'market' | 'limit'
|
|
86
|
+
export type TimeInForce = 'day' | 'gtc' | 'ioc' | 'fok'
|
|
87
|
+
|
|
88
|
+
export interface Order {
|
|
89
|
+
/** Deterministic id, unique within a run (e.g. 'qa-t42-S03'). */
|
|
90
|
+
clientOrderId: string
|
|
91
|
+
symbol: string
|
|
92
|
+
side: OrderSide
|
|
93
|
+
/** Absolute quantity in shares (fractional allowed — see oms.ts). */
|
|
94
|
+
qty: number
|
|
95
|
+
type: OrderType
|
|
96
|
+
limitPrice?: number
|
|
97
|
+
tif: TimeInForce
|
|
98
|
+
/** Free-form provenance tag, e.g. the candidate id. */
|
|
99
|
+
tag?: string
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Turns target weights into orders given the portfolio state at decision
|
|
103
|
+
* time. The one shared implementation lives in oms.ts (long-only in v2). */
|
|
104
|
+
export type Rebalancer = (args: {
|
|
105
|
+
targets: TargetPosition[]
|
|
106
|
+
symbols: string[]
|
|
107
|
+
/** Equity at decision time (close of day t). */
|
|
108
|
+
equity: number
|
|
109
|
+
/** Last price per symbol at decision time (close of day t). */
|
|
110
|
+
prices: number[]
|
|
111
|
+
/** Current position value in dollars per symbol. */
|
|
112
|
+
positionValues: number[]
|
|
113
|
+
/** Decision day index — used for deterministic order ids. */
|
|
114
|
+
t: number
|
|
115
|
+
tag?: string
|
|
116
|
+
}) => Order[]
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// v1 batch contract (baselines + legacy fixtures; wrapped by driver.ts).
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
export interface Signal {
|
|
123
|
+
/** Day index into the shared date axis. */
|
|
124
|
+
t: number
|
|
125
|
+
/** Target weight per ticker (same order as `bars`). */
|
|
126
|
+
weights: number[]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type GenerateSignals = (bars: Bar[][]) => Signal[]
|
|
130
|
+
|
|
131
|
+
export interface StrategyModule {
|
|
132
|
+
generateSignals: GenerateSignals
|
|
133
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for the persistent vectorbt scoring worker (python/vbt-worker.py).
|
|
3
|
+
*
|
|
4
|
+
* One worker is spawned per campaign (cold numba JIT is paid once, at the
|
|
5
|
+
* `ready` handshake) and every scoring call is a JSONL request/response over
|
|
6
|
+
* stdin/stdout. The worker is the OFFICIAL scorer; the TS engine
|
|
7
|
+
* (backtest.ts) remains as contract prefilter + leak-audit substrate only.
|
|
8
|
+
*
|
|
9
|
+
* Crash discipline (the ledger-orphan posture, applied to in-flight work):
|
|
10
|
+
* if the worker dies, every pending request settles IMMEDIATELY with a
|
|
11
|
+
* `VbtWorkerCrashError` — nothing is left orphaned awaiting a dead process.
|
|
12
|
+
* The next request lazily respawns the worker, and `score()` retries a
|
|
13
|
+
* request exactly once if the crash happened while it was in flight. A
|
|
14
|
+
* second crash on the same request propagates — fail loud, not retry-loop.
|
|
15
|
+
*
|
|
16
|
+
* Environment: pinned by python/pyproject.toml + python/uv.lock and launched
|
|
17
|
+
* through `uv run --frozen`, so the scoring stack is byte-reproducible.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from 'node:child_process'
|
|
21
|
+
import { createInterface } from 'node:readline'
|
|
22
|
+
import { existsSync } from 'node:fs'
|
|
23
|
+
import { join } from 'node:path'
|
|
24
|
+
import { fileURLToPath } from 'node:url'
|
|
25
|
+
import type { Bar, Signal } from './types.ts'
|
|
26
|
+
|
|
27
|
+
export const VBT_PYTHON_DIR = fileURLToPath(new URL('./python', import.meta.url))
|
|
28
|
+
|
|
29
|
+
export interface VbtWindowStats {
|
|
30
|
+
totalReturn: number
|
|
31
|
+
/** Positive convention (0.12 = a 12% peak-to-trough drawdown). */
|
|
32
|
+
maxDD: number
|
|
33
|
+
/** Annualized, sample-std, 252 trading days — same formula as backtest.ts. */
|
|
34
|
+
sharpe: number
|
|
35
|
+
trades: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface VbtScore {
|
|
39
|
+
windows: VbtWindowStats[]
|
|
40
|
+
full: VbtWindowStats
|
|
41
|
+
/** Group equity per day, close-marked, equity[0] = 1. */
|
|
42
|
+
equity: number[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface VbtScoreRequest {
|
|
46
|
+
/** T x N fill prices (next-open model). */
|
|
47
|
+
openPrices: number[][]
|
|
48
|
+
/** T x N valuation prices. */
|
|
49
|
+
closePrices: number[][]
|
|
50
|
+
/** One row per day: the decision made at that day's close, or null. */
|
|
51
|
+
weights: Array<number[] | null>
|
|
52
|
+
costBps: number
|
|
53
|
+
slippageBps: number
|
|
54
|
+
/** [start, end) day-index ranges to score. */
|
|
55
|
+
windows: Array<[number, number]>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class VbtWorkerCrashError extends Error {
|
|
59
|
+
constructor(detail: string) {
|
|
60
|
+
super(`vbt worker crashed: ${detail}`)
|
|
61
|
+
this.name = 'VbtWorkerCrashError'
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface Pending {
|
|
66
|
+
resolve: (value: Record<string, unknown>) => void
|
|
67
|
+
reject: (error: Error) => void
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface VbtWorkerOptions {
|
|
71
|
+
pythonDir?: string
|
|
72
|
+
/** Covers spawn + cold numba JIT on first start. */
|
|
73
|
+
readyTimeoutMs?: number
|
|
74
|
+
requestTimeoutMs?: number
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class VbtWorker {
|
|
78
|
+
private readonly pythonDir: string
|
|
79
|
+
private readonly readyTimeoutMs: number
|
|
80
|
+
private readonly requestTimeoutMs: number
|
|
81
|
+
private child: ChildProcessWithoutNullStreams | null = null
|
|
82
|
+
private ready: Promise<void> | null = null
|
|
83
|
+
private pending = new Map<number, Pending>()
|
|
84
|
+
private nextId = 1
|
|
85
|
+
private closed = false
|
|
86
|
+
private stderrTail: string[] = []
|
|
87
|
+
|
|
88
|
+
constructor(options: VbtWorkerOptions = {}) {
|
|
89
|
+
this.pythonDir = options.pythonDir ?? VBT_PYTHON_DIR
|
|
90
|
+
this.readyTimeoutMs = options.readyTimeoutMs ?? 180_000
|
|
91
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 120_000
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** True when the pinned worker environment can be launched here: `uv` on
|
|
95
|
+
* PATH and the lockfile present. Tests use this to skip-with-reason. */
|
|
96
|
+
static isAvailable(pythonDir = VBT_PYTHON_DIR): boolean {
|
|
97
|
+
if (!existsSync(join(pythonDir, 'uv.lock'))) return false
|
|
98
|
+
const probe = spawnSync('uv', ['--version'], { stdio: 'ignore' })
|
|
99
|
+
return probe.status === 0
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private start(): Promise<void> {
|
|
103
|
+
if (this.ready !== null) return this.ready
|
|
104
|
+
if (this.closed) return Promise.reject(new Error('vbt worker: client closed'))
|
|
105
|
+
const workerPath = join(this.pythonDir, 'vbt-worker.py')
|
|
106
|
+
const child = spawn('uv', ['run', '--frozen', '--project', this.pythonDir, 'python', workerPath], {
|
|
107
|
+
cwd: this.pythonDir,
|
|
108
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
109
|
+
})
|
|
110
|
+
this.child = child
|
|
111
|
+
this.stderrTail = []
|
|
112
|
+
// EPIPE on a dying worker's stdin is handled by the exit path (settle +
|
|
113
|
+
// respawn) — a stream 'error' event must not crash the host process.
|
|
114
|
+
child.stdin.on('error', () => {})
|
|
115
|
+
child.stderr.setEncoding('utf8')
|
|
116
|
+
child.stderr.on('data', (chunk: string) => {
|
|
117
|
+
this.stderrTail.push(chunk)
|
|
118
|
+
if (this.stderrTail.length > 40) this.stderrTail.shift()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
this.ready = new Promise<void>((resolveReady, rejectReady) => {
|
|
122
|
+
let isReady = false
|
|
123
|
+
const readyTimer = setTimeout(() => {
|
|
124
|
+
if (!isReady) {
|
|
125
|
+
rejectReady(new Error(`vbt worker: no ready handshake within ${this.readyTimeoutMs}ms`))
|
|
126
|
+
child.kill('SIGKILL')
|
|
127
|
+
}
|
|
128
|
+
}, this.readyTimeoutMs)
|
|
129
|
+
|
|
130
|
+
const lines = createInterface({ input: child.stdout })
|
|
131
|
+
lines.on('line', (line) => {
|
|
132
|
+
let parsed: Record<string, unknown>
|
|
133
|
+
try {
|
|
134
|
+
parsed = JSON.parse(line) as Record<string, unknown>
|
|
135
|
+
} catch {
|
|
136
|
+
return // non-protocol noise on stdout is ignored, never fatal
|
|
137
|
+
}
|
|
138
|
+
if (parsed.ready === true) {
|
|
139
|
+
isReady = true
|
|
140
|
+
clearTimeout(readyTimer)
|
|
141
|
+
resolveReady()
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
const id = parsed.id
|
|
145
|
+
if (typeof id === 'number') {
|
|
146
|
+
const waiter = this.pending.get(id)
|
|
147
|
+
if (waiter !== undefined) {
|
|
148
|
+
this.pending.delete(id)
|
|
149
|
+
waiter.resolve(parsed)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
child.on('exit', (code, signal) => {
|
|
155
|
+
clearTimeout(readyTimer)
|
|
156
|
+
const detail = `exit code=${code} signal=${signal} stderr: ${this.stderrTail.join('').slice(-800)}`
|
|
157
|
+
if (!isReady) rejectReady(new VbtWorkerCrashError(detail))
|
|
158
|
+
this.settleAllPending(new VbtWorkerCrashError(detail))
|
|
159
|
+
this.child = null
|
|
160
|
+
this.ready = null // next request respawns
|
|
161
|
+
})
|
|
162
|
+
child.on('error', (cause) => {
|
|
163
|
+
clearTimeout(readyTimer)
|
|
164
|
+
if (!isReady) rejectReady(new VbtWorkerCrashError(String(cause)))
|
|
165
|
+
this.settleAllPending(new VbtWorkerCrashError(String(cause)))
|
|
166
|
+
this.child = null
|
|
167
|
+
this.ready = null
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
return this.ready
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Ledger-orphan discipline: a dead worker never leaves an unresolved
|
|
174
|
+
* request behind — everything in flight settles NOW. */
|
|
175
|
+
private settleAllPending(error: Error): void {
|
|
176
|
+
const waiters = [...this.pending.values()]
|
|
177
|
+
this.pending.clear()
|
|
178
|
+
for (const waiter of waiters) waiter.reject(error)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private async request(payload: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
182
|
+
await this.start()
|
|
183
|
+
const child = this.child
|
|
184
|
+
if (child === null) throw new VbtWorkerCrashError('worker exited during handshake')
|
|
185
|
+
const id = this.nextId++
|
|
186
|
+
const reply = await new Promise<Record<string, unknown>>((resolve, reject) => {
|
|
187
|
+
const timer = setTimeout(() => {
|
|
188
|
+
this.pending.delete(id)
|
|
189
|
+
reject(new Error(`vbt worker: request ${id} timed out after ${this.requestTimeoutMs}ms`))
|
|
190
|
+
child.kill('SIGKILL')
|
|
191
|
+
}, this.requestTimeoutMs)
|
|
192
|
+
this.pending.set(id, {
|
|
193
|
+
resolve: (value) => {
|
|
194
|
+
clearTimeout(timer)
|
|
195
|
+
resolve(value)
|
|
196
|
+
},
|
|
197
|
+
reject: (error) => {
|
|
198
|
+
clearTimeout(timer)
|
|
199
|
+
reject(error)
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
try {
|
|
203
|
+
child.stdin.write(JSON.stringify({ id, ...payload }) + '\n')
|
|
204
|
+
} catch {
|
|
205
|
+
// A destroyed stdin means the worker is dying; the exit handler
|
|
206
|
+
// settles this pending request with VbtWorkerCrashError.
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
if (reply.ok !== true) {
|
|
210
|
+
throw new Error(`vbt worker: request failed: ${String(reply.error ?? 'unknown error')}`)
|
|
211
|
+
}
|
|
212
|
+
return reply
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async ping(): Promise<string> {
|
|
216
|
+
const reply = await this.request({ op: 'ping' })
|
|
217
|
+
return String(reply.vbt)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async score(req: VbtScoreRequest): Promise<VbtScore> {
|
|
221
|
+
const payload = {
|
|
222
|
+
op: 'score',
|
|
223
|
+
open_prices: req.openPrices,
|
|
224
|
+
close_prices: req.closePrices,
|
|
225
|
+
weights: req.weights,
|
|
226
|
+
cost_bps: req.costBps,
|
|
227
|
+
slippage_bps: req.slippageBps,
|
|
228
|
+
windows: req.windows,
|
|
229
|
+
}
|
|
230
|
+
let reply: Record<string, unknown>
|
|
231
|
+
try {
|
|
232
|
+
reply = await this.request(payload)
|
|
233
|
+
} catch (cause) {
|
|
234
|
+
if (!(cause instanceof VbtWorkerCrashError)) throw cause
|
|
235
|
+
// One retry after a crash-respawn; a second crash propagates.
|
|
236
|
+
reply = await this.request(payload)
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
windows: reply.windows as VbtWindowStats[],
|
|
240
|
+
full: reply.full as VbtWindowStats,
|
|
241
|
+
equity: reply.equity as number[],
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async close(): Promise<void> {
|
|
246
|
+
this.closed = true
|
|
247
|
+
const child = this.child
|
|
248
|
+
this.child = null
|
|
249
|
+
this.ready = null
|
|
250
|
+
this.settleAllPending(new Error('vbt worker: client closed'))
|
|
251
|
+
if (child !== null) {
|
|
252
|
+
child.stdin.end()
|
|
253
|
+
await new Promise<void>((resolve) => {
|
|
254
|
+
const killTimer = setTimeout(() => {
|
|
255
|
+
child.kill('SIGKILL')
|
|
256
|
+
resolve()
|
|
257
|
+
}, 3_000)
|
|
258
|
+
child.on('exit', () => {
|
|
259
|
+
clearTimeout(killTimer)
|
|
260
|
+
resolve()
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Adapters: aligned bars + Signal[] -> the worker's matrix protocol.
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
/** Row-major T x N open/close price matrices from aligned bar series. */
|
|
272
|
+
export function barsToPriceMatrices(bars: Bar[][]): { open: number[][]; close: number[][] } {
|
|
273
|
+
const N = bars.length
|
|
274
|
+
const T = bars[0]?.length ?? 0
|
|
275
|
+
const open: number[][] = new Array(T)
|
|
276
|
+
const close: number[][] = new Array(T)
|
|
277
|
+
for (let t = 0; t < T; t++) {
|
|
278
|
+
const openRow = new Array<number>(N)
|
|
279
|
+
const closeRow = new Array<number>(N)
|
|
280
|
+
for (let k = 0; k < N; k++) {
|
|
281
|
+
openRow[k] = bars[k]![t]!.open
|
|
282
|
+
closeRow[k] = bars[k]![t]!.close
|
|
283
|
+
}
|
|
284
|
+
open[t] = openRow
|
|
285
|
+
close[t] = closeRow
|
|
286
|
+
}
|
|
287
|
+
return { open, close }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Decision rows for the worker: weights at each decision day, null rows on
|
|
291
|
+
* no-rebalance days (JSON's stand-in for the NaN convention). */
|
|
292
|
+
export function signalsToWeightRows(signals: Signal[], totalDays: number): Array<number[] | null> {
|
|
293
|
+
const rows: Array<number[] | null> = new Array(totalDays).fill(null)
|
|
294
|
+
for (const signal of signals) {
|
|
295
|
+
if (!Number.isInteger(signal.t) || signal.t < 0 || signal.t >= totalDays) {
|
|
296
|
+
throw new Error(`signalsToWeightRows: signal.t=${signal.t} outside [0, ${totalDays})`)
|
|
297
|
+
}
|
|
298
|
+
if (rows[signal.t] !== null) throw new Error(`signalsToWeightRows: two signals share t=${signal.t}`)
|
|
299
|
+
rows[signal.t] = signal.weights
|
|
300
|
+
}
|
|
301
|
+
return rows
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Score a decision record on the official vectorbt engine. */
|
|
305
|
+
export async function scoreSignals(
|
|
306
|
+
worker: VbtWorker,
|
|
307
|
+
bars: Bar[][],
|
|
308
|
+
signals: Signal[],
|
|
309
|
+
config: { costBps: number; slippageBps: number },
|
|
310
|
+
windows: Array<[number, number]>,
|
|
311
|
+
): Promise<VbtScore> {
|
|
312
|
+
const { open, close } = barsToPriceMatrices(bars)
|
|
313
|
+
return worker.score({
|
|
314
|
+
openPrices: open,
|
|
315
|
+
closePrices: close,
|
|
316
|
+
weights: signalsToWeightRows(signals, open.length),
|
|
317
|
+
costBps: config.costBps,
|
|
318
|
+
slippageBps: config.slippageBps,
|
|
319
|
+
windows,
|
|
320
|
+
})
|
|
321
|
+
}
|