-
Inspect
git status --short --branch. In a human-started run, confirm a unique run tag and createarena/<tag>frommaster; never reuse a tag. In an arena worktree, the driver has already created the branch: do not switch it. -
Run
uv syncanduv run python setup_data.py --verify. It must printDATA OK. -
Read
strategy.pyandevaluate.pyfully. Readdata/NOISE_FLOOR.jsonanddata/BASELINE.json. Iflineage.jsonis absent it is seeded from the baseline on first score. -
Run
uv run python run.py. It prints the newruns/<n>directory only after trusted code has atomically writtenscore.json. Confirm the score matchesdata/BASELINE.jsonto1e-8. -
Create
results.tsvif absent, using this TAB-separated header exactly:commit score mean_sharpe std_sharpe max_dd turnover status description
The setup baseline is not an experiment: never append it to results.tsv. Experiment 1 starts
with the first new hypothesis and pre-run commit.
You CAN:
- Edit anything in
strategy.py: features, signal logic, hyperparameters, and helper functions. - Read and plot TRAIN-period data to form hypotheses.
- Read
evaluate.pyto understand exactly how the evaluator scores you. - Read
lineage.json,recursion_metrics.json, andruns/<n>/score.jsonincluding the structuredcomparisonanddecisionblocks.
You CANNOT:
- Modify
evaluate.py,run.py,setup_data.py,program.md,lineage.jsonby hand, or anything indata/. Protected changes fail their hashes or the arena audit and disqualify the run.lineage.jsonis advanced only by the evaluator after a trial. - Install packages or import outside numpy, pandas, scipy, numba, math, itertools, and dataclasses.
- Read, print, serialize, or otherwise inspect raw VALID rows. Strategy code receives a frame only so it can compute signals. Treat that capability as evaluator-owned, not an invitation to exfiltrate observations.
- Look at LOCKBOX. It is unavailable without the arena's one-shot token.
Your goal is to maximize the official champion score in score.json / lineage.json:
score = mean(fold_sharpes) - 0.5 * std(fold_sharpes)
Returns are net of immutable fees, spread, square-root impact, and historical funding. Hard risk and causality gates apply before acceptance.
Simplicity criterion. A +0.01 improvement that adds 40 lines of special-case logic is probably not worth it. A +0.01 improvement from deleting code is worth keeping.
Search progress and official validation are intentionally separate.
candidate
↓
search acceptance → lineage archive → next mutations
↓
official promotion test → confirmed champion
↓ once (arena)
sealed multi-regime final evaluation (lockbox)
A candidate may become the new working parent when the evaluator reports
decision.search_accepted: true. The default policy is:
- gates PASS
scoreimproves on the working parent by more thanmateriality_margin(0.02 — an explicit margin, not “noise”)- at least 4 of 6 folds improve versus the working parent
- 2× and 3× cost Sharpes are not materially worse (
cost_deltas/ cost policy in evaluate)
Search accepts are logged with status search. They advance recursion even when the move is
too small for official promotion.
A candidate becomes the official champion when decision.official_promoted: true
(status keep). The official bar stays strong for backward compatibility:
- score must clear the champion by the official margin (at least the calibrated
noise_floor, and never below materiality adjusted for prior promotion attempts) - at least 5 of 6 folds improve versus the official champion
- cost sensitivity is not materially worse
Only keep rows are confirmed promotions. The dashboard draws official progress as a solid
line and working-parent progress as a dashed line.
lineage.json keeps four archive slots so a rejected official candidate can still remain a
useful parent on another dimension:
| Slot | Preference |
|---|---|
best_overall |
highest score |
most_stable |
lowest fold Sharpe dispersion among competitive scores |
lowest_cost_sensitivity |
best worst-of 2×/3× cost Sharpes |
most_distinct |
lowest return correlation with other archived lineages |
Status archive means the commit was retained for at least one slot without becoming the
working parent. You may switch the mutation base by restoring an archive commit’s
strategy.py (see loop below). Unconditional git reset --hard HEAD~1 is not the default
when a candidate claims an archive slot or search accept.
After every run, read runs/<n>/score.json fields:
comparison.score_delta— candidate versus working parentcomparison.fold_deltas— six fold Sharpe deltascomparison.cost_deltas— 1× / 2× / 3× Sharpe deltascomparison.drawdown_delta,comparison.turnover_deltacomparison.return_correlation— versus parent when returns existcomparison.archive_correlations— versus each archive slotcomparison.gate_headroom— distance to hard gatesdecision.status/decision.reasons/decision.archive_claimsdecision.reset_to— full commit hash to restore on discard/tie
That feedback is the recursive signal. Do not invent a private score from VALID rows.
- Official promotion uses the calibrated
noise_flooras a statistical bar, not a search step size. Search usesmateriality_margininstead. - If a change helps exactly one fold and hurts elsewhere, discard it.
- Prefer fewer parameters when score and fold agreement are comparable.
- State an economic mechanism. Update
HYPOTHESISwhenever the signal changes. Say who is on the other side, why they keep trading, and what would make the effect decay. - Never tune toward a particular fold. You will ultimately be checked on data you cannot open.
- When stuck in parameter sweeps, switch to another archive lineage (
most_distinctormost_stable) or a structurally different mechanism.
After every run, read only the runs/<n>/score.json path printed by run.py. Do not parse
stdout for metrics. Do not read run.log unless the run crashed. On a crash, inspect only
tail -n 50 runs/<n>/run.log; evaluator sanitization strips instruction-like text first.
Append one TAB-separated row to results.tsv after every attempt:
commit score mean_sharpe std_sharpe max_dd turnover status description
status is one of:
| status | meaning |
|---|---|
keep |
official champion promotion |
search |
working-parent search accept |
archive |
retained in lineage archive only |
discard |
rejected; restore working parent |
tie |
positive but inside official bar / failed search gates |
crash |
no score.json |
gated |
hard gate failure |
tampered |
protection failure |
The arena may add dq to its public state. Use 0.000000 for crashes and -999.000000 for
gate/tamper failures. Log every experiment. The complete trace is the benchmark.
Copy status from score.json → decision.status whenever that field exists. Do not second-guess
the evaluator.
-
Read
git log --oneline -20, the tail ofresults.tsv, andlineage.json. Know the official champion, working parent, and archive tips. -
Form one hypothesis. Prefer mutating the working parent; periodically branch from a non-best archive slot when diversity collapses (high archive correlations).
-
Edit only
strategy.py, includingHYPOTHESIS. -
Commit before running:
git add strategy.py && git commit -m "<one-line description>". -
Run with a five-minute timeout:
uv run python run.py. Record the printed run directory. -
Read that directory's
score.json; never grep a metric from logs. -
No
score.jsonmeans crash. Read only the sanitized last 50 log lines. Fix trivial syntax or import errors, but abandon a deeper failure after two tries. -
Append one result row using
decision.status. Record the commit withgit rev-parse HEAD(full hash). -
Act on
decision.action:keep_commit(keep/search/archive): leave the commit on the branch.lineage.jsonis already updated by the evaluator.reset: rungit reset --hard <decision.reset_to>(the working-parent commit). This reset is authorized only inside the disposable autonomous competitor worktree. Never reset master.
-
Optional lineage switch: to mutate a different archive tip, restore that commit’s genome without destroying history of other keeps:
git checkout <archive_commit> -- strategy.py && git commit -m "switch lineage to <slot>"Then continue the loop from step 2. Do not invent commits; use hashes from
lineage.json.
The arena's experiment budget is authoritative. Do not create extra unlogged trials, change the data, or evade a timeout.
Do not ask “should I keep going?” The human may be asleep. Continue until the arena reaches its experiment budget or the human interrupts the process.
If ideas run thin, re-read the evaluator, comparison blocks on near-miss trials, and
lineage.json. Inspect TRAIN funding and open-interest columns. Prefer a structurally
different economic hypothesis to another tiny parameter sweep. Near-misses may combine across
archive lineages, but added complexity must earn its keep under the search or official bar.