@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.
Files changed (191) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/HARNESS.md +43 -0
  3. package/dist/adapters.js +23 -23
  4. package/dist/benchmarks/_harness.d.ts +1 -1
  5. package/dist/benchmarks/_harness.js +1 -1
  6. package/dist/benchmarks/aec-bench.js +2 -2
  7. package/dist/benchmarks/agentbench.js +2 -2
  8. package/dist/benchmarks/appworld.js +2 -2
  9. package/dist/benchmarks/bfcl.js +2 -2
  10. package/dist/benchmarks/commit0.js +2 -2
  11. package/dist/benchmarks/crag.js +2 -2
  12. package/dist/benchmarks/dabstep.js +2 -2
  13. package/dist/benchmarks/enterpriseops-gym.js +2 -2
  14. package/dist/benchmarks/finresearchbench.js +2 -2
  15. package/dist/benchmarks/nomiracl.js +2 -2
  16. package/dist/benchmarks/open-rag-bench.js +2 -2
  17. package/dist/benchmarks/programbench.js +2 -2
  18. package/dist/benchmarks/ragbench.js +2 -2
  19. package/dist/benchmarks/swe-bench.js +2 -2
  20. package/dist/benchmarks/t2-ragbench.js +2 -2
  21. package/dist/benchmarks/tau-bench-shared.js +2 -2
  22. package/dist/benchmarks/tau2-bench.js +3 -3
  23. package/dist/benchmarks/tau3-banking.js +3 -3
  24. package/dist/benchmarks/terminal-bench.js +2 -2
  25. package/dist/benchmarks/toollm.js +2 -2
  26. package/dist/benchmarks/webarena-verified.js +2 -2
  27. package/dist/{chunk-CKUVRZ2T.js → chunk-3U5TXJZS.js} +2 -2
  28. package/dist/{chunk-YCGY7UIZ.js → chunk-7GRVHU22.js} +2 -2
  29. package/dist/{chunk-Z7ML6L77.js → chunk-HWST3SED.js} +2 -2
  30. package/dist/{chunk-SYDW647C.js → chunk-IA2FBTWC.js} +2 -2
  31. package/dist/{chunk-R67DFVLO.js → chunk-IFVINJ4B.js} +2 -2
  32. package/dist/{chunk-R36V2VP7.js → chunk-IZ5M6OAC.js} +2 -2
  33. package/dist/{chunk-ODT47UAY.js → chunk-K3BQGZCT.js} +2 -2
  34. package/dist/{chunk-IFAV6KEM.js → chunk-KP5KD6EN.js} +2 -2
  35. package/dist/{chunk-ZEWMTR5M.js → chunk-MQMRLGOG.js} +2 -2
  36. package/dist/{chunk-TSWPNOYM.js → chunk-NQG5XDSB.js} +2 -2
  37. package/dist/{chunk-7WSD27QQ.js → chunk-PB64GYIG.js} +2 -2
  38. package/dist/{chunk-HBSWHQNJ.js → chunk-RCYQEFNX.js} +3 -3
  39. package/dist/{chunk-J3KDJNX2.js → chunk-RH5F53JT.js} +2 -2
  40. package/dist/{chunk-UAIOHCUK.js → chunk-SFLA7OH3.js} +3 -3
  41. package/dist/{chunk-Y6O2OCUO.js → chunk-SHM6MRRF.js} +2 -2
  42. package/dist/{chunk-KDIKRJGB.js → chunk-SHYIRB7I.js} +2 -2
  43. package/dist/{chunk-HHXFIHXC.js → chunk-SVR2LKYI.js} +2 -2
  44. package/dist/{chunk-PWQVGAJB.js → chunk-V7AEBY6U.js} +21 -21
  45. package/dist/{chunk-LRRD7NAG.js → chunk-WSKWVEQB.js} +18 -2
  46. package/dist/chunk-WSKWVEQB.js.map +1 -0
  47. package/dist/{chunk-2PVVP7GN.js → chunk-XKEFIFIC.js} +2 -2
  48. package/dist/{chunk-JRWWGMK7.js → chunk-XYA4XSNU.js} +2 -2
  49. package/dist/{chunk-X5YKXC6V.js → chunk-YSMEKBTD.js} +2 -2
  50. package/dist/{chunk-2XU6OGEN.js → chunk-Z4TZ76N7.js} +2 -2
  51. package/dist/index.js +23 -23
  52. package/package.json +4 -4
  53. package/scripts/verify-packed-consumer.mjs +1 -1
  54. package/src/benchmarks/_harness.ts +20 -2
  55. package/src/benchmarks/humaneval.test.mts +2 -2
  56. package/src/quant-arena/README.md +144 -0
  57. package/src/quant-arena/backtest.test.mts +135 -0
  58. package/src/quant-arena/backtest.ts +218 -0
  59. package/src/quant-arena/data.test.mts +44 -0
  60. package/src/quant-arena/data.ts +141 -0
  61. package/src/quant-arena/driver.test.mts +253 -0
  62. package/src/quant-arena/driver.ts +219 -0
  63. package/src/quant-arena/fixtures/data/PROVENANCE.md +26 -0
  64. package/src/quant-arena/fixtures/data/holdout/IDX.csv +523 -0
  65. package/src/quant-arena/fixtures/data/holdout/S01.csv +523 -0
  66. package/src/quant-arena/fixtures/data/holdout/S02.csv +523 -0
  67. package/src/quant-arena/fixtures/data/holdout/S03.csv +523 -0
  68. package/src/quant-arena/fixtures/data/holdout/S04.csv +523 -0
  69. package/src/quant-arena/fixtures/data/holdout/S05.csv +523 -0
  70. package/src/quant-arena/fixtures/data/holdout/S06.csv +523 -0
  71. package/src/quant-arena/fixtures/data/holdout/S07.csv +523 -0
  72. package/src/quant-arena/fixtures/data/holdout/S08.csv +523 -0
  73. package/src/quant-arena/fixtures/data/holdout/S09.csv +523 -0
  74. package/src/quant-arena/fixtures/data/holdout/S10.csv +523 -0
  75. package/src/quant-arena/fixtures/data/insample/IDX.csv +2087 -0
  76. package/src/quant-arena/fixtures/data/insample/S01.csv +2087 -0
  77. package/src/quant-arena/fixtures/data/insample/S02.csv +2087 -0
  78. package/src/quant-arena/fixtures/data/insample/S03.csv +2087 -0
  79. package/src/quant-arena/fixtures/data/insample/S04.csv +2087 -0
  80. package/src/quant-arena/fixtures/data/insample/S05.csv +2087 -0
  81. package/src/quant-arena/fixtures/data/insample/S06.csv +2087 -0
  82. package/src/quant-arena/fixtures/data/insample/S07.csv +2087 -0
  83. package/src/quant-arena/fixtures/data/insample/S08.csv +2087 -0
  84. package/src/quant-arena/fixtures/data/insample/S09.csv +2087 -0
  85. package/src/quant-arena/fixtures/data/insample/S10.csv +2087 -0
  86. package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +16 -0
  87. package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +5 -0
  88. package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +171 -0
  89. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +119 -0
  90. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +119 -0
  91. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +105 -0
  92. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +102 -0
  93. package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +4 -0
  94. package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +2 -0
  95. package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +84 -0
  96. package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +117 -0
  97. package/src/quant-arena/holdout-certify.mts +206 -0
  98. package/src/quant-arena/holdout-certify.test.mts +82 -0
  99. package/src/quant-arena/leak-audit.test.mts +79 -0
  100. package/src/quant-arena/leak-audit.ts +95 -0
  101. package/src/quant-arena/make-fixtures.mts +161 -0
  102. package/src/quant-arena/multiplicity.test.mts +68 -0
  103. package/src/quant-arena/multiplicity.ts +87 -0
  104. package/src/quant-arena/nautilus-certify.ts +31 -0
  105. package/src/quant-arena/oms.ts +90 -0
  106. package/src/quant-arena/profiles/quant-researcher.profile.json +7 -0
  107. package/src/quant-arena/python/pyproject.toml +8 -0
  108. package/src/quant-arena/python/uv.lock +1297 -0
  109. package/src/quant-arena/python/vbt-worker.py +192 -0
  110. package/src/quant-arena/quant-loop.mts +813 -0
  111. package/src/quant-arena/quant-loop.test.mts +75 -0
  112. package/src/quant-arena/strategies/buy-hold-index/strategy.ts +11 -0
  113. package/src/quant-arena/strategies/equal-weight/strategy.ts +20 -0
  114. package/src/quant-arena/strategies/sma-crossover/strategy.ts +42 -0
  115. package/src/quant-arena/types.ts +133 -0
  116. package/src/quant-arena/vbt-client.ts +321 -0
  117. package/src/quant-arena/vbt-parity.test.mts +183 -0
  118. package/src/quant-arena/windows.test.mts +45 -0
  119. package/src/quant-arena/windows.ts +54 -0
  120. package/src/rollout-ledger/backfill-swe-arena.mts +606 -0
  121. package/src/rollout-ledger/backfill-swe-arena.test.mts +338 -0
  122. package/src/rollout-ledger/settle-capture.mts +442 -0
  123. package/src/rollout-ledger/settle-capture.test.mts +270 -0
  124. package/src/swe-arena/activation.mts +228 -0
  125. package/src/swe-arena/activation.test.mts +303 -0
  126. package/src/swe-arena/arms.ts +30 -14
  127. package/src/swe-arena/briefing.mts +217 -0
  128. package/src/swe-arena/briefing.test.mts +178 -0
  129. package/src/swe-arena/calibrate.ts +102 -1
  130. package/src/swe-arena/cell-evidence.mts +41 -9
  131. package/src/swe-arena/factory-command-container.ts +284 -0
  132. package/src/swe-arena/factory-judge-child.mts +228 -0
  133. package/src/swe-arena/factory.test.mts +643 -0
  134. package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +51 -0
  135. package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +29 -0
  136. package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +64 -0
  137. package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +48 -0
  138. package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +29 -0
  139. package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +48 -0
  140. package/src/swe-arena/fixtures/factory/loops-28/calibration.md +47 -0
  141. package/src/swe-arena/fixtures/factory/loops-28/manifest.json +30 -0
  142. package/src/swe-arena/fixtures/factory/loops-28/spec.md +50 -0
  143. package/src/swe-arena/fixtures/run-report/README.md +43 -0
  144. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +173 -0
  145. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +100 -0
  146. package/src/swe-arena/fixtures/run-report/gen3-rollup.json +551 -0
  147. package/src/swe-arena/fixtures/run-report/gen3-rollup.md +64 -0
  148. package/src/swe-arena/fixtures.ts +135 -2
  149. package/src/swe-arena/gepa-seat.mts +583 -0
  150. package/src/swe-arena/gepa-seat.test.mts +635 -0
  151. package/src/swe-arena/ledger-orphans.mts +77 -0
  152. package/src/swe-arena/ledger-orphans.test.mts +147 -0
  153. package/src/swe-arena/lineage-record.mts +164 -0
  154. package/src/swe-arena/lineage-record.test.mts +115 -0
  155. package/src/swe-arena/outer-loop.mts +677 -27
  156. package/src/swe-arena/premeasured-from-cells.mts +281 -0
  157. package/src/swe-arena/premeasured-from-cells.test.mts +180 -0
  158. package/src/swe-arena/proposer-fanout.mts +295 -14
  159. package/src/swe-arena/proposer-fanout.test.mts +247 -0
  160. package/src/swe-arena/proposer-provenance.mts +177 -0
  161. package/src/swe-arena/proposer-provenance.test.mts +106 -0
  162. package/src/swe-arena/run-experiment.mts +367 -1
  163. package/src/swe-arena/run-report.mts +75 -0
  164. package/src/swe-arena/run-supervisor.test.mts +5 -3
  165. package/src/swe-arena/score-split.mts +140 -0
  166. package/src/swe-arena/score-split.test.mts +123 -0
  167. package/src/swe-arena/types.ts +52 -0
  168. package/src/swe-structural.mts +245 -837
  169. package/dist/chunk-LRRD7NAG.js.map +0 -1
  170. /package/dist/{chunk-CKUVRZ2T.js.map → chunk-3U5TXJZS.js.map} +0 -0
  171. /package/dist/{chunk-YCGY7UIZ.js.map → chunk-7GRVHU22.js.map} +0 -0
  172. /package/dist/{chunk-Z7ML6L77.js.map → chunk-HWST3SED.js.map} +0 -0
  173. /package/dist/{chunk-SYDW647C.js.map → chunk-IA2FBTWC.js.map} +0 -0
  174. /package/dist/{chunk-R67DFVLO.js.map → chunk-IFVINJ4B.js.map} +0 -0
  175. /package/dist/{chunk-R36V2VP7.js.map → chunk-IZ5M6OAC.js.map} +0 -0
  176. /package/dist/{chunk-ODT47UAY.js.map → chunk-K3BQGZCT.js.map} +0 -0
  177. /package/dist/{chunk-IFAV6KEM.js.map → chunk-KP5KD6EN.js.map} +0 -0
  178. /package/dist/{chunk-ZEWMTR5M.js.map → chunk-MQMRLGOG.js.map} +0 -0
  179. /package/dist/{chunk-TSWPNOYM.js.map → chunk-NQG5XDSB.js.map} +0 -0
  180. /package/dist/{chunk-7WSD27QQ.js.map → chunk-PB64GYIG.js.map} +0 -0
  181. /package/dist/{chunk-HBSWHQNJ.js.map → chunk-RCYQEFNX.js.map} +0 -0
  182. /package/dist/{chunk-J3KDJNX2.js.map → chunk-RH5F53JT.js.map} +0 -0
  183. /package/dist/{chunk-UAIOHCUK.js.map → chunk-SFLA7OH3.js.map} +0 -0
  184. /package/dist/{chunk-Y6O2OCUO.js.map → chunk-SHM6MRRF.js.map} +0 -0
  185. /package/dist/{chunk-KDIKRJGB.js.map → chunk-SHYIRB7I.js.map} +0 -0
  186. /package/dist/{chunk-HHXFIHXC.js.map → chunk-SVR2LKYI.js.map} +0 -0
  187. /package/dist/{chunk-PWQVGAJB.js.map → chunk-V7AEBY6U.js.map} +0 -0
  188. /package/dist/{chunk-2PVVP7GN.js.map → chunk-XKEFIFIC.js.map} +0 -0
  189. /package/dist/{chunk-JRWWGMK7.js.map → chunk-XYA4XSNU.js.map} +0 -0
  190. /package/dist/{chunk-X5YKXC6V.js.map → chunk-YSMEKBTD.js.map} +0 -0
  191. /package/dist/{chunk-2XU6OGEN.js.map → chunk-Z4TZ76N7.js.map} +0 -0
