@tangle-network/agent-bench 0.11.3 → 0.13.0

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 (170) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/HARNESS.md +6 -2
  3. package/README.md +1 -4
  4. package/package.json +5 -5
  5. package/scripts/run-package-tests.mjs +2 -2
  6. package/src/quant-arena/README.md +0 -144
  7. package/src/quant-arena/backtest.test.mts +0 -135
  8. package/src/quant-arena/backtest.ts +0 -218
  9. package/src/quant-arena/data.test.mts +0 -44
  10. package/src/quant-arena/data.ts +0 -141
  11. package/src/quant-arena/driver.test.mts +0 -253
  12. package/src/quant-arena/driver.ts +0 -219
  13. package/src/quant-arena/fixtures/data/PROVENANCE.md +0 -26
  14. package/src/quant-arena/fixtures/data/holdout/IDX.csv +0 -523
  15. package/src/quant-arena/fixtures/data/holdout/S01.csv +0 -523
  16. package/src/quant-arena/fixtures/data/holdout/S02.csv +0 -523
  17. package/src/quant-arena/fixtures/data/holdout/S03.csv +0 -523
  18. package/src/quant-arena/fixtures/data/holdout/S04.csv +0 -523
  19. package/src/quant-arena/fixtures/data/holdout/S05.csv +0 -523
  20. package/src/quant-arena/fixtures/data/holdout/S06.csv +0 -523
  21. package/src/quant-arena/fixtures/data/holdout/S07.csv +0 -523
  22. package/src/quant-arena/fixtures/data/holdout/S08.csv +0 -523
  23. package/src/quant-arena/fixtures/data/holdout/S09.csv +0 -523
  24. package/src/quant-arena/fixtures/data/holdout/S10.csv +0 -523
  25. package/src/quant-arena/fixtures/data/insample/IDX.csv +0 -2087
  26. package/src/quant-arena/fixtures/data/insample/S01.csv +0 -2087
  27. package/src/quant-arena/fixtures/data/insample/S02.csv +0 -2087
  28. package/src/quant-arena/fixtures/data/insample/S03.csv +0 -2087
  29. package/src/quant-arena/fixtures/data/insample/S04.csv +0 -2087
  30. package/src/quant-arena/fixtures/data/insample/S05.csv +0 -2087
  31. package/src/quant-arena/fixtures/data/insample/S06.csv +0 -2087
  32. package/src/quant-arena/fixtures/data/insample/S07.csv +0 -2087
  33. package/src/quant-arena/fixtures/data/insample/S08.csv +0 -2087
  34. package/src/quant-arena/fixtures/data/insample/S09.csv +0 -2087
  35. package/src/quant-arena/fixtures/data/insample/S10.csv +0 -2087
  36. package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +0 -16
  37. package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +0 -5
  38. package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +0 -171
  39. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +0 -119
  40. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +0 -119
  41. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +0 -105
  42. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +0 -102
  43. package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +0 -4
  44. package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +0 -2
  45. package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +0 -84
  46. package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +0 -117
  47. package/src/quant-arena/holdout-certify.mts +0 -206
  48. package/src/quant-arena/holdout-certify.test.mts +0 -82
  49. package/src/quant-arena/leak-audit.test.mts +0 -79
  50. package/src/quant-arena/leak-audit.ts +0 -95
  51. package/src/quant-arena/make-fixtures.mts +0 -161
  52. package/src/quant-arena/multiplicity.test.mts +0 -68
  53. package/src/quant-arena/multiplicity.ts +0 -87
  54. package/src/quant-arena/nautilus-certify.ts +0 -31
  55. package/src/quant-arena/oms.ts +0 -90
  56. package/src/quant-arena/profiles/quant-researcher.profile.json +0 -12
  57. package/src/quant-arena/python/pyproject.toml +0 -8
  58. package/src/quant-arena/python/uv.lock +0 -1297
  59. package/src/quant-arena/python/vbt-worker.py +0 -192
  60. package/src/quant-arena/quant-loop.mts +0 -840
  61. package/src/quant-arena/quant-loop.test.mts +0 -75
  62. package/src/quant-arena/strategies/buy-hold-index/strategy.ts +0 -11
  63. package/src/quant-arena/strategies/equal-weight/strategy.ts +0 -20
  64. package/src/quant-arena/strategies/sma-crossover/strategy.ts +0 -42
  65. package/src/quant-arena/types.ts +0 -133
  66. package/src/quant-arena/vbt-client.ts +0 -321
  67. package/src/quant-arena/vbt-parity.test.mts +0 -183
  68. package/src/quant-arena/windows.test.mts +0 -45
  69. package/src/quant-arena/windows.ts +0 -54
  70. package/src/rollout-ledger/backfill-swe-arena.mts +0 -610
  71. package/src/rollout-ledger/backfill-swe-arena.test.mts +0 -347
  72. package/src/rollout-ledger/settle-capture.mts +0 -448
  73. package/src/rollout-ledger/settle-capture.test.mts +0 -270
  74. package/src/swe-arena/activation.mts +0 -225
  75. package/src/swe-arena/activation.test.mts +0 -300
  76. package/src/swe-arena/analyze.ts +0 -211
  77. package/src/swe-arena/arms.ts +0 -862
  78. package/src/swe-arena/bootstrap-meta.mts +0 -188
  79. package/src/swe-arena/bootstrap-meta.test.mts +0 -51
  80. package/src/swe-arena/briefing.mts +0 -217
  81. package/src/swe-arena/briefing.test.mts +0 -179
  82. package/src/swe-arena/calibrate.ts +0 -217
  83. package/src/swe-arena/capabilities.mts +0 -76
  84. package/src/swe-arena/capabilities.test.mts +0 -57
  85. package/src/swe-arena/capacity.ts +0 -198
  86. package/src/swe-arena/cell-evidence.mts +0 -437
  87. package/src/swe-arena/cell-evidence.test.mts +0 -248
  88. package/src/swe-arena/diagnosis-ensemble.test.mts +0 -210
  89. package/src/swe-arena/diagnosis-ensemble.ts +0 -523
  90. package/src/swe-arena/execution.test.mts +0 -1171
  91. package/src/swe-arena/factory-command-container.ts +0 -284
  92. package/src/swe-arena/factory-judge-child.mts +0 -228
  93. package/src/swe-arena/factory.test.mts +0 -645
  94. package/src/swe-arena/fixtures/analyze.py +0 -80
  95. package/src/swe-arena/fixtures/excludes.txt +0 -8
  96. package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +0 -51
  97. package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +0 -29
  98. package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +0 -64
  99. package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +0 -48
  100. package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +0 -29
  101. package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +0 -48
  102. package/src/swe-arena/fixtures/factory/loops-28/calibration.md +0 -47
  103. package/src/swe-arena/fixtures/factory/loops-28/manifest.json +0 -30
  104. package/src/swe-arena/fixtures/factory/loops-28/spec.md +0 -50
  105. package/src/swe-arena/fixtures/gen1-salvage/README.md +0 -45
  106. package/src/swe-arena/fixtures/gen1-salvage/cand0-e6d7361.diff +0 -116
  107. package/src/swe-arena/fixtures/gen1-salvage/cand1-76a8590.diff +0 -293
  108. package/src/swe-arena/fixtures/holdout-preregister.log +0 -12
  109. package/src/swe-arena/fixtures/holdout.json +0 -44
  110. package/src/swe-arena/fixtures/instances.json +0 -146
  111. package/src/swe-arena/fixtures/ledger.jsonl +0 -12
  112. package/src/swe-arena/fixtures/patches/pallets__flask-5014.solo.patch +0 -36
  113. package/src/swe-arena/fixtures/patches/pydata__xarray-4687.sup.patch +0 -33
  114. package/src/swe-arena/fixtures/rejudge.jsonl +0 -15
  115. package/src/swe-arena/fixtures/rematch.jsonl +0 -3
  116. package/src/swe-arena/fixtures/rematch2.jsonl +0 -3
  117. package/src/swe-arena/fixtures/rematch3.jsonl +0 -3
  118. package/src/swe-arena/fixtures/run-report/README.md +0 -43
  119. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +0 -173
  120. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +0 -100
  121. package/src/swe-arena/fixtures/run-report/gen3-rollup.json +0 -551
  122. package/src/swe-arena/fixtures/run-report/gen3-rollup.md +0 -64
  123. package/src/swe-arena/fixtures/sup-journal-true.json +0 -19
  124. package/src/swe-arena/fixtures/verify/astropy__astropy-13033.sh +0 -48
  125. package/src/swe-arena/fixtures/verify/django__django-11532.sh +0 -50
  126. package/src/swe-arena/fixtures/verify/matplotlib__matplotlib-20826.sh +0 -76
  127. package/src/swe-arena/fixtures/verify/pydata__xarray-4687.sh +0 -44
  128. package/src/swe-arena/fixtures/verify/pytest-dev__pytest-6197.sh +0 -32
  129. package/src/swe-arena/fixtures/verify/sphinx-doc__sphinx-9658.sh +0 -51
  130. package/src/swe-arena/fixtures/worker-tokens.json +0 -42
  131. package/src/swe-arena/fixtures.ts +0 -237
  132. package/src/swe-arena/gepa-seat.mts +0 -886
  133. package/src/swe-arena/gepa-seat.test.mts +0 -1136
  134. package/src/swe-arena/holdout-certify.mts +0 -408
  135. package/src/swe-arena/holdout-certify.test.mts +0 -160
  136. package/src/swe-arena/implementation-ref.test.mts +0 -64
  137. package/src/swe-arena/implementation-ref.ts +0 -62
  138. package/src/swe-arena/judge-child.mts +0 -37
  139. package/src/swe-arena/ledger-orphans.mts +0 -77
  140. package/src/swe-arena/ledger-orphans.test.mts +0 -149
  141. package/src/swe-arena/manifest.mts +0 -293
  142. package/src/swe-arena/manifest.test.mts +0 -169
  143. package/src/swe-arena/materialize.ts +0 -142
  144. package/src/swe-arena/outer-loop.mts +0 -2854
  145. package/src/swe-arena/outer-loop.test.mts +0 -714
  146. package/src/swe-arena/parity.test.mts +0 -87
  147. package/src/swe-arena/premeasured-from-cells.mts +0 -296
  148. package/src/swe-arena/premeasured-from-cells.test.mts +0 -201
  149. package/src/swe-arena/proc.test.mts +0 -172
  150. package/src/swe-arena/proc.ts +0 -260
  151. package/src/swe-arena/profiles/deepseek-author.profile.json +0 -12
  152. package/src/swe-arena/profiles/default-author.profile.json +0 -12
  153. package/src/swe-arena/proposer-fanout.mts +0 -736
  154. package/src/swe-arena/proposer-fanout.test.mts +0 -660
  155. package/src/swe-arena/proposer-provenance.mts +0 -176
  156. package/src/swe-arena/proposer-provenance.test.mts +0 -106
  157. package/src/swe-arena/reconcile.ts +0 -0
  158. package/src/swe-arena/replay.mts +0 -183
  159. package/src/swe-arena/replay.test.mts +0 -300
  160. package/src/swe-arena/run-experiment.mts +0 -729
  161. package/src/swe-arena/run-report.mts +0 -75
  162. package/src/swe-arena/run-supervisor.mjs +0 -297
  163. package/src/swe-arena/run-supervisor.test.mts +0 -539
  164. package/src/swe-arena/score-split.mts +0 -140
  165. package/src/swe-arena/score-split.test.mts +0 -123
  166. package/src/swe-arena/scratch-worktree-serialization.test.mts +0 -72
  167. package/src/swe-arena/scratch-worktree.test.mts +0 -56
  168. package/src/swe-arena/scratch-worktree.ts +0 -64
  169. package/src/swe-arena/serialized-judge.ts +0 -414
  170. package/src/swe-arena/types.ts +0 -218
@@ -1,183 +0,0 @@
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
- })
@@ -1,45 +0,0 @@
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
- })
@@ -1,54 +0,0 @@
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
- }