@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.
- package/CHANGELOG.md +11 -0
- package/HARNESS.md +43 -0
- package/README.md +7 -0
- package/dist/adapters.js +23 -23
- package/dist/benchmarks/_harness.d.ts +1 -1
- package/dist/benchmarks/_harness.js +1 -1
- package/dist/benchmarks/aec-bench.js +2 -2
- package/dist/benchmarks/agentbench.js +2 -2
- package/dist/benchmarks/appworld.js +2 -2
- package/dist/benchmarks/bfcl.js +2 -2
- package/dist/benchmarks/commit0.js +2 -2
- package/dist/benchmarks/crag.js +2 -2
- package/dist/benchmarks/dabstep.js +2 -2
- package/dist/benchmarks/enterpriseops-gym.js +2 -2
- package/dist/benchmarks/finresearchbench.js +2 -2
- package/dist/benchmarks/nomiracl.js +2 -2
- package/dist/benchmarks/open-rag-bench.js +2 -2
- package/dist/benchmarks/programbench.js +2 -2
- package/dist/benchmarks/ragbench.js +2 -2
- package/dist/benchmarks/swe-bench.js +2 -2
- package/dist/benchmarks/t2-ragbench.js +2 -2
- package/dist/benchmarks/tau-bench-shared.js +2 -2
- package/dist/benchmarks/tau2-bench.js +3 -3
- package/dist/benchmarks/tau3-banking.js +3 -3
- package/dist/benchmarks/terminal-bench.js +2 -2
- package/dist/benchmarks/toollm.js +2 -2
- package/dist/benchmarks/webarena-verified.js +2 -2
- package/dist/{chunk-CKUVRZ2T.js → chunk-3U5TXJZS.js} +2 -2
- package/dist/{chunk-YCGY7UIZ.js → chunk-7GRVHU22.js} +2 -2
- package/dist/{chunk-Z7ML6L77.js → chunk-HWST3SED.js} +2 -2
- package/dist/{chunk-SYDW647C.js → chunk-IA2FBTWC.js} +2 -2
- package/dist/{chunk-R67DFVLO.js → chunk-IFVINJ4B.js} +2 -2
- package/dist/{chunk-R36V2VP7.js → chunk-IZ5M6OAC.js} +2 -2
- package/dist/{chunk-ODT47UAY.js → chunk-K3BQGZCT.js} +2 -2
- package/dist/{chunk-IFAV6KEM.js → chunk-KP5KD6EN.js} +2 -2
- package/dist/{chunk-ZEWMTR5M.js → chunk-MQMRLGOG.js} +2 -2
- package/dist/{chunk-TSWPNOYM.js → chunk-NQG5XDSB.js} +2 -2
- package/dist/{chunk-7WSD27QQ.js → chunk-PB64GYIG.js} +2 -2
- package/dist/{chunk-HBSWHQNJ.js → chunk-RCYQEFNX.js} +3 -3
- package/dist/{chunk-J3KDJNX2.js → chunk-RH5F53JT.js} +2 -2
- package/dist/{chunk-UAIOHCUK.js → chunk-SFLA7OH3.js} +3 -3
- package/dist/{chunk-Y6O2OCUO.js → chunk-SHM6MRRF.js} +2 -2
- package/dist/{chunk-KDIKRJGB.js → chunk-SHYIRB7I.js} +2 -2
- package/dist/{chunk-HHXFIHXC.js → chunk-SVR2LKYI.js} +2 -2
- package/dist/{chunk-PWQVGAJB.js → chunk-V7AEBY6U.js} +21 -21
- package/dist/{chunk-LRRD7NAG.js → chunk-WSKWVEQB.js} +18 -2
- package/dist/chunk-WSKWVEQB.js.map +1 -0
- package/dist/{chunk-2PVVP7GN.js → chunk-XKEFIFIC.js} +2 -2
- package/dist/{chunk-JRWWGMK7.js → chunk-XYA4XSNU.js} +2 -2
- package/dist/{chunk-X5YKXC6V.js → chunk-YSMEKBTD.js} +2 -2
- package/dist/{chunk-2XU6OGEN.js → chunk-Z4TZ76N7.js} +2 -2
- package/dist/index.js +23 -23
- package/package.json +5 -5
- package/scripts/trata-hedge/README.md +6 -5
- package/scripts/verify-packed-consumer.mjs +1 -1
- package/src/benchmarks/_harness.ts +20 -2
- package/src/benchmarks/humaneval.test.mts +2 -2
- package/src/gate.ts +1 -1
- package/src/hev-eval.mts +5 -2
- package/src/hev-improve.mts +118 -73
- package/src/official-optimizer-config.mts +89 -0
- package/src/official-optimizer-config.test.mts +88 -0
- package/src/profiles.ts +2 -2
- package/src/quant-arena/README.md +144 -0
- package/src/quant-arena/backtest.test.mts +135 -0
- package/src/quant-arena/backtest.ts +218 -0
- package/src/quant-arena/data.test.mts +44 -0
- package/src/quant-arena/data.ts +141 -0
- package/src/quant-arena/driver.test.mts +253 -0
- package/src/quant-arena/driver.ts +219 -0
- package/src/quant-arena/fixtures/data/PROVENANCE.md +26 -0
- package/src/quant-arena/fixtures/data/holdout/IDX.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S01.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S02.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S03.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S04.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S05.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S06.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S07.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S08.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S09.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S10.csv +523 -0
- package/src/quant-arena/fixtures/data/insample/IDX.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S01.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S02.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S03.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S04.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S05.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S06.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S07.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S08.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S09.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S10.csv +2087 -0
- package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +16 -0
- package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +5 -0
- package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +171 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +119 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +119 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +105 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +102 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +4 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +2 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +84 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +117 -0
- package/src/quant-arena/holdout-certify.mts +206 -0
- package/src/quant-arena/holdout-certify.test.mts +82 -0
- package/src/quant-arena/leak-audit.test.mts +79 -0
- package/src/quant-arena/leak-audit.ts +95 -0
- package/src/quant-arena/make-fixtures.mts +161 -0
- package/src/quant-arena/multiplicity.test.mts +68 -0
- package/src/quant-arena/multiplicity.ts +87 -0
- package/src/quant-arena/nautilus-certify.ts +31 -0
- package/src/quant-arena/oms.ts +90 -0
- package/src/quant-arena/profiles/quant-researcher.profile.json +7 -0
- package/src/quant-arena/python/pyproject.toml +8 -0
- package/src/quant-arena/python/uv.lock +1297 -0
- package/src/quant-arena/python/vbt-worker.py +192 -0
- package/src/quant-arena/quant-loop.mts +813 -0
- package/src/quant-arena/quant-loop.test.mts +75 -0
- package/src/quant-arena/strategies/buy-hold-index/strategy.ts +11 -0
- package/src/quant-arena/strategies/equal-weight/strategy.ts +20 -0
- package/src/quant-arena/strategies/sma-crossover/strategy.ts +42 -0
- package/src/quant-arena/types.ts +133 -0
- package/src/quant-arena/vbt-client.ts +321 -0
- package/src/quant-arena/vbt-parity.test.mts +183 -0
- package/src/quant-arena/windows.test.mts +45 -0
- package/src/quant-arena/windows.ts +54 -0
- package/src/rollout-ledger/backfill-swe-arena.mts +606 -0
- package/src/rollout-ledger/backfill-swe-arena.test.mts +342 -0
- package/src/rollout-ledger/settle-capture.mts +442 -0
- package/src/rollout-ledger/settle-capture.test.mts +270 -0
- package/src/smoke-structural-rollout.mts +15 -9
- package/src/swe-arena/activation.mts +225 -0
- package/src/swe-arena/activation.test.mts +300 -0
- package/src/swe-arena/arms.ts +30 -14
- package/src/swe-arena/briefing.mts +217 -0
- package/src/swe-arena/briefing.test.mts +178 -0
- package/src/swe-arena/calibrate.ts +102 -1
- package/src/swe-arena/cell-evidence.mts +41 -9
- package/src/swe-arena/factory-command-container.ts +284 -0
- package/src/swe-arena/factory-judge-child.mts +228 -0
- package/src/swe-arena/factory.test.mts +643 -0
- package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +51 -0
- package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +29 -0
- package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +64 -0
- package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +48 -0
- package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +29 -0
- package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +48 -0
- package/src/swe-arena/fixtures/factory/loops-28/calibration.md +47 -0
- package/src/swe-arena/fixtures/factory/loops-28/manifest.json +30 -0
- package/src/swe-arena/fixtures/factory/loops-28/spec.md +50 -0
- package/src/swe-arena/fixtures/run-report/README.md +43 -0
- package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +173 -0
- package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +100 -0
- package/src/swe-arena/fixtures/run-report/gen3-rollup.json +551 -0
- package/src/swe-arena/fixtures/run-report/gen3-rollup.md +64 -0
- package/src/swe-arena/fixtures.ts +135 -2
- package/src/swe-arena/gepa-seat.mts +877 -0
- package/src/swe-arena/gepa-seat.test.mts +1059 -0
- package/src/swe-arena/implementation-ref.test.mts +64 -0
- package/src/swe-arena/implementation-ref.ts +62 -0
- package/src/swe-arena/ledger-orphans.mts +77 -0
- package/src/swe-arena/ledger-orphans.test.mts +147 -0
- package/src/swe-arena/outer-loop.mts +710 -33
- package/src/swe-arena/premeasured-from-cells.mts +281 -0
- package/src/swe-arena/premeasured-from-cells.test.mts +180 -0
- package/src/swe-arena/proposer-fanout.mts +327 -31
- package/src/swe-arena/proposer-fanout.test.mts +246 -0
- package/src/swe-arena/proposer-provenance.mts +172 -0
- package/src/swe-arena/proposer-provenance.test.mts +106 -0
- package/src/swe-arena/run-experiment.mts +367 -1
- package/src/swe-arena/run-report.mts +75 -0
- package/src/swe-arena/run-supervisor.test.mts +5 -3
- package/src/swe-arena/score-split.mts +140 -0
- package/src/swe-arena/score-split.test.mts +123 -0
- package/src/swe-arena/scratch-worktree.test.mts +55 -0
- package/src/swe-arena/scratch-worktree.ts +34 -0
- package/src/swe-arena/types.ts +52 -0
- package/src/swe-code-improve.mts +24 -25
- package/src/swe-improve.mts +129 -96
- package/src/swe-local-proof.mts +6 -1
- package/src/swe-stream.mts +4 -2
- package/src/swe-structural.mts +245 -837
- package/src/tb-container-executor.test.mts +30 -6
- package/src/tb-supervisor-sidecar.mts +2 -1
- package/src/trata-gepa.mts +182 -245
- package/dist/chunk-LRRD7NAG.js.map +0 -1
- package/src/live-improve-campaign-mbpp.mts +0 -641
- package/src/live-improve-campaign.mts +0 -500
- /package/dist/{chunk-CKUVRZ2T.js.map → chunk-3U5TXJZS.js.map} +0 -0
- /package/dist/{chunk-YCGY7UIZ.js.map → chunk-7GRVHU22.js.map} +0 -0
- /package/dist/{chunk-Z7ML6L77.js.map → chunk-HWST3SED.js.map} +0 -0
- /package/dist/{chunk-SYDW647C.js.map → chunk-IA2FBTWC.js.map} +0 -0
- /package/dist/{chunk-R67DFVLO.js.map → chunk-IFVINJ4B.js.map} +0 -0
- /package/dist/{chunk-R36V2VP7.js.map → chunk-IZ5M6OAC.js.map} +0 -0
- /package/dist/{chunk-ODT47UAY.js.map → chunk-K3BQGZCT.js.map} +0 -0
- /package/dist/{chunk-IFAV6KEM.js.map → chunk-KP5KD6EN.js.map} +0 -0
- /package/dist/{chunk-ZEWMTR5M.js.map → chunk-MQMRLGOG.js.map} +0 -0
- /package/dist/{chunk-TSWPNOYM.js.map → chunk-NQG5XDSB.js.map} +0 -0
- /package/dist/{chunk-7WSD27QQ.js.map → chunk-PB64GYIG.js.map} +0 -0
- /package/dist/{chunk-HBSWHQNJ.js.map → chunk-RCYQEFNX.js.map} +0 -0
- /package/dist/{chunk-J3KDJNX2.js.map → chunk-RH5F53JT.js.map} +0 -0
- /package/dist/{chunk-UAIOHCUK.js.map → chunk-SFLA7OH3.js.map} +0 -0
- /package/dist/{chunk-Y6O2OCUO.js.map → chunk-SHM6MRRF.js.map} +0 -0
- /package/dist/{chunk-KDIKRJGB.js.map → chunk-SHYIRB7I.js.map} +0 -0
- /package/dist/{chunk-HHXFIHXC.js.map → chunk-SVR2LKYI.js.map} +0 -0
- /package/dist/{chunk-PWQVGAJB.js.map → chunk-V7AEBY6U.js.map} +0 -0
- /package/dist/{chunk-2PVVP7GN.js.map → chunk-XKEFIFIC.js.map} +0 -0
- /package/dist/{chunk-JRWWGMK7.js.map → chunk-XYA4XSNU.js.map} +0 -0
- /package/dist/{chunk-X5YKXC6V.js.map → chunk-YSMEKBTD.js.map} +0 -0
- /package/dist/{chunk-2XU6OGEN.js.map → chunk-Z4TZ76N7.js.map} +0 -0
package/src/hev-improve.mts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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 {
|
|
17
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
//
|
|
96
|
+
// All three partitions are disjoint slices of the harder middle band.
|
|
78
97
|
const train = await loadHumanEval(trainN, offset)
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
const
|
|
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('
|
|
84
|
-
console.log(`worker=${workerModel}
|
|
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(`
|
|
87
|
-
console.log(`
|
|
88
|
-
console.log(
|
|
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 (
|
|
92
|
-
const instr =
|
|
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
|
|
98
|
-
|
|
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
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
210
|
+
maxConcurrency,
|
|
211
|
+
reps: 1,
|
|
212
|
+
runDir,
|
|
213
|
+
optimizationRunOptions: {
|
|
214
|
+
expectUsage: 'warn',
|
|
215
|
+
maxConcurrency,
|
|
216
|
+
reps: 1,
|
|
217
|
+
},
|
|
171
218
|
})
|
|
172
219
|
|
|
173
|
-
|
|
174
|
-
console.log(
|
|
175
|
-
console.log(`
|
|
176
|
-
console.log(`
|
|
177
|
-
console.log(`
|
|
178
|
-
console.log(`
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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) |
|