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