@tangle-network/agent-bench 0.3.7 → 0.4.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 (211) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/HARNESS.md +43 -0
  3. package/README.md +7 -0
  4. package/dist/adapters.js +23 -23
  5. package/dist/benchmarks/_harness.d.ts +1 -1
  6. package/dist/benchmarks/_harness.js +1 -1
  7. package/dist/benchmarks/aec-bench.js +2 -2
  8. package/dist/benchmarks/agentbench.js +2 -2
  9. package/dist/benchmarks/appworld.js +2 -2
  10. package/dist/benchmarks/bfcl.js +2 -2
  11. package/dist/benchmarks/commit0.js +2 -2
  12. package/dist/benchmarks/crag.js +2 -2
  13. package/dist/benchmarks/dabstep.js +2 -2
  14. package/dist/benchmarks/enterpriseops-gym.js +2 -2
  15. package/dist/benchmarks/finresearchbench.js +2 -2
  16. package/dist/benchmarks/nomiracl.js +2 -2
  17. package/dist/benchmarks/open-rag-bench.js +2 -2
  18. package/dist/benchmarks/programbench.js +2 -2
  19. package/dist/benchmarks/ragbench.js +2 -2
  20. package/dist/benchmarks/swe-bench.js +2 -2
  21. package/dist/benchmarks/t2-ragbench.js +2 -2
  22. package/dist/benchmarks/tau-bench-shared.js +2 -2
  23. package/dist/benchmarks/tau2-bench.js +3 -3
  24. package/dist/benchmarks/tau3-banking.js +3 -3
  25. package/dist/benchmarks/terminal-bench.js +2 -2
  26. package/dist/benchmarks/toollm.js +2 -2
  27. package/dist/benchmarks/webarena-verified.js +2 -2
  28. package/dist/{chunk-CKUVRZ2T.js → chunk-3U5TXJZS.js} +2 -2
  29. package/dist/{chunk-YCGY7UIZ.js → chunk-7GRVHU22.js} +2 -2
  30. package/dist/{chunk-Z7ML6L77.js → chunk-HWST3SED.js} +2 -2
  31. package/dist/{chunk-SYDW647C.js → chunk-IA2FBTWC.js} +2 -2
  32. package/dist/{chunk-R67DFVLO.js → chunk-IFVINJ4B.js} +2 -2
  33. package/dist/{chunk-R36V2VP7.js → chunk-IZ5M6OAC.js} +2 -2
  34. package/dist/{chunk-ODT47UAY.js → chunk-K3BQGZCT.js} +2 -2
  35. package/dist/{chunk-IFAV6KEM.js → chunk-KP5KD6EN.js} +2 -2
  36. package/dist/{chunk-ZEWMTR5M.js → chunk-MQMRLGOG.js} +2 -2
  37. package/dist/{chunk-TSWPNOYM.js → chunk-NQG5XDSB.js} +2 -2
  38. package/dist/{chunk-7WSD27QQ.js → chunk-PB64GYIG.js} +2 -2
  39. package/dist/{chunk-HBSWHQNJ.js → chunk-RCYQEFNX.js} +3 -3
  40. package/dist/{chunk-J3KDJNX2.js → chunk-RH5F53JT.js} +2 -2
  41. package/dist/{chunk-UAIOHCUK.js → chunk-SFLA7OH3.js} +3 -3
  42. package/dist/{chunk-Y6O2OCUO.js → chunk-SHM6MRRF.js} +2 -2
  43. package/dist/{chunk-KDIKRJGB.js → chunk-SHYIRB7I.js} +2 -2
  44. package/dist/{chunk-HHXFIHXC.js → chunk-SVR2LKYI.js} +2 -2
  45. package/dist/{chunk-PWQVGAJB.js → chunk-V7AEBY6U.js} +21 -21
  46. package/dist/{chunk-LRRD7NAG.js → chunk-WSKWVEQB.js} +18 -2
  47. package/dist/chunk-WSKWVEQB.js.map +1 -0
  48. package/dist/{chunk-2PVVP7GN.js → chunk-XKEFIFIC.js} +2 -2
  49. package/dist/{chunk-JRWWGMK7.js → chunk-XYA4XSNU.js} +2 -2
  50. package/dist/{chunk-X5YKXC6V.js → chunk-YSMEKBTD.js} +2 -2
  51. package/dist/{chunk-2XU6OGEN.js → chunk-Z4TZ76N7.js} +2 -2
  52. package/dist/index.js +23 -23
  53. package/package.json +5 -5
  54. package/scripts/trata-hedge/README.md +6 -5
  55. package/scripts/verify-packed-consumer.mjs +1 -1
  56. package/src/benchmarks/_harness.ts +20 -2
  57. package/src/benchmarks/humaneval.test.mts +2 -2
  58. package/src/gate.ts +1 -1
  59. package/src/hev-eval.mts +5 -2
  60. package/src/hev-improve.mts +118 -73
  61. package/src/official-optimizer-config.mts +89 -0
  62. package/src/official-optimizer-config.test.mts +88 -0
  63. package/src/profiles.ts +2 -2
  64. package/src/quant-arena/README.md +144 -0
  65. package/src/quant-arena/backtest.test.mts +135 -0
  66. package/src/quant-arena/backtest.ts +218 -0
  67. package/src/quant-arena/data.test.mts +44 -0
  68. package/src/quant-arena/data.ts +141 -0
  69. package/src/quant-arena/driver.test.mts +253 -0
  70. package/src/quant-arena/driver.ts +219 -0
  71. package/src/quant-arena/fixtures/data/PROVENANCE.md +26 -0
  72. package/src/quant-arena/fixtures/data/holdout/IDX.csv +523 -0
  73. package/src/quant-arena/fixtures/data/holdout/S01.csv +523 -0
  74. package/src/quant-arena/fixtures/data/holdout/S02.csv +523 -0
  75. package/src/quant-arena/fixtures/data/holdout/S03.csv +523 -0
  76. package/src/quant-arena/fixtures/data/holdout/S04.csv +523 -0
  77. package/src/quant-arena/fixtures/data/holdout/S05.csv +523 -0
  78. package/src/quant-arena/fixtures/data/holdout/S06.csv +523 -0
  79. package/src/quant-arena/fixtures/data/holdout/S07.csv +523 -0
  80. package/src/quant-arena/fixtures/data/holdout/S08.csv +523 -0
  81. package/src/quant-arena/fixtures/data/holdout/S09.csv +523 -0
  82. package/src/quant-arena/fixtures/data/holdout/S10.csv +523 -0
  83. package/src/quant-arena/fixtures/data/insample/IDX.csv +2087 -0
  84. package/src/quant-arena/fixtures/data/insample/S01.csv +2087 -0
  85. package/src/quant-arena/fixtures/data/insample/S02.csv +2087 -0
  86. package/src/quant-arena/fixtures/data/insample/S03.csv +2087 -0
  87. package/src/quant-arena/fixtures/data/insample/S04.csv +2087 -0
  88. package/src/quant-arena/fixtures/data/insample/S05.csv +2087 -0
  89. package/src/quant-arena/fixtures/data/insample/S06.csv +2087 -0
  90. package/src/quant-arena/fixtures/data/insample/S07.csv +2087 -0
  91. package/src/quant-arena/fixtures/data/insample/S08.csv +2087 -0
  92. package/src/quant-arena/fixtures/data/insample/S09.csv +2087 -0
  93. package/src/quant-arena/fixtures/data/insample/S10.csv +2087 -0
  94. package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +16 -0
  95. package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +5 -0
  96. package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +171 -0
  97. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +119 -0
  98. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +119 -0
  99. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +105 -0
  100. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +102 -0
  101. package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +4 -0
  102. package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +2 -0
  103. package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +84 -0
  104. package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +117 -0
  105. package/src/quant-arena/holdout-certify.mts +206 -0
  106. package/src/quant-arena/holdout-certify.test.mts +82 -0
  107. package/src/quant-arena/leak-audit.test.mts +79 -0
  108. package/src/quant-arena/leak-audit.ts +95 -0
  109. package/src/quant-arena/make-fixtures.mts +161 -0
  110. package/src/quant-arena/multiplicity.test.mts +68 -0
  111. package/src/quant-arena/multiplicity.ts +87 -0
  112. package/src/quant-arena/nautilus-certify.ts +31 -0
  113. package/src/quant-arena/oms.ts +90 -0
  114. package/src/quant-arena/profiles/quant-researcher.profile.json +7 -0
  115. package/src/quant-arena/python/pyproject.toml +8 -0
  116. package/src/quant-arena/python/uv.lock +1297 -0
  117. package/src/quant-arena/python/vbt-worker.py +192 -0
  118. package/src/quant-arena/quant-loop.mts +813 -0
  119. package/src/quant-arena/quant-loop.test.mts +75 -0
  120. package/src/quant-arena/strategies/buy-hold-index/strategy.ts +11 -0
  121. package/src/quant-arena/strategies/equal-weight/strategy.ts +20 -0
  122. package/src/quant-arena/strategies/sma-crossover/strategy.ts +42 -0
  123. package/src/quant-arena/types.ts +133 -0
  124. package/src/quant-arena/vbt-client.ts +321 -0
  125. package/src/quant-arena/vbt-parity.test.mts +183 -0
  126. package/src/quant-arena/windows.test.mts +45 -0
  127. package/src/quant-arena/windows.ts +54 -0
  128. package/src/rollout-ledger/backfill-swe-arena.mts +606 -0
  129. package/src/rollout-ledger/backfill-swe-arena.test.mts +342 -0
  130. package/src/rollout-ledger/settle-capture.mts +442 -0
  131. package/src/rollout-ledger/settle-capture.test.mts +270 -0
  132. package/src/smoke-structural-rollout.mts +15 -9
  133. package/src/swe-arena/activation.mts +225 -0
  134. package/src/swe-arena/activation.test.mts +300 -0
  135. package/src/swe-arena/arms.ts +30 -14
  136. package/src/swe-arena/briefing.mts +217 -0
  137. package/src/swe-arena/briefing.test.mts +178 -0
  138. package/src/swe-arena/calibrate.ts +102 -1
  139. package/src/swe-arena/cell-evidence.mts +41 -9
  140. package/src/swe-arena/factory-command-container.ts +284 -0
  141. package/src/swe-arena/factory-judge-child.mts +228 -0
  142. package/src/swe-arena/factory.test.mts +643 -0
  143. package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +51 -0
  144. package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +29 -0
  145. package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +64 -0
  146. package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +48 -0
  147. package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +29 -0
  148. package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +48 -0
  149. package/src/swe-arena/fixtures/factory/loops-28/calibration.md +47 -0
  150. package/src/swe-arena/fixtures/factory/loops-28/manifest.json +30 -0
  151. package/src/swe-arena/fixtures/factory/loops-28/spec.md +50 -0
  152. package/src/swe-arena/fixtures/run-report/README.md +43 -0
  153. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +173 -0
  154. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +100 -0
  155. package/src/swe-arena/fixtures/run-report/gen3-rollup.json +551 -0
  156. package/src/swe-arena/fixtures/run-report/gen3-rollup.md +64 -0
  157. package/src/swe-arena/fixtures.ts +135 -2
  158. package/src/swe-arena/gepa-seat.mts +877 -0
  159. package/src/swe-arena/gepa-seat.test.mts +1059 -0
  160. package/src/swe-arena/implementation-ref.test.mts +64 -0
  161. package/src/swe-arena/implementation-ref.ts +62 -0
  162. package/src/swe-arena/ledger-orphans.mts +77 -0
  163. package/src/swe-arena/ledger-orphans.test.mts +147 -0
  164. package/src/swe-arena/outer-loop.mts +710 -33
  165. package/src/swe-arena/premeasured-from-cells.mts +281 -0
  166. package/src/swe-arena/premeasured-from-cells.test.mts +180 -0
  167. package/src/swe-arena/proposer-fanout.mts +327 -31
  168. package/src/swe-arena/proposer-fanout.test.mts +246 -0
  169. package/src/swe-arena/proposer-provenance.mts +172 -0
  170. package/src/swe-arena/proposer-provenance.test.mts +106 -0
  171. package/src/swe-arena/run-experiment.mts +367 -1
  172. package/src/swe-arena/run-report.mts +75 -0
  173. package/src/swe-arena/run-supervisor.test.mts +5 -3
  174. package/src/swe-arena/score-split.mts +140 -0
  175. package/src/swe-arena/score-split.test.mts +123 -0
  176. package/src/swe-arena/scratch-worktree.test.mts +55 -0
  177. package/src/swe-arena/scratch-worktree.ts +34 -0
  178. package/src/swe-arena/types.ts +52 -0
  179. package/src/swe-code-improve.mts +24 -25
  180. package/src/swe-improve.mts +129 -96
  181. package/src/swe-local-proof.mts +6 -1
  182. package/src/swe-stream.mts +4 -2
  183. package/src/swe-structural.mts +245 -837
  184. package/src/tb-container-executor.test.mts +30 -6
  185. package/src/tb-supervisor-sidecar.mts +2 -1
  186. package/src/trata-gepa.mts +182 -245
  187. package/dist/chunk-LRRD7NAG.js.map +0 -1
  188. package/src/live-improve-campaign-mbpp.mts +0 -641
  189. package/src/live-improve-campaign.mts +0 -500
  190. /package/dist/{chunk-CKUVRZ2T.js.map → chunk-3U5TXJZS.js.map} +0 -0
  191. /package/dist/{chunk-YCGY7UIZ.js.map → chunk-7GRVHU22.js.map} +0 -0
  192. /package/dist/{chunk-Z7ML6L77.js.map → chunk-HWST3SED.js.map} +0 -0
  193. /package/dist/{chunk-SYDW647C.js.map → chunk-IA2FBTWC.js.map} +0 -0
  194. /package/dist/{chunk-R67DFVLO.js.map → chunk-IFVINJ4B.js.map} +0 -0
  195. /package/dist/{chunk-R36V2VP7.js.map → chunk-IZ5M6OAC.js.map} +0 -0
  196. /package/dist/{chunk-ODT47UAY.js.map → chunk-K3BQGZCT.js.map} +0 -0
  197. /package/dist/{chunk-IFAV6KEM.js.map → chunk-KP5KD6EN.js.map} +0 -0
  198. /package/dist/{chunk-ZEWMTR5M.js.map → chunk-MQMRLGOG.js.map} +0 -0
  199. /package/dist/{chunk-TSWPNOYM.js.map → chunk-NQG5XDSB.js.map} +0 -0
  200. /package/dist/{chunk-7WSD27QQ.js.map → chunk-PB64GYIG.js.map} +0 -0
  201. /package/dist/{chunk-HBSWHQNJ.js.map → chunk-RCYQEFNX.js.map} +0 -0
  202. /package/dist/{chunk-J3KDJNX2.js.map → chunk-RH5F53JT.js.map} +0 -0
  203. /package/dist/{chunk-UAIOHCUK.js.map → chunk-SFLA7OH3.js.map} +0 -0
  204. /package/dist/{chunk-Y6O2OCUO.js.map → chunk-SHM6MRRF.js.map} +0 -0
  205. /package/dist/{chunk-KDIKRJGB.js.map → chunk-SHYIRB7I.js.map} +0 -0
  206. /package/dist/{chunk-HHXFIHXC.js.map → chunk-SVR2LKYI.js.map} +0 -0
  207. /package/dist/{chunk-PWQVGAJB.js.map → chunk-V7AEBY6U.js.map} +0 -0
  208. /package/dist/{chunk-2PVVP7GN.js.map → chunk-XKEFIFIC.js.map} +0 -0
  209. /package/dist/{chunk-JRWWGMK7.js.map → chunk-XYA4XSNU.js.map} +0 -0
  210. /package/dist/{chunk-X5YKXC6V.js.map → chunk-YSMEKBTD.js.map} +0 -0
  211. /package/dist/{chunk-2XU6OGEN.js.map → chunk-Z4TZ76N7.js.map} +0 -0
