Scope
Train a PPO agent to PICK from a 5-rule hand-coded rule set (EW, risk-parity, vol-target, credit-gate, trend-filter) instead of outputting a continuous weight vector. The meta-allocator changes the problem formulation: discrete action (5-way), simpler than the continuous 4-dim v3 action, and the rules are pre-validated candidates the agent can choose between.
Verdict: REJECT
15 cells (5 walk-forward windows x 3 seeds). The agent picked EW 62% of the time and was statistically indistinguishable from the equal-weight baseline on every cell. The rule-switch cost alone (35.7 switches per 126-day OOS cell at 25 bps/switch = ~7 bps/day = 17% annualized drag) makes the net result worse than gross.
Pre-registered pass bar (from rl_v3_hypothesis.md)
ADOPT requires ALL THREE:
- OOS Sharpe > equal-weight baseline Sharpe + 0.10
- Bootstrap p (agent > baseline) < 0.05
- OOS max drawdown < 0.30
Pipeline summary
| Stage | Deliverable | Verdict |
|---|---|---|
| env | validation/rl_v3_meta.py | MetaRuleEnv with Discrete(5) action space, 9/9 tests pass |
| trainer | models/rl_v3_meta_trainer.py | 15 cells trained (3 seeds x 5 windows, 8k timesteps) |
| OOS | reports/rl_v3_meta_oos.md | REJECT (median uplift -0.254, max p 0.646, maxDD -20.10%) |
| audit | reports/rl_v3_meta_audit.md | mechanism: agent is materially different from EW (corr +0.012), NOT a SPY bet (corr -0.002), but DOES WORSE in bear regimes (-0.00058 vs +0.00209) |
| net | reports/rl_v3_meta_net.md | REJECT (net uplift -1.674, S3 TRIGGERED) |
| ledger | this report | REJECT |
OOS scoreboard (15 cells)
| Window | Seed | OOS start | OOS end | Agent Sharpe | EW Sharpe | Uplift | p(agent>EW) |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 2023-11-15 | 2024-05-16 | +2.19 | +2.44 | -0.254 | 0.535 |
| 0 | 1 | 2023-11-15 | 2024-05-16 | +2.52 | +2.44 | +0.076 | 0.502 |
| 0 | 2 | 2023-11-15 | 2024-05-16 | +2.22 | +2.44 | -0.228 | 0.531 |
| 1 | 0 | 2024-05-17 | 2024-11-14 | -0.16 | +0.61 | -0.770 | 0.646 |
| 1 | 1 | 2024-05-17 | 2024-11-14 | +0.43 | +0.61 | -0.184 | 0.562 |
| 1 | 2 | 2024-05-17 | 2024-11-14 | +0.70 | +0.61 | +0.085 | 0.518 |
| 2 | 0 | 2024-11-15 | 2025-05-20 | -1.69 | -0.94 | -0.754 | 0.591 |
| 2 | 1 | 2024-11-15 | 2025-05-20 | -1.71 | -0.94 | -0.769 | 0.617 |
| 2 | 2 | 2024-11-15 | 2025-05-20 | -1.76 | -0.94 | -0.824 | 0.609 |
| 3 | 0 | 2025-05-21 | 2025-11-18 | +1.25 | +1.81 | -0.554 | 0.605 |
| 3 | 1 | 2025-05-21 | 2025-11-18 | +1.76 | +1.81 | -0.043 | 0.514 |
| 3 | 2 | 2025-05-21 | 2025-11-18 | +2.17 | +1.81 | +0.360 | 0.449 |
| 4 | 0 | 2025-11-19 | 2026-05-21 | +4.51 | +4.78 | -0.270 | 0.536 |
| 4 | 1 | 2025-11-19 | 2026-05-21 | +4.40 | +4.78 | -0.374 | 0.573 |
| 4 | 2 | 2025-11-19 | 2026-05-21 | +4.88 | +4.78 | +0.104 | 0.458 |
Aggregate: median uplift -0.254, max bootstrap p 0.646, worst maxDD -20.10%.
Mechanism audit
| Sleeve | Agent | EW | delta |
|---|---|---|---|
| eqmom | 0.250 | 0.250 | +0.000 |
| gate60_ETH | 0.231 | 0.250 | -0.019 |
| gate60_BTC | 0.242 | 0.250 | -0.008 |
| spy_bench | 0.257 | 0.250 | +0.007 |
The mean agent allocation is within 0.02 of EW on every sleeve. The 5-17% rule choices (TREND_FILTER, VOL_TARGET, RISK_PARITY, CREDIT_GATE) collectively moved weights by ~1.5% per sleeve. That magnitude is too small to register against the noise in 6mo OOS windows.
| Regime | Agent | EW | delta |
|---|---|---|---|
| bull | +0.00071 | +0.00076 | -0.00004 |
| bear | -0.00058 | +0.00209 | -0.00267 |
The agent does WORSE in bear regimes. The plan's pre-registered mechanism prediction (the agent should win in bear regimes) is not observed. This is a stronger failure than v3, where the agent was at least better in bear regimes (marginally).
Net-cost analysis
| Rule | OOS days | Share |
|---|---|---|
| EW | 1162 | 62.0% |
| RISK_PARITY | 98 | 5.2% |
| VOL_TARGET | 206 | 11.0% |
| CREDIT_GATE | 86 | 4.6% |
| TREND_FILTER | 323 | 17.2% |
The agent switched rules 35.7 times per OOS cell on average (35.7 / 126 = 28% of OOS days). At 25 bps per switch, that's ~7 bps/day or ~17% annualized drag. This single factor explains why the net result is uniformly worse than gross: even when the gross was slightly positive (window 0 seed 1: +0.08 uplift), the cost overwhelms it (net: -1.67).
The cost could be reduced by adding a hold_period (action frozen
for N bars after any rule change) — but that is a different design
than what was tested here.
Stop conditions (hypothesis doc)
- S1 (baseline Sharpe > 0.3): NOT triggered. EW median OOS Sharpe on the 13-window baseline was +0.61, well above the 0.3 floor.
- S2 (median agent uplift >= 0): TRIGGERED. Median uplift -0.254.
- S3 (net uplift >= 50% of gross): TRIGGERED. Net is 0% of gross (in fact, deeply negative).
Combined RL study verdict (v3 + v4 + meta-allocator)
This is the third RL arm in this repo to close at REJECT. The pattern is now consistent:
| Study | Action space | Cost in env? | Verdict |
|---|---|---|---|
| rl_oos (v1) | continuous [0,1] | no | REJECT (p=0.714) |
| rl_v3 (this repo) | continuous [0,1.5]^4 | no | REJECT (median -1.28) |
| rl_v4 | Δ-weights overlay, hard caps, weekly rebalance, cost in env | yes | REJECT (median -0.002) |
| rl_v3 meta | discrete {EW, RP, VT, CG, TF} | post-hoc only | REJECT (median -0.25) |
Across all four arms:
- The agent learns something (mechanism PASS in v3, v4)
- But the magnitude is too small to clear p<0.05 on OOS
- And the cost (whether modeled in env or post-hoc) overwhelms any marginal edge
- And EW is genuinely hard to beat (median Sharpe +0.61 on the 13-window baseline, maxDD well within 30%)
This is now a settled finding for this sleeve set and data window. A future RL attempt would need either a different sleeve set (structural alpha the agent can exploit) or a different baseline (EW is the right null to beat; the question is whether the agent's edge is real or noise).
What this means for the live book
Nothing changes. The validated equal-weight baseline (production/book_allocator.py) stays the production default. No production/ runner is registered. No live orders. No new data dependency. The v3, v4, and meta results all converge on the same conclusion: an RL allocator does not add value to this sleeve set / data window / cost model.
What we WOULD change before another RL attempt
- Different sleeve set. The eqmom flagship, gate60 crypto trend, and SPY bench are all positively correlated equity-like bets. The agent has no source of decorrelated alpha to exploit. A sleeve set that includes a genuine diversifier (trend-following on TLT, carry on FX, volatility-selling) would give the agent something to allocate.
- Different baseline. EW is hard to beat. A weaker baseline (e.g. fixed-50%-equity) would be easier to improve on, but would not produce a useful live deployment.
- A cost model that the agent can SEE during training. The meta-allocator's high switching cost was a surprise; the env didn't include it. Baking the cost into the reward (as v4 did) is necessary for the agent to learn to hold positions.
- A hold-period constraint. If the action is discrete, the agent should be required to hold for N bars after any switch. This is a small change but it bounds the cost drag.
Files this study produced
| File | Purpose |
|---|---|
| validation/rl_v3_meta.py | env + rule set + OOS replay |
| models/rl_v3_meta_trainer.py | PPO walk-forward trainer |
| validation/rl_v3_meta_audit.py | mechanism audit |
| validation/rl_v3_meta_net.py | net-cost backtest |
| tests/test_rl_v3_meta.py | 9 unit tests (all pass) |
| models/checkpoints/rl_v3_meta/meta_main/ | 15 PPO checkpoints |
| reports/rl_v3_meta_oos.md, rl_v3_meta_audit.md, rl_v3_meta_net.md | per-stage reports |
| reports/rl_v3_meta_decision.md | this ledger entry |
| scripts/fetch_crypto_long.py | yfinance 1Day fetcher (used for the longer walk-forward) |
| validation/rl_v3_baseline.py | updated to use longer crypto history |
Date locked
2026-09-04