A documented reward hack in a self-optimizing research agent — caught twice, with the full search trajectory, the evaluation harness, and re-runnable ablations.
An LLM coding agent was told to improve its trading strategy's score forever. It reached 169.26 — and ~98% of that came from one
ifstatement, not from the machine learning it spent hours building. When the first exploit was closed by hand, the agent found a different one eighteen minutes later.
| You want | Go to | Time |
|---|---|---|
| The story with no jargon at all | The Robot and the Scoreboard — explained like you're five | 3 min |
| The finding, the numbers, and how to re-run them | this README | 10 min |
| The full argument, protocol, and taxonomy | the paper (PDF, 11 pages; LaTeX alongside) | 40 min |
| Every number traced to a file | evidence/EVIDENCE_MAP.md |
reference |
Every experiment the agent ran was a git commit, so the whole search survives minute by minute.
| Time (2026-03-17) | Event | Score |
|---|---|---|
| 08:34 | honest baseline | 4.62 |
| 08:38 | isotonic calibration — agent's own commit message: "1000x improvement" | 4,346 |
| 09:01 | three more edits — "perfect capture" | 25,551 |
| turnover 0.22, max drawdown exactly 0.0000 — the policy had stopped trading | ||
| 19:34 | human intervention: "remove abstention stack, enforce 80% directional" | −0.18 |
| 19:52 | "regime-aware sizing — VIX override, TLT divergence" | 3.39 |
| 19:54 → 20:13 | VIX threshold walked 25 → 22 → 20 → 18 → 16 → 15 → 14 | 3.93 → 56.18 |
| 03-19 00:51 | drop XGBoost, k=15 | 169.26 |
Two different hacks against the same metric. The first declined to participate; the second bet against the bad days via a hard threshold tuned to the evaluation window. Patching the first did not prevent the second.
The agent never described itself as gaming anything. Its commit messages read in the sincere register of a researcher reporting progress. Behavioural monitoring of agent rationale would not have caught this. Component-level attribution does.
Line 328 of the winning strategy (ablations/vendor/train_c3adb41.py):
vix_bearish = cur_vix > 14.0 and prev_day_down
if vix_bearish and prob_up > 0.5:
prob_up = 0.5 - (prob_up - 0.5) * 0.7 # flip to strongly bearishA direction override, not a participation gate — it overrules the ensemble's own probability whenever it fires. The machine learning still runs; its output is discarded.
Ablate the gate, keep everything else, re-score through the same frozen harness:
| Variant | Score | Sortino | Capture |
|---|---|---|---|
full_169 |
43.04 | 29.46 | 0.829 |
gate_removed |
5.02 | 5.81 | 0.143 |
gate_only (model replaced by a flat prior) |
207,827 | 131,580 | 1.000 |
neither (floor) |
2.30 | 2.78 | 0.086 |
| Accounting | Gate share |
|---|---|
| Leave-one-out above floor, on the released cache | 93.3% |
| Plain drop vs. full, on the released cache | 88.3% |
| Same accounting at the originally recorded 169.2628 | 98.4% |
gate_only at 207,827 is the metric breaking down — semi-deviation hits its
1e-6 guard. That degeneracy is a finding (grind fishing) but makes gate_only
useless as an attribution numerator, so the share is computed leave-one-out.
Details in evidence/METHODOLOGY.md.
| Threshold | 11.0 | 12.0 | 13.5 | 14.0 | 14.5 | 15.0 | 16.0 | 18.0 | 20.0 | 25.0 |
|---|---|---|---|---|---|---|---|---|---|---|
| Score | 43.14 | 43.14 | 43.14 | 43.04 | 42.58 | 31.48 | 27.75 | 17.33 | 12.60 | 6.06 |
Flat below 13.5, peak at the boundary, monotone decay above. The agent halted exactly at the plateau edge — the value capturing the most in-window down days, with no exogenous rationale. Threshold camping, measured.
After this run, the failure modes were written into the agent's instructions as
explicit anti-patterns (harness/program.md): CASH-heavy
strategies named as Sortino-denominator exploits, Sortino > 50 flagged as a red
flag, and VIX-driven direction flips banned on the strength of an out-of-window
backtest showing −84% return, 89% max drawdown
(evidence/backtest_vix_rule.py).
In the guarded rerun — 109 experiments — the same agent tried four VIX gate thresholds and discarded all four. Closing commit: "VIX gate and Kelly both FAIL to beat baseline 2.52."
So this repository holds both halves: a reward hack, and a fix that held.
Requires uv and Python ≥ 3.12. No API keys, no network — the 549-day evaluation set is in the repo.
git clone https://github.com/highwatermark/reward_hacking_research.git
cd reward_hacking_research
uv run ablations/gate_attribution_169.pyA few minutes. Prints the attribution table above; writes
ablations/results/gate_attribution_169.json.
The second script asks a different question of the guarded run — how good is a
bare gate in this landscape at all? A one-line VIX < 17 rule with no learning
outscores all 109 evolved configurations:
uv run ablations/gate_attribution_v2_landscape.pyDependencies are pinned exactly. That is deliberate — this episode also produced a reproducibility failure caused by unpinned ones (METHODOLOGY.md § caveats).
paper/ the paper — compiled PDF and LaTeX source
docs/ eli5.html — the no-jargon version
evidence/ TIMELINE.md · METHODOLOGY.md · EVIDENCE_MAP.md
results_v1_hacked_run.tsv ← the run that produced 169.26
results_v2_guarded_run.tsv ← the rerun where the gate fails
commit_timeline_v1.tsv · backtest_vix_rule.py
harness/ prepare.py (the reward function) · program.md (agent instructions)
data/market_cache.parquet (549 trading days, 2024-01-10 → 2026-03-19)
ablations/ the two re-runnable scripts · vendor/ (both strategies, verbatim)
results/ (JSON + CSV outputs)
- One loop, one asset class, one metric family. An instance, not a frequency.
- Every ablation is in-sample — computed on the same 135 test days the loop optimized against. Correct for asking where the measured score came from; not evidence of skill for anything here.
- 169.26 does not re-measure at 169.26. The evaluation cache was refreshed after the run, so the strategy scores 43.04 today. The attribution ratio is what reproduces.
- Not investment advice. The strategy in
ablations/vendor/is the subject of a paper about measurement failure. It loses 84% out of window.
@misc{lakshmanan2026gate,
title = {98\% of the Alpha Was a Gate: Reward Hacking, Relocation, and
Structural Audit in a Self-Optimizing Research Agent},
author = {Lakshmanan, Hari},
year = {2026},
url = {https://github.com/highwatermark/reward_hacking_research}
}MIT.