@@ -1,8 +1,6 @@
1
1
  /**
2
- * SELF-IMPROVEMENT on HumanEval the prompt-sensitive, VISIBLE-ORACLE counterpart
3
- * to the SWE-bench run. Same machinery (improve(surface:'prompt') + gepaProposer +
4
- * held-out gate), but the worker is a single chat completion and the judge is the
5
- * deterministic Docker checker (run the function against its own hidden unit tests).
2
+ * Official GEPA prompt optimization on HumanEval. The worker is a single chat
3
+ * completion and the judge is the deterministic Docker checker.
6
4
  *
7
5
  * WHY this exists: on SWE-bench the same GEPA loop was NULL because the grading test
8
6
  * is withheld — the worker cannot verify, so prompt wording cannot move resolve.
@@ -13,11 +11,22 @@
13
11
  * Worker + reflect models call the zai coding endpoint directly (no tangle router,
14
12
  * no WAF, no 503): TANGLE_API_KEY=$ZAI_API_KEY ROUTER_BASE=https://api.z.ai/api/coding/paas/v4
15
13
  */
16
- import { improve } from '@tangle-network/agent-runtime'
17
- import type { AgentProfile } from '@tangle-network/agent-interface'
14
+ import {
15
+ improve,
16
+ officialGepa,
17
+ type ReadonlyAgentProfile,
18
+ } from '@tangle-network/agent-runtime'
19
+ import {
20
+ canonicalCandidateDigest,
21
+ type AgentProfile,
22
+ } from '@tangle-network/agent-interface'
18
23
  import type { DispatchContext, JudgeConfig, Scenario } from '@tangle-network/agent-eval/contract'
19
- import { gepaProposer } from '@tangle-network/agent-eval/campaign'
20
24
  import { extractCode, loadHumanEval, runChecker, type HumanEvalTask } from './benchmarks/humaneval'
25
+ import {
26
+ assertCompleteCost,
27
+ officialOptimizerModel,
28
+ requiredTokenPricing,
29
+ } from './official-optimizer-config.mjs'
21
30
 
22
31
  // The SEED instruction GEPA evolves. Byte-identical to humaneval.ts basePrompt's
23
32
  // solveInstruction so the baseline arm reproduces the plain-prompt denominator.
@@ -26,7 +35,6 @@ const SEED_INSTRUCTION =
26
35
 
27
36
  interface Completion {
28
37
  text: string
29
- usd: number
30
38
  tokIn: number
31
39
  tokOut: number
32
40
  }
@@ -45,10 +53,7 @@ async function complete(base: string, key: string, model: string, prompt: string
45
53
  const text = d.choices?.[0]?.message?.content ?? ''
46
54
  const tokIn = d.usage?.prompt_tokens ?? 0
47
55
  const tokOut = d.usage?.completion_tokens ?? 0
48
- // zai glm pricing is ~ $0.6/M in, $2.2/M out (coding plan); a rough cost tag so the
49
- // stub-guard sees a real backend. Exact cost is not the metric (pass-rate is).
50
- const usd = (tokIn * 0.6 + tokOut * 2.2) / 1_000_000
51
- return { text, usd, tokIn, tokOut }
56
+ return { text, tokIn, tokOut }
52
57
  }
53
58
 
54
59
  async function main(): Promise<void> {
@@ -63,51 +68,81 @@ async function main(): Promise<void> {
63
68
  const reflectBase = process.env.REFLECT_BASE ?? base
64
69
  const reflectKey = process.env.REFLECT_KEY ?? key
65
70
  const trainN = Number(process.env.TRAIN_N ?? 12)
66
- const holdoutN = Number(process.env.HOLDOUT_N ?? 12)
71
+ const selectionN = Number(process.env.SELECTION_N ?? 12)
72
+ const testN = Number(process.env.TEST_N ?? 12)
67
73
  const offset = Number(process.env.OFFSET ?? 80)
68
- // generations=1 never exercises the GEPA Pareto/combine path (the frontier
69
- // needs >=1 completed generation before combine can fire) — default to a
70
- // multi-generation budget so the default run measures the full loop.
71
- const generations = Number(process.env.GENERATIONS ?? 6)
72
- const population = Number(process.env.POPULATION ?? 4)
74
+ const maxEvaluations = Number(process.env.MAX_EVALUATIONS ?? 24)
75
+ const maxProposerCostUsd = Number(process.env.MAX_PROPOSER_COST_USD ?? 5)
73
76
  const workerMaxTokens = Number(process.env.MAX_TOKENS ?? 6000)
74
77
  const reflectMaxTokens = Number(process.env.REFLECT_MAX_TOKENS ?? 8000)
75
78
  const maxConcurrency = Number(process.env.MAX_CONCURRENCY ?? 4)
79
+ const runDir = process.env.RUN_DIR ?? '.runs/humaneval-official-gepa'
80
+ if (process.env.DRYRUN) {
81
+ console.log(
82
+ `DRYRUN: imports OK (improve=${typeof improve}, officialGepa=${typeof officialGepa})`,
83
+ )
84
+ return
85
+ }
86
+ const workerPricing = requiredTokenPricing(process.env, 'WORKER')
87
+ const optimizer = officialOptimizerModel({
88
+ env: process.env,
89
+ model: reflectModel,
90
+ baseUrl: reflectBase,
91
+ apiKey: reflectKey,
92
+ maxCostUsd: maxProposerCostUsd,
93
+ maxOutputTokensPerRequest: reflectMaxTokens,
94
+ })
76
95
 
77
- // TRAIN and HOLDOUT are DISJOINT slices of the harder middle band (offset).
96
+ // All three partitions are disjoint slices of the harder middle band.
78
97
  const train = await loadHumanEval(trainN, offset)
79
- const holdout = await loadHumanEval(holdoutN, offset + trainN)
80
- const byId = new Map<string, HumanEvalTask>([...train, ...holdout].map((t) => [t.taskId, t]))
81
- const allIds = [...byId.keys()]
98
+ const selection = await loadHumanEval(selectionN, offset + trainN)
99
+ const testCases = await loadHumanEval(testN, offset + trainN + selectionN)
100
+ const byId = new Map<string, HumanEvalTask>(
101
+ [...train, ...selection, ...testCases].map((t) => [t.taskId, t]),
102
+ )
82
103
 
83
- console.log('═══ HumanEval self-improvement VISIBLE oracle (deterministic Docker checker) ═══')
84
- console.log(`worker=${workerModel} reflect=${reflectModel} base=${base}`)
104
+ console.log('=== HumanEval prompt optimization with official GEPA ===')
105
+ console.log(`worker=${workerModel} reflect=${reflectModel} base=${base}`)
85
106
  console.log(`train=[${train.map((t) => t.taskId).join(', ')}]`)
86
- console.log(`holdout=[${holdout.map((t) => t.taskId).join(', ')}]`)
87
- console.log(`generations=${generations} population=${population} offset=${offset} maxTokens=${workerMaxTokens}`)
88
- console.log(`≈ ${trainN * (1 + generations * population) + 2 * holdoutN} cells (each = 1 completion + 1 Docker check)\n`)
107
+ console.log(`selection=[${selection.map((t) => t.taskId).join(', ')}]`)
108
+ console.log(`test=[${testCases.map((t) => t.taskId).join(', ')}]`)
109
+ console.log(`maxEvaluations=${maxEvaluations} maxProposerCostUsd=${maxProposerCostUsd} offset=${offset} maxTokens=${workerMaxTokens}`)
110
+ console.log(`runDir=${runDir}\n`)
89
111
 
90
112
  const stats = { n: 0 }
91
- const agent = async (surface: unknown, scenario: Scenario, ctx: DispatchContext): Promise<string | null> => {
92
- const instr = String(surface)
113
+ const agent = async (candidate: ReadonlyAgentProfile, scenario: Scenario, ctx: DispatchContext): Promise<string | null> => {
114
+ const instr = candidate.prompt?.systemPrompt
115
+ if (instr === undefined) throw new Error('agent: candidate profile has no system prompt')
93
116
  const t = byId.get(scenario.id)
94
117
  if (!t) throw new Error(`agent: unknown scenario ${scenario.id}`)
95
118
  const prompt = `${instr}\n\n\`\`\`python\n${t.prompt}\`\`\``
96
119
  const t0 = Date.now()
97
- const r = await complete(base, key, workerModel, prompt, workerMaxTokens)
98
- const zeroUsage = r.tokIn === 0 && r.tokOut === 0
120
+ const paid = await ctx.cost.runPaidCall({
121
+ channel: 'agent',
122
+ actor: 'humaneval-worker',
123
+ model: workerModel,
124
+ execute: () => complete(base, key, workerModel, prompt, workerMaxTokens),
125
+ receipt: (result) => {
126
+ const usageUnknown = result.tokIn === 0 && result.tokOut === 0
127
+ return {
128
+ model: workerModel,
129
+ inputTokens: result.tokIn,
130
+ outputTokens: result.tokOut,
131
+ customTokenPricing: workerPricing,
132
+ ...(usageUnknown ? { usageUnknown: true } : {}),
133
+ }
134
+ },
135
+ })
136
+ if (!paid.succeeded) throw paid.error
137
+ const r = paid.value
99
138
  const hasText = r.text.trim().length > 0
100
- ctx.cost.observe(zeroUsage && hasText ? Math.max(r.usd, 0.0001) : r.usd, workerModel)
101
- ctx.cost.observeTokens(
102
- zeroUsage && hasText ? { input: Math.max(r.tokIn, 1), output: Math.max(r.tokOut, 1) } : { input: r.tokIn, output: r.tokOut },
103
- )
104
139
  stats.n += 1
105
140
  const codeLen = extractCode(r.text).length
106
141
  console.log(` [agent] ${scenario.id} instr=${instr.length}c code=${codeLen}b tok=in:${r.tokIn}/out:${r.tokOut} ${Math.round((Date.now() - t0) / 1000)}s`)
107
142
  return hasText ? r.text : null
108
143
  }
109
144
 
110
- const judge: JudgeConfig<string, Scenario> = {
145
+ const judge: JudgeConfig<string | null, Scenario> = {
111
146
  name: 'humaneval-docker',
112
147
  dimensions: [{ key: 'pass', description: 'the completed function passes its hidden unit tests (deterministic Docker checker)' }],
113
148
  async score({ artifact, scenario }) {
@@ -137,49 +172,59 @@ async function main(): Promise<void> {
137
172
  }
138
173
 
139
174
  const profile: AgentProfile = { name: 'hev-solver', prompt: { systemPrompt: SEED_INSTRUCTION } }
140
- const proposer = gepaProposer({
141
- llm: { baseUrl: reflectBase, apiKey: reflectKey },
142
- model: reflectModel,
143
- target:
144
- 'the instruction/system prompt strategy for a SMALL model completing Python functions to pass hidden unit tests. ' +
145
- 'Propose SUBSTANTIALLY different strategies, not wording tweaks: e.g. require the model to first reason step-by-step ' +
146
- 'about the algorithm and edge cases (empty inputs, off-by-one, boundary values, types) in a brief plan or comments ' +
147
- 'BEFORE writing the code; provide a short worked example; or add an explicit self-check against the docstring. ' +
148
- 'Bold rewrites that change model BEHAVIOR beat cosmetic edits.',
149
- maxTokens: reflectMaxTokens,
150
- temperature: 0.7,
151
- })
152
-
153
- const scenarios: Scenario[] = allIds.map((id) => ({ id, kind: 'humaneval' }))
154
- const holdoutScenarios: Scenario[] = holdout.map((t) => ({ id: t.taskId, kind: 'humaneval' }))
175
+ const scenario = (task: HumanEvalTask): Scenario => ({ id: task.taskId, kind: 'humaneval' })
155
176
 
156
- const out = await improve(profile, [], {
177
+ const out = await improve(profile, {
157
178
  surface: 'prompt',
158
- gate: 'holdout',
159
- generator: proposer,
160
- scenarios,
161
- judge,
179
+ executionRef: canonicalCandidateDigest({
180
+ callback: 'bench/hev-improve',
181
+ model: workerModel,
182
+ endpoint: new URL(base).origin,
183
+ maxTokens: workerMaxTokens,
184
+ checker: 'local-python',
185
+ }),
186
+ method: officialGepa<Scenario, string | null>({
187
+ objective:
188
+ 'Improve the complete instruction for a small model that writes Python functions which pass hidden unit tests.',
189
+ background:
190
+ 'Prefer behavioral strategies over wording changes. Address algorithm choice, edge cases, boundary values, type behavior, and self-checking. Return only the complete instruction.',
191
+ recipe: {
192
+ kind: 'engine',
193
+ run: {
194
+ engine: 'gepa',
195
+ maxEvaluations,
196
+ maxProposerCostUsd,
197
+ },
198
+ },
199
+ optimizer,
200
+ resume: 'if-compatible',
201
+ trustResumeState: true,
202
+ describeScenario: (item) => ({ prompt: byId.get(item.id)?.prompt ?? item.id }),
203
+ }),
204
+ trainScenarios: train.map(scenario),
205
+ selectionScenarios: selection.map(scenario),
206
+ testScenarios: testCases.map(scenario),
207
+ judges: [judge],
162
208
  agent,
163
209
  expectUsage: 'warn',
164
- // rawTraceContext stays OFF deliberately: it swaps the distilled findings for
165
- // filesystem paths + grep/cat instructions (rawTraceDistiller), which only a
166
- // coding harness can execute. This run's proposer is prompt-tier (gepaProposer
167
- // — a single LLM call that cannot run grep), so the trace evidence arrives via
168
- // the judge's traceback notes + the breakdown's `emitted` excerpt instead.
169
- budget: { generations, populationSize: population, holdoutScenarios, maxConcurrency, reps: 1 },
170
- llm: { baseUrl: reflectBase, apiKey: reflectKey, model: reflectModel },
210
+ maxConcurrency,
211
+ reps: 1,
212
+ runDir,
213
+ optimizationRunOptions: {
214
+ expectUsage: 'warn',
215
+ maxConcurrency,
216
+ reps: 1,
217
+ },
171
218
  })
172
219
 
173
- console.log('\n═══ RESULT ═══')
174
- console.log(`gateDecision=${out.gateDecision} shipped=${out.shipped} lift=${out.lift}`)
175
- console.log(`baseline holdout pass-rate = ${out.raw.baseline.compositeMean}`)
176
- console.log(`winner holdout pass-rate = ${out.raw.winner.compositeMean}`)
177
- console.log(`baseline per-scenario: ${JSON.stringify(out.raw.baseline.perScenario)}`)
178
- console.log(`winner per-scenario: ${JSON.stringify(out.raw.winner.perScenario)}`)
179
- if (out.raw.winner.label) console.log(`winner label : ${out.raw.winner.label}`)
180
- if ((out.raw.winner as { surface?: unknown }).surface) {
181
- console.log(`winner instruction:\n${String((out.raw.winner as { surface?: unknown }).surface).slice(0, 1200)}`)
182
- }
220
+ assertCompleteCost('humaneval official GEPA run', out.cost)
221
+ console.log('\n=== RESULT ===')
222
+ console.log(`decision=${out.decision} lift=${out.lift} interval=[${out.liftInterval.low}, ${out.liftInterval.high}]`)
223
+ console.log(`baseline test pass-rate=${out.raw.best.baselineComposite}`)
224
+ console.log(`winner test pass-rate=${out.raw.best.winnerComposite}`)
225
+ console.log(`test scenarios=${JSON.stringify(out.raw.best.scenarioScores)}`)
226
+ console.log(`cost=${JSON.stringify(out.cost)}`)
227
+ console.log(`winner instruction:\n${String(out.candidate.value).slice(0, 2000)}`)
183
228
  }
184
229
 
185
230
  main().catch((e) => {
@@ -0,0 +1,89 @@
1
+ function requiredNonNegativeNumber(
2
+ env: NodeJS.ProcessEnv,
3
+ name: string,
4
+ ): number {
5
+ const raw = env[name]
6
+ if (raw === undefined || raw.trim() === '') {
7
+ throw new Error(`env ${name} is required`)
8
+ }
9
+ const value = Number(raw)
10
+ if (!Number.isFinite(value) || value < 0) {
11
+ throw new Error(`env ${name} must be a finite non-negative number`)
12
+ }
13
+ return value
14
+ }
15
+
16
+ function positiveInteger(
17
+ env: NodeJS.ProcessEnv,
18
+ name: string,
19
+ fallback: number,
20
+ ): number {
21
+ const value = Number(env[name] ?? fallback)
22
+ if (!Number.isSafeInteger(value) || value <= 0) {
23
+ throw new Error(`env ${name} must be a positive integer`)
24
+ }
25
+ return value
26
+ }
27
+
28
+ export function requiredTokenPricing(
29
+ env: NodeJS.ProcessEnv,
30
+ prefix: string,
31
+ ) {
32
+ return {
33
+ inputUsdPerMillion: requiredNonNegativeNumber(
34
+ env,
35
+ `${prefix}_INPUT_USD_PER_MILLION`,
36
+ ),
37
+ cachedInputUsdPerMillion: requiredNonNegativeNumber(
38
+ env,
39
+ `${prefix}_CACHED_INPUT_USD_PER_MILLION`,
40
+ ),
41
+ cacheWriteUsdPerMillion: requiredNonNegativeNumber(
42
+ env,
43
+ `${prefix}_CACHE_WRITE_USD_PER_MILLION`,
44
+ ),
45
+ outputUsdPerMillion: requiredNonNegativeNumber(
46
+ env,
47
+ `${prefix}_OUTPUT_USD_PER_MILLION`,
48
+ ),
49
+ }
50
+ }
51
+
52
+ export function officialOptimizerModel(options: {
53
+ env: NodeJS.ProcessEnv
54
+ model: string
55
+ baseUrl: string
56
+ apiKey: string
57
+ maxCostUsd: number
58
+ maxOutputTokensPerRequest: number
59
+ envPrefix?: string
60
+ }) {
61
+ const { env } = options
62
+ const envPrefix = options.envPrefix ?? 'REFLECT'
63
+ return {
64
+ model: options.model,
65
+ baseUrl: options.baseUrl,
66
+ apiKey: options.apiKey,
67
+ budget: {
68
+ maxCostUsd: options.maxCostUsd,
69
+ maxRequests: positiveInteger(env, `${envPrefix}_MAX_REQUESTS`, 100),
70
+ maxRequestBytes: positiveInteger(env, `${envPrefix}_MAX_REQUEST_BYTES`, 2_000_000),
71
+ maxResponseBytes: positiveInteger(env, `${envPrefix}_MAX_RESPONSE_BYTES`, 2_000_000),
72
+ maxOutputTokensPerRequest: options.maxOutputTokensPerRequest,
73
+ requestTimeoutMs: positiveInteger(env, `${envPrefix}_REQUEST_TIMEOUT_MS`, 300_000),
74
+ pricing: requiredTokenPricing(env, envPrefix),
75
+ },
76
+ }
77
+ }
78
+
79
+ export function assertCompleteCost(
80
+ label: string,
81
+ cost: { accountingComplete: boolean; incompleteReasons: readonly string[] },
82
+ ): void {
83
+ if (cost.accountingComplete) return
84
+ const reasons =
85
+ cost.incompleteReasons.length > 0
86
+ ? cost.incompleteReasons.join('; ')
87
+ : 'no incomplete reason was recorded'
88
+ throw new Error(`${label}: cost accounting is incomplete: ${reasons}`)
89
+ }
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ assertCompleteCost,
4
+ officialOptimizerModel,
5
+ requiredTokenPricing,
6
+ } from './official-optimizer-config.mts'
7
+
8
+ const pricingEnv = {
9
+ OPT_INPUT_USD_PER_MILLION: '1',
10
+ OPT_CACHED_INPUT_USD_PER_MILLION: '0.1',
11
+ OPT_CACHE_WRITE_USD_PER_MILLION: '1.25',
12
+ OPT_OUTPUT_USD_PER_MILLION: '5',
13
+ }
14
+
15
+ describe('official optimizer configuration', () => {
16
+ it('builds a bounded model configuration from an arbitrary environment prefix', () => {
17
+ const model = officialOptimizerModel({
18
+ env: {
19
+ ...pricingEnv,
20
+ OPT_MAX_REQUESTS: '7',
21
+ OPT_MAX_REQUEST_BYTES: '1000',
22
+ OPT_MAX_RESPONSE_BYTES: '2000',
23
+ OPT_REQUEST_TIMEOUT_MS: '3000',
24
+ },
25
+ envPrefix: 'OPT',
26
+ model: 'test-model',
27
+ baseUrl: 'http://127.0.0.1:8080/v1',
28
+ apiKey: 'test-key',
29
+ maxCostUsd: 2,
30
+ maxOutputTokensPerRequest: 4000,
31
+ })
32
+
33
+ expect(model).toEqual({
34
+ model: 'test-model',
35
+ baseUrl: 'http://127.0.0.1:8080/v1',
36
+ apiKey: 'test-key',
37
+ budget: {
38
+ maxCostUsd: 2,
39
+ maxRequests: 7,
40
+ maxRequestBytes: 1000,
41
+ maxResponseBytes: 2000,
42
+ maxOutputTokensPerRequest: 4000,
43
+ requestTimeoutMs: 3000,
44
+ pricing: {
45
+ inputUsdPerMillion: 1,
46
+ cachedInputUsdPerMillion: 0.1,
47
+ cacheWriteUsdPerMillion: 1.25,
48
+ outputUsdPerMillion: 5,
49
+ },
50
+ },
51
+ })
52
+ })
53
+
54
+ it('requires every token price instead of inventing cost data', () => {
55
+ expect(() =>
56
+ requiredTokenPricing(
57
+ {
58
+ ...pricingEnv,
59
+ OPT_OUTPUT_USD_PER_MILLION: undefined,
60
+ },
61
+ 'OPT',
62
+ ),
63
+ ).toThrow('env OPT_OUTPUT_USD_PER_MILLION is required')
64
+ })
65
+
66
+ it('rejects invalid request limits before constructing the optimizer', () => {
67
+ expect(() =>
68
+ officialOptimizerModel({
69
+ env: { ...pricingEnv, OPT_MAX_REQUESTS: '0' },
70
+ envPrefix: 'OPT',
71
+ model: 'test-model',
72
+ baseUrl: 'http://127.0.0.1:8080/v1',
73
+ apiKey: 'test-key',
74
+ maxCostUsd: 2,
75
+ maxOutputTokensPerRequest: 4000,
76
+ }),
77
+ ).toThrow('env OPT_MAX_REQUESTS must be a positive integer')
78
+ })
79
+
80
+ it('rejects incomplete cost records', () => {
81
+ expect(() =>
82
+ assertCompleteCost('official optimizer', {
83
+ accountingComplete: false,
84
+ incompleteReasons: ['provider omitted usage'],
85
+ }),
86
+ ).toThrow('official optimizer: cost accounting is incomplete: provider omitted usage')
87
+ })
88
+ })
package/src/profiles.ts CHANGED
@@ -5,8 +5,8 @@
5
5
  * "operator type" or "analyst type" — there are profiles + the operator toolbox they use to manage
6
6
  * each other (in-process via the Scope, in a sandbox via the same verbs exposed as MCP tools).
7
7
  *
8
- * The directory is OPTIMIZED over time by the agent-eval RSI loop (`runImprovementLoop`): `gepaProposer`
9
- * evolves the prompts, a `skillOptProposer` evolves `skills`, Pareto-tracked + holdout-gated.
8
+ * Complete agent-eval methods optimize these profiles over explicit train,
9
+ * selection, and final-test partitions.
10
10
  * The trace-analyst's findings are the optimizer's input — the loop improves the profile that the
11
11
  * findings say is weak.
12
12
  */
@@ -0,0 +1,144 @@
1
+ # QUANT-ARENA — a self-improving trading-strategy lab
2
+
3
+ A small, fully auditable research loop: an AI strategy author writes candidate strategies, every candidate is screened for look-ahead bias, backtested walk-forward on overlapping in-sample windows against pinned benchmarks, and judged by an acceptance rule whose bar **rises with every strategy tried**.
4
+ Every attempt — accepted, rejected, or killed for leaking — becomes a permanent row in a lab notebook.
5
+ The final two years of data are a locked out-of-sample set that only a separate certification command may touch, once.
6
+
7
+ Everything is plain TypeScript you can read in an afternoon: the backtester is one file with zero dependencies, the acceptance math is one file, the data is committed CSV.
8
+
9
+ ## 1. Quickstart
10
+
11
+ ```bash
12
+ # from bench/ (needs node >= 20, the `claude` CLI logged in, and `uv` on PATH
13
+ # — scoring runs in a pinned python environment, see "Two engines" below)
14
+ npx tsx src/quant-arena/quant-loop.mts --out /tmp/quant-demo --candidates 2
15
+ ```
16
+
17
+ That runs a full research campaign: 2 strategy authors x 2 candidates each — the committed capture of exactly this command cost $1.65 of model spend across 8 metered calls; backtests are free.
18
+ No API? `--skip-llm-audit` keeps everything but the adversarial code review; the mechanical look-ahead check still runs.
19
+
20
+ Run the unit tests (backtester hand-computed cases, look-ahead detection, acceptance math, window reproducibility):
21
+
22
+ ```bash
23
+ npx vitest run src/quant-arena
24
+ ```
25
+
26
+ ## 2. What happens when you run it
27
+
28
+ 1. **Data loads.** ~8 years of daily bars for 11 tickers (an index `IDX` plus `S01`-`S10`) from `fixtures/data/insample/`. The final 2 years live in `fixtures/data/holdout/` and are **not** loaded — see step 8. The series are synthetic (regime-switching factor model, seeded, regenerable) because the free real-data source we checked licenses personal use only; `fixtures/data/PROVENANCE.md` has the details and how to drop in your own CSVs.
29
+ 2. **Evaluation windows are drawn.** 8 overlapping 504-day (~2-year) blocks, block-bootstrap sampled from the in-sample years with a fixed seed — every candidate in the campaign is scored on the same windows, and reruns reproduce bit-identically.
30
+ 3. **Benchmarks run.** Three pinned incumbents: buy-and-hold the index, equal-weight monthly rebalance, and a 20/100 moving-average crossover. Their per-window Sharpe ratios define the bar: "best benchmark" is the per-window maximum.
31
+ 4. **Strategy authors write code.** Each author is a Claude call with a pinned identity (one plain, one with a quant-researcher system prompt). It gets the strategy contract, the universe summary, the benchmarks' per-window Sharpes, and the current acceptance bar — and must reply with one self-contained TypeScript module exporting `onBar(ctx)`: the harness calls it once per trading day with the bars **up to that day only** (the arrays are physically sliced, so reading the future is structurally impossible), plus the strategy's current holdings and equity, and it answers with target portfolio weights or "hold". Strategies never place orders — a single shared rebalancer (`oms.ts`) turns everyone's target weights into orders under the same sizing rule, LEAN-style `(targetWeight x equity - currentPosition) / price`, long-only. Model spend is metered into a durable cost log (`cost-ledger.jsonl`) with per-call receipts.
32
+ 5. **Look-ahead screening, stage 1 (mechanical).** The candidate is re-run on data truncated at several cutoff days. Signals up to each cutoff must be bit-identical to the full-data run — any divergence proves the code read the future, and the candidate is killed with the divergence quoted.
33
+ 6. **Look-ahead screening, stage 2 (adversarial).** A second, cheap model reads the source with one job: find look-ahead — indexing past `t`, whole-series statistics feeding per-day decisions, hardcoded dates that smell like memorization. Verdict is JSON; anything but a clean verdict kills the candidate, and an unparseable reply kills it too (the rule fails closed).
34
+ 7. **Backtest and verdict.** Survivors are backtested over the whole in-sample period (next-day-open fills, 15 bps one-way costs, no shorting, no leverage) and scored per window. Two engines run: the one-file TypeScript reference engine first, as a fail-closed contract check, then the industry-standard **vectorbt** engine (a persistent python worker in a version-locked environment) produces the official numbers. A parity test suite holds the two engines to agreement on golden fixtures — exact on a no-trade book, within machine precision whenever the book holds cash, and within a documented 0.5% on fully-invested books (the engines differ only in whether fees may be financed by a slightly negative cash balance). The acceptance rule (section 3) decides. Accepted or not, the try is appended to `notebook.jsonl` with its window scores, audit evidence, code hash, and authoring cost.
35
+ 8. **Certification, later and by hand.** When you believe a winner, run it once against the untouched final 2 years:
36
+
37
+ ```bash
38
+ npx tsx src/quant-arena/holdout-certify.mts --strategy <path>/strategy.ts --out /tmp/quant-demo
39
+ ```
40
+
41
+ It backtests in-sample + out-of-sample on one axis (so lookbacks are warm), scores only the out-of-sample days against the same three benchmarks, and appends the in-sample vs out-of-sample comparison to the notebook. A second run for the same strategy hash refuses without `--force` — an out-of-sample set answers once; re-rolling it until it agrees turns it into another in-sample set.
42
+
43
+ ## 3. Why the acceptance rule is strict
44
+
45
+ If you test enough random strategies against the same data, the best one looks brilliant by luck alone.
46
+ Under the assumption of zero skill, the expected best Sharpe among N independent tries grows roughly like sqrt(2 ln N) — try 50 strategies and luck alone buys the winner a substantial edge.
47
+ So the bar a candidate must clear is not fixed: it is `0.10 + 0.15 * sqrt(2 ln N)` of mean excess Sharpe, where N counts **every** candidate ever tried in the campaign, including ones killed for leaking (this is a simplified, auditable version of the Deflated Sharpe Ratio of Bailey & Lopez de Prado, Journal of Portfolio Management, 2014).
48
+ A candidate must ALSO beat the best benchmark in at least 6 of the 8 windows, because one lucky two-year stretch should never carry a decision.
49
+ Every try is a permanent notebook row, so N can never be quietly reset — the price of another shot at the data is a higher bar for everyone after it.
50
+
51
+ ## 4. Reading the notebook
52
+
53
+ `notebook.jsonl` is append-only JSON lines. Three row types:
54
+
55
+ - `quant-arena.baselines.v1` — the campaign header: seed, cost assumptions, the 8 windows with dates, and each benchmark's per-window Sharpe.
56
+ - `quant-arena.candidate.v1` — one per try. The fields that matter:
57
+ - `nTried` — this try's position in the campaign; sets its acceptance bar.
58
+ - `leakAudit.truncation` / `leakAudit.llm` — both screening verdicts with evidence.
59
+ - `eval.perWindow` — Sharpe vs best-benchmark Sharpe for each window, with dates.
60
+ - `verdict` — `accepted`, `rejected-no-edge` (failed the acceptance rule), `rejected-leak`, `rejected-contract` (didn't satisfy the module contract), or `rejected-error`.
61
+ - `reasons` — the decision spelled out, numbers included.
62
+ - `quant-arena.certification.v1` — the one-shot out-of-sample result, in-sample stats side by side.
63
+
64
+ A real excerpt from the committed demo campaign (`fixtures/demo-campaign/`): try #4 cleared both look-ahead screens, then lost to the benchmarks in 7 of 8 windows — and after four tries the bar it would have needed had already risen to 0.35 (condensed for width):
65
+
66
+ ```
67
+ {
68
+ "candidateId": "cand-004-quant-researcher",
69
+ "proposer": "quant-researcher",
70
+ "nTried": 4,
71
+ "leakAudit": {
72
+ "truncation": {
73
+ "clean": true
74
+ },
75
+ "llm": {
76
+ "verdict": "clean"
77
+ }
78
+ },
79
+ "eval": {
80
+ "wins": 1,
81
+ "requiredWins": 6,
82
+ "meanExcessSharpe": -0.085,
83
+ "threshold": 0.35,
84
+ "perWindow": [
85
+ {
86
+ "startDate": "2017-04-06",
87
+ "endDate": "2019-03-12",
88
+ "sharpe": 0.94,
89
+ "bestBaselineSharpe": 1.21,
90
+ "excess": -0.27
91
+ },
92
+ {
93
+ "startDate": "2018-04-18",
94
+ "endDate": "2020-03-23",
95
+ "sharpe": 0.56,
96
+ "bestBaselineSharpe": 0.61,
97
+ "excess": -0.05
98
+ },
99
+ {
100
+ "startDate": "2018-08-06",
101
+ "endDate": "2020-07-09",
102
+ "sharpe": 0.2,
103
+ "bestBaselineSharpe": 0.29,
104
+ "excess": -0.08
105
+ },
106
+ "... 5 more windows"
107
+ ]
108
+ },
109
+ "verdict": "rejected-no-edge",
110
+ "reasons": [
111
+ "consistency: beat the best baseline in only 1/8 windows (need 6)",
112
+ "multiplicity: mean excess Sharpe -0.085 < required 0.350 (bar after 4 tried candidates)"
113
+ ]
114
+ }
115
+ ```
116
+
117
+ ## 5. Plugging in your own backtester and data
118
+
119
+ Scoring goes through one narrow seam: a worker process that takes `{open prices, close prices, target-weight rows, costs, windows}` as JSON lines on stdin and answers `{per-window total return / max drawdown / Sharpe / trade count, full equity curve}` on stdout — see the protocol comment at the top of `python/vbt-worker.py` and the client in `vbt-client.ts`.
120
+ The shipped worker is vectorbt (`Portfolio.from_orders`, target-percent sizing, shared cash, sells before buys), version-locked by `python/pyproject.toml` + `python/uv.lock`; to swap in your own engine, speak the same protocol and keep the fill model (decide at close, fill at next open, bps fees on traded dollars) or re-derive the parity fixtures in `vbt-parity.test.mts` for your model.
121
+ Point the data loader at your own Stooq-format CSVs (one file per ticker, `IDX.csv` as the benchmark asset, an `insample/` and a `holdout/` directory).
122
+ Keep the physical in-sample/out-of-sample split and the once-only certification rule — they are the point, not an implementation detail.
123
+ A third engine is planned but not built: event-driven certification of a winner's order stream through Nautilus Trader (`nautilus-certify.ts` is the named stub).
124
+
125
+ ## Files
126
+
127
+ | file | what it is |
128
+ | --- | --- |
129
+ | `types.ts` | the strategy contract (v2 `onBar` + the order types), including the no-look-ahead rule |
130
+ | `driver.ts` | the incremental harness: feeds `onBar` day by day with physically truncated history; wraps old batch strategies unchanged |
131
+ | `oms.ts` | the one shared rebalancer: target weights -> orders (strategies never place orders) |
132
+ | `backtest.ts` | the TypeScript reference engine: next-open fills, bps costs, no shorting — zero dependencies; contract prefilter |
133
+ | `vbt-client.ts` + `python/vbt-worker.py` | the official scorer: persistent vectorbt worker, pinned env (`python/uv.lock`), crash-safe request handling |
134
+ | `vbt-parity.test.mts` | the two engines held to agreement on golden fixtures (prints both curves on any disagreement) |
135
+ | `nautilus-certify.ts` | named stub for the planned event-driven certification engine (not implemented) |
136
+ | `windows.ts` | seeded block-bootstrap evaluation windows |
137
+ | `multiplicity.ts` | the rising acceptance bar (documented formula + citation) |
138
+ | `leak-audit.ts` | the mechanical truncation-invariance check |
139
+ | `quant-loop.mts` | the campaign: author -> screen -> backtest -> verdict -> notebook |
140
+ | `holdout-certify.mts` | the once-only out-of-sample certification |
141
+ | `strategies/` | the three pinned benchmarks |
142
+ | `fixtures/data/` | committed daily bars + provenance; `holdout/` is the locked final 2 years |
143
+ | `fixtures/demo-campaign/` | a real captured campaign against the v1 batch contract: notebook, authored strategies, cost receipts |
144
+ | `fixtures/demo-campaign-v2/` | a real captured campaign against the v2 `onBar` contract (1 candidate, honestly rejected: 0/8 windows) |