@@ -0,0 +1,183 @@
1
+ /**
2
+ * PARITY CI: the TS reference engine (backtest.ts) and the official vectorbt
3
+ * scorer (python/vbt-worker.py) must agree on golden fixtures under the
4
+ * aligned fill model (decide at close t, fill at t+1's open, bps fees +
5
+ * slippage charged on traded dollars).
6
+ *
7
+ * Measured tolerance regimes (see the fully-invested note below):
8
+ * - no-trade: EXACT (both curves are flat 1.0);
9
+ * - any book holding a cash buffer >= accumulated fees: machine epsilon
10
+ * (measured ~2e-15 max relative diff over 300 days);
11
+ * - fully-invested books (weights sum to exactly 1): ~2e-3, a REAL model
12
+ * difference, not noise — the TS engine finances fees at 0% by letting
13
+ * cash go slightly negative, while vectorbt refuses negative cash and
14
+ * buys marginally less. vectorbt is the economically conservative side
15
+ * and is the official scorer; the TS engine remains prefilter-only.
16
+ *
17
+ * When a fixture disagrees, the failure output prints both curves' tails.
18
+ *
19
+ * Requires `uv` on PATH (python/uv.lock pins the worker env); otherwise the
20
+ * suite is SKIPPED with that reason — the seam under test is still real.
21
+ */
22
+
23
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest'
24
+ import { runBacktest, statsForRange } from './backtest.ts'
25
+ import { generateSignalsFromStrategy, runIncremental, strategyFromGenerateSignals } from './driver.ts'
26
+ import { generateSignals as smaCrossover } from './strategies/sma-crossover/strategy.ts'
27
+ import { scoreSignals, VbtWorker, type VbtScore } from './vbt-client.ts'
28
+ import type { Bar, Signal } from './types.ts'
29
+
30
+ const vbtAvailable = VbtWorker.isAvailable()
31
+
32
+ function syntheticBars(n: number, tickers: number): Bar[][] {
33
+ return Array.from({ length: tickers }, (_, k) =>
34
+ Array.from({ length: n }, (_, t) => {
35
+ const close = 100 + 10 * Math.sin(t / 7 + k) + 0.05 * t + 3 * Math.sin(t / 23 + 2 * k)
36
+ const open = 100 + 10 * Math.sin((t - 0.5) / 7 + k) + 0.05 * t + 3 * Math.sin((t - 0.5) / 23 + 2 * k)
37
+ return {
38
+ date: `d${String(t).padStart(4, '0')}`,
39
+ open,
40
+ high: Math.max(open, close) + 1,
41
+ low: Math.min(open, close) - 1,
42
+ close,
43
+ volume: 1000,
44
+ }
45
+ }),
46
+ )
47
+ }
48
+
49
+ const BT = { costBps: 10, slippageBps: 5 }
50
+
51
+ /** Assert two equity curves agree; on violation print both tails first. */
52
+ function expectCurveParity(tsEquity: number[], vbtEquity: number[], relTol: number, label: string): void {
53
+ expect(vbtEquity.length).toBe(tsEquity.length)
54
+ let worst = 0
55
+ let worstAt = -1
56
+ for (let t = 0; t < tsEquity.length; t++) {
57
+ const rel = Math.abs(tsEquity[t]! - vbtEquity[t]!) / Math.abs(tsEquity[t]!)
58
+ if (rel > worst) {
59
+ worst = rel
60
+ worstAt = t
61
+ }
62
+ }
63
+ if (worst > relTol) {
64
+ const tail = (curve: number[]) => curve.slice(-10).map((v) => v.toFixed(10)).join(', ')
65
+ console.error(
66
+ `PARITY FAILURE [${label}]: max relative diff ${worst.toExponential(3)} at t=${worstAt} (tol ${relTol})\n` +
67
+ ` TS tail: ${tail(tsEquity)}\n` +
68
+ ` VBT tail: ${tail(vbtEquity)}`,
69
+ )
70
+ }
71
+ expect(worst, `${label}: max relative equity diff (worst at t=${worstAt})`).toBeLessThanOrEqual(relTol)
72
+ }
73
+
74
+ describe.skipIf(!vbtAvailable)('TS engine vs vectorbt worker parity (golden fixtures)', () => {
75
+ let worker: VbtWorker
76
+
77
+ beforeAll(async () => {
78
+ worker = new VbtWorker()
79
+ await worker.ping() // pay the cold-JIT warmup once, up front
80
+ }, 240_000)
81
+
82
+ afterAll(async () => {
83
+ await worker.close()
84
+ })
85
+
86
+ async function compare(
87
+ label: string,
88
+ bars: Bar[][],
89
+ signals: Signal[],
90
+ relTol: number,
91
+ ): Promise<{ ts: ReturnType<typeof runBacktest>; vbt: VbtScore }> {
92
+ const T = bars[0]!.length
93
+ const windows: Array<[number, number]> = [
94
+ [0, T],
95
+ [Math.floor(T / 4), Math.floor(T / 2)],
96
+ ]
97
+ const ts = runBacktest(bars, signals, BT)
98
+ const vbt = await scoreSignals(worker, bars, signals, BT, windows)
99
+ expectCurveParity(ts.equity, vbt.equity, relTol, label)
100
+ // Window stats derive from the curves with the SAME formulas both sides,
101
+ // so they inherit the curve tolerance: compare RELATIVELY (Sharpe can be
102
+ // O(30) in magnitude on synthetic fixtures — digit-based closeness lies).
103
+ const statTol = relTol === 0 ? 1e-12 : Math.max(relTol * 10, 1e-11)
104
+ const expectRelClose = (actual: number, wanted: number, what: string): void => {
105
+ const rel = Math.abs(actual - wanted) / Math.max(1, Math.abs(wanted))
106
+ expect(rel, `${label}/${what}: vbt=${actual} ts=${wanted}`).toBeLessThanOrEqual(statTol)
107
+ }
108
+ for (const [i, [start, end]] of windows.entries()) {
109
+ const tsStats = statsForRange(ts, start, end)
110
+ const vbtStats = vbt.windows[i]!
111
+ expectRelClose(vbtStats.totalReturn, tsStats.totalReturn, `w${i}.totalReturn`)
112
+ expectRelClose(vbtStats.maxDD, tsStats.maxDrawdown, `w${i}.maxDD`)
113
+ expectRelClose(vbtStats.sharpe, tsStats.sharpe, `w${i}.sharpe`)
114
+ expect(vbtStats.trades).toBe(tsStats.tradeCount)
115
+ }
116
+ return { ts, vbt }
117
+ }
118
+
119
+ it('golden 1 — no-trade: both curves are exactly flat at 1', { timeout: 120_000 }, async () => {
120
+ const bars = syntheticBars(300, 3)
121
+ const { ts, vbt } = await compare('no-trade', bars, [], 0)
122
+ expect(ts.equity.every((e) => e === 1)).toBe(true)
123
+ expect(vbt.equity.every((e) => e === 1)).toBe(true)
124
+ expect(vbt.full.trades).toBe(0)
125
+ })
126
+
127
+ it('golden 2 — high-turnover daily flip (2% cash buffer): machine-epsilon parity', { timeout: 120_000 }, async () => {
128
+ const bars = syntheticBars(300, 3)
129
+ const flip: Signal[] = []
130
+ for (let t = 10; t < 290; t++) flip.push({ t, weights: t % 2 === 0 ? [0, 0.98, 0] : [0, 0, 0.98] })
131
+ const { vbt } = await compare('high-turnover-98', bars, flip, 1e-12)
132
+ expect(vbt.full.trades).toBeGreaterThan(500)
133
+ })
134
+
135
+ it('golden 3 — sma-crossover, 5 tickers (fully invested): documented fee-financing tolerance', { timeout: 120_000 }, async () => {
136
+ const bars = syntheticBars(500, 5)
137
+ await compare('sma-crossover', bars, smaCrossover(bars), 5e-3)
138
+ })
139
+
140
+ it('golden 4 — monthly partial-invest book: machine-epsilon parity', { timeout: 120_000 }, async () => {
141
+ const bars = syntheticBars(300, 3)
142
+ const monthly: Signal[] = []
143
+ for (let t = 0; t < 300; t += 21) monthly.push({ t, weights: [0.2, 0.3, 0.3] })
144
+ await compare('monthly-partial', bars, monthly, 1e-12)
145
+ })
146
+
147
+ it('golden 5 — the v2 demo candidate (authored via onBar) scores identically through the driver', { timeout: 120_000 }, async () => {
148
+ const mod = await import('./fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts')
149
+ const bars = syntheticBars(500, 5)
150
+ const symbols = bars.map((_, k) => (k === 0 ? 'IDX' : `T${String(k).padStart(2, '0')}`))
151
+ const strategy = { onBar: mod.onBar }
152
+ const signals = generateSignalsFromStrategy(strategy, { symbols, ...BT })(bars)
153
+ await compare('v2-demo-candidate', bars, signals, 5e-3)
154
+ // The driver's own equity path also matches the TS reference engine.
155
+ const run = runIncremental(bars, strategy, { ...BT, symbols })
156
+ const reference = runBacktest(bars, run.decisions, BT)
157
+ for (let t = 0; t < run.equityByDay.length; t++) {
158
+ expect(run.equityByDay[t]).toBe(reference.equity[t])
159
+ }
160
+ })
161
+
162
+ it('crash-restart: a killed worker settles in-flight requests and the retry succeeds', { timeout: 240_000 }, async () => {
163
+ const own = new VbtWorker()
164
+ try {
165
+ await own.ping()
166
+ // Reach into the client to kill the child mid-session (test-only).
167
+ const child = (own as unknown as { child: { kill(sig: string): void } | null }).child
168
+ expect(child).not.toBeNull()
169
+ child!.kill('SIGKILL')
170
+ // score() retries once across the crash: respawn + re-issue.
171
+ const bars = syntheticBars(60, 2)
172
+ const vbt = await scoreSignals(own, bars, [{ t: 5, weights: [0.5, 0.3] }], BT, [[0, 60]])
173
+ expect(vbt.equity.length).toBe(60)
174
+ expect(vbt.full.trades).toBe(2)
175
+ } finally {
176
+ await own.close()
177
+ }
178
+ })
179
+ })
180
+
181
+ describe.skipIf(vbtAvailable)('vectorbt worker unavailable', () => {
182
+ it.skip('skipped: `uv` not on PATH or python/uv.lock missing — parity fixtures need the pinned worker env', () => {})
183
+ })
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { bootstrapWindows, mulberry32 } from './windows.ts'
3
+
4
+ describe('bootstrapWindows', () => {
5
+ it('is deterministic: same seed, same windows — bit-identical', () => {
6
+ const a = bootstrapWindows(2086, { k: 8, windowDays: 504, seed: 42, warmupDays: 120 })
7
+ const b = bootstrapWindows(2086, { k: 8, windowDays: 504, seed: 42, warmupDays: 120 })
8
+ expect(a).toEqual(b)
9
+ })
10
+
11
+ it('different seeds draw different windows', () => {
12
+ const a = bootstrapWindows(2086, { k: 8, windowDays: 504, seed: 42, warmupDays: 120 })
13
+ const b = bootstrapWindows(2086, { k: 8, windowDays: 504, seed: 43, warmupDays: 120 })
14
+ expect(a).not.toEqual(b)
15
+ })
16
+
17
+ it('respects warmup and data bounds for every window', () => {
18
+ const windows = bootstrapWindows(2086, { k: 8, windowDays: 504, seed: 7, warmupDays: 120 })
19
+ expect(windows).toHaveLength(8)
20
+ for (const w of windows) {
21
+ expect(w.start).toBeGreaterThanOrEqual(120)
22
+ expect(w.end - w.start).toBe(504)
23
+ expect(w.end).toBeLessThanOrEqual(2086)
24
+ }
25
+ // Sorted ascending, so campaigns log them stably.
26
+ const starts = windows.map((w) => w.start)
27
+ expect(starts).toEqual([...starts].sort((a, b) => a - b))
28
+ })
29
+
30
+ it('fails loud when the data cannot fit a window', () => {
31
+ expect(() => bootstrapWindows(400, { k: 8, windowDays: 504, seed: 1, warmupDays: 120 })).toThrow(/cannot fit/)
32
+ expect(() => bootstrapWindows(2086, { k: 0, windowDays: 504, seed: 1, warmupDays: 120 })).toThrow(/positive integer/)
33
+ })
34
+
35
+ it('mulberry32 streams are reproducible and in [0, 1)', () => {
36
+ const r1 = mulberry32(123)
37
+ const r2 = mulberry32(123)
38
+ for (let i = 0; i < 100; i++) {
39
+ const v = r1()
40
+ expect(v).toBe(r2())
41
+ expect(v).toBeGreaterThanOrEqual(0)
42
+ expect(v).toBeLessThan(1)
43
+ }
44
+ })
45
+ })
@@ -0,0 +1,54 @@
1
+ /**
2
+ * In-sample evaluation windows: K overlapping blocks drawn by a seeded block
3
+ * bootstrap over the in-sample years. Deterministic given (totalDays, opts) —
4
+ * the same seed always yields the same windows, so every candidate in a
5
+ * campaign is scored on the SAME slices and reruns reproduce bit-identically.
6
+ */
7
+
8
+ export interface EvalWindow {
9
+ /** Day-index range [start, end) into the in-sample date axis. */
10
+ start: number
11
+ end: number
12
+ }
13
+
14
+ /** Small deterministic PRNG (mulberry32). Good enough for window draws; NOT
15
+ * for anything cryptographic. */
16
+ export function mulberry32(seed: number): () => number {
17
+ let a = seed >>> 0
18
+ return () => {
19
+ a = (a + 0x6d2b79f5) >>> 0
20
+ let t = a
21
+ t = Math.imul(t ^ (t >>> 15), t | 1)
22
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
23
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
24
+ }
25
+ }
26
+
27
+ export interface BootstrapWindowOptions {
28
+ k: number
29
+ windowDays: number
30
+ seed: number
31
+ /** Earliest allowed window start — leaves room for indicator warmup so a
32
+ * window never scores days where a lookback strategy is still all-cash by
33
+ * construction. */
34
+ warmupDays: number
35
+ }
36
+
37
+ export function bootstrapWindows(totalDays: number, opts: BootstrapWindowOptions): EvalWindow[] {
38
+ const { k, windowDays, seed, warmupDays } = opts
39
+ if (k < 1 || !Number.isInteger(k)) throw new Error(`bootstrapWindows: k must be a positive integer, got ${k}`)
40
+ if (windowDays < 2) throw new Error(`bootstrapWindows: windowDays must be >= 2, got ${windowDays}`)
41
+ const lastStart = totalDays - windowDays
42
+ if (lastStart < warmupDays) {
43
+ throw new Error(
44
+ `bootstrapWindows: ${totalDays} days cannot fit a ${windowDays}-day window after ${warmupDays} warmup days`,
45
+ )
46
+ }
47
+ const rand = mulberry32(seed)
48
+ const starts: number[] = []
49
+ for (let i = 0; i < k; i++) {
50
+ starts.push(warmupDays + Math.floor(rand() * (lastStart - warmupDays + 1)))
51
+ }
52
+ starts.sort((a, b) => a - b)
53
+ return starts.map((start) => ({ start, end: start + windowDays }))
54
+ }