# MLP Activation Strategy (`strategy_mlp_scores`) — Plan & Progress

> Multi-session implementation tracker. Approved plan from 2026-06-09; original
> plan file: `~/.claude/plans/humming-spinning-dewdrop.md`. Update the Progress
> checklist as steps complete so any session can resume from here.

## Progress

- [x] **1. RED tests** — `tests/test_mlp_forward.py`, `tests/test_mlp_artifact.py` (+ `tests/conftest.py`)
- [x] **2. Core module** — `strategies/strategy_mlp_scores.py`: `_tanh`, `mlp_forward`, artifact I/O, `FEATURE_COLS`, `FEATURE_WEIGHT_PARAMS`
- [x] **3. generate_signals + signal tests** — `tests/test_mlp_signals.py` linear-degeneracy test passes (identical trade bars vs perceptron strategy, with and without trailing stop); end-to-end `validate_strategy.py` sanity run OK (random artifact → 361 trades, plumbing verified)
- [x] **4a. Deps** — `requirements.txt` += torch, cma, pytest (installed via `/usr/bin/python3 -m pip install --user`); `tools/train_mlp.py` written (both phases)
- [x] **4b. Verify Phase 1** — smoke run OK on MPS (early stopping works; tiny smoke slice overfits as expected — train 0.24→0.03 while val rises, epoch-0 checkpoint kept; real 12k-bar run is the meaningful test). 19/19 tests pass incl. torch reference.
- [x] **5a. Phase 2 CMA-ES smoke** — full two-phase `--smoke` runs end-to-end (CMA-ES ask/tell loop, fold-Calmar fitness, artifact with recommended thresholds). Thresholds now **seeded from pretrained-net score percentiles** (60/75/30) — fixed defaults sat outside the score range → zero trades → flat fitness.
- [x] **5b. Acceptance test** — `tests/test_acceptance_mlp.py` written (`@pytest.mark.slow`; smoke train + validate end-to-end). **Real 6H run deferred to M4** (`/usr/bin/python3 tools/train_mlp.py --data data/COINBASE_BTCUSD-6H.csv`, ~30-60 min).
- [x] **6. Threshold params JSONs** — `strategies/params/params_strategy_mlp_scores_COINBASE_BTCUSD_{4H,6H,8H,12H,1D}.json` written. Threshold sweep (optimize_strategy.py run) deferred until artifact exists after real training.
- [x] **7. Pine strategy** — `strategies/strategy_mlp_scores.pine` written (~530 lines): indicator section, `f_tanh`, 49-feature vector, MLP forward pass, trade logic, 49 plotchar exports, placeholder sentinel. Plot budget: ~63/64. Security budget: 39/40.
- [x] **8. Pine codegen** — `tools/generate_pine_mlp_presets.py` written; `tests/test_pine_codegen.py` 30/30 green. `fmt_float9` uses 10 sig figs (≤1e-9 rel error). **User TV compile-test still pending** — paste `strategy_mlp_scores.pine` into TradingView to settle size question before training other TFs.
- [ ] **9. Remaining TFs** — train 4H/8H/12H/1D, regenerate all 5 presets (deferred to M4)
- [x] **10. Parity tooling** — `tools/check_mlp_parity.py` written. TV round-trip check pending (needs TV re-export with `mlp_score` column after Pine is live).
- [x] **11. Docs** — MLP commands + Testing section added to `CLAUDE.md`; `strategies/params/mlp/` convention documented.

## Session Notes (read before resuming)

- **Interpreter**: use `/usr/bin/python3` (3.9.6). Homebrew python3 has no packages.
  Installed to user site this session: pytest 8.4.2, numba 0.60.0, torch 2.8.0 (MPS works), cma 4.4.4.
- **Run tests**: `/usr/bin/python3 -m pytest tests/ -q --ignore=tests/test_acceptance_mlp.py` — 30 passed as of 2026-06-10.
- **Plan correction**: `config.WEIGHT_COLS` order ≠ `_prepare_features()` column order
  (positions 12–14 differ). `FEATURE_COLS` in `strategy_mlp_scores.py` follows the
  feature-matrix order; `FEATURE_WEIGHT_PARAMS` maps feature index → `i_w_*` name.
  Tests assert order against `_prepare_features` directly, sets against WEIGHT_COLS.
- **Plan correction**: ES fitness uses **mean per-fold OOS Calmar** (matching
  `auto_optimize_loop.py`'s WFO convention), NOT per-fold composite —
  `composite_score()`'s 1000% P&L gate can never pass on ~1-year folds.
- **Phase 1 simplification**: early-stops on the last WFO fold's OOS span
  (2025-10-01+) instead of full per-epoch 6-fold CV; Phase 2 does the real
  fold-averaged selection.
- `--smoke` mode: 800 bars, hidden [8,4], 1 pseudo-fold (last 25%), tiny budgets.
- **Fitness semantics**: no-trade candidates score fitness 0, which beats negative
  mean fold Calmar by design (flat > losing). On the smoke window (ATH correction)
  the honest optimum was no-trade — expected; the real 6-fold run has bull folds.

## Approved Plan (reference)

### Context

The current strategy is a single-layer perceptron: 49 pre-normalised feature columns → weighted sum → score in [-1000, +1000] → crossunder entry/exit thresholds. This replaces the score computation with a small MLP that can learn nonlinear feature interactions, keeping everything else (feature pipeline, crossunder trade logic, metrics, parity workflow) intact.

**Decisions confirmed with user:**
- **Output head**: single scalar score, `1000 × tanh(...)`, existing crossunder entry/exit machinery unchanged.
- **Training**: Phase 1 supervised pretrain (PyTorch) + Phase 2 CMA-ES fine-tune on the WFO-fold-averaged backtest metric.
- **Pilot scope**: COINBASE_BTCUSD only, all 5 TFs (4H/6H/8H/12H/1D). One new Pine file.
- **Constraint**: existing library files NOT modified — new parallel files; reuse via import.
- **Pine parity**: forward pass computed inside the new Pine strategy (loops over `array.from` weight literals).

### Architecture

```
score = 1000 · tanh( W3 · tanh( W2 · tanh(W1·x + b1) + b2 ) + b3 )
```
- Input = 49 features in `_prepare_features()` order. Default **49→16→8→1** (941 params); fallback **49→8→4→1** (441 params) via `--hidden 8 4`, decided by TV compile test (step 8).
- `tanh` closed form in both languages (Pine v6 has no `math.tanh`):
  `tanh(x) = x ≥ 20 ? 1 : x ≤ -20 ? -1 : (exp(2x)−1)/(exp(2x)+1)`
- `i_div_window` unsupported in pilot (MLP sees raw single-bar RSID flags).

### Key file specs (not yet built)

**`strategies/strategy_mlp_scores.pine`** — new file (fresh 64-plot / 40-security budgets):
sentinel block `// ── AUTO-GENERATED MLP PRESETS` … `// ── END AUTO-GENERATED` with preset
auto-detect (`syminfo.prefix + "_" + syminfo.ticker + " " + tf`), per-preset thresholds via
`switch`, weights as per-row `array.from` literals populated into shared `var float[]` arrays
inside `if _preset == "..."` blocks on `barstate.isfirst`. Indicator section copied verbatim
(keeps request.security at 39/40). Exports: `plot(mlp_score)`, thresholds, plotchar of all
49 features (~55/64 plots).
**Size risk (#1)**: 941 floats × 5 presets ≈ 150 KB source — compile-test ONE preset first;
fall back to 441 params (~35 KB) or per-TF files.

**`tools/generate_pine_mlp_presets.py`** — modeled on `tools/generate_pine_presets.py`
(sentinel rewrite, preset keys, `_sync_pine_dates`). Reads artifacts + winner threshold CSVs
(falls back to artifact's `recommended_thresholds` if no winner CSV). Float format `%.9g`
minimum. Emits arch + artifact-hash comment per preset. `--dry-run` supported.

**`tools/check_mlp_parity.py`** — CLI modeled on `diagnose_tv_parity.py`. Compares Python
`mlp_forward` vs TV-exported `mlp_score` column. Pass: max |Δ| < 0.01 AND zero
threshold-side disagreements; warn-list bars with |score − threshold| < 0.05.

**Threshold params JSON** (`strategies/params/params_strategy_mlp_scores_COINBASE_BTCUSD_{TF}.json`):
```json
{
  "mlp_weights_file": {"values": ["strategies/params/mlp/mlp_weights_COINBASE_BTCUSD_6H.json"]},
  "i_long_entry_activation_threshold": {"start": -300.0, "stop": 300.0, "step": 5.0},
  "i_long_exit_activation_threshold": {"start": -300.0, "stop": 400.0, "step": 10.0},
  "i_long_exit_activation_confirmation_threshold": {"start": -300.0, "stop": 400.0, "step": 10.0},
  "i_trailing_stop_threshold": {"start": 0.0, "stop": 50.0, "step": 5.0},
  "i_use_long_exit_confirmation": {"values": [1]},
  "i_use_long_entry_confirmation": {"values": [false]},
  "i_regime_window": {"values": [0]},
  "i_regime_entry_min_score": {"values": [-1000.0]},
  "i_mvrv_suppress_bear": {"values": [false, true]}
}
```
Sweep command:
```
/usr/bin/python3 strategies/optimize_strategy.py --data data/COINBASE_BTCUSD-6H.csv \
  --strategy_file strategy_mlp_scores.py \
  --params_file strategies/params/params_strategy_mlp_scores_COINBASE_BTCUSD_6H.json \
  --random_search 5000 --metric "Calmar Ratio"
```

**Remaining tests** — `tests/test_pine_codegen.py` (9-sig-digit round-trip < 1e-3;
sentinel replacement idempotent); `tests/test_acceptance_mlp.py` (`@pytest.mark.slow`,
`train_mlp.py --smoke` → artifact → `validate_strategy.py` subprocess → finite metrics).

### Training commands

```bash
# Smoke (plumbing): ~1 min
/usr/bin/python3 tools/train_mlp.py --data data/COINBASE_BTCUSD-6H.csv --smoke
# Real run per TF (Phase 1 + Phase 2):
/usr/bin/python3 tools/train_mlp.py --data data/COINBASE_BTCUSD-6H.csv
# Fallback small net if TV compile fails:
/usr/bin/python3 tools/train_mlp.py --data data/COINBASE_BTCUSD-6H.csv --hidden 8 4
```

### Verification summary

- `python3 -m pytest tests/ -q` (use /usr/bin/python3)
- `validate_strategy.py --strategy_file strategy_mlp_scores.py --params_file <json>` single backtest
- `tools/check_pine.py` on new pine; TV compile test at step 8
- `tools/check_mlp_parity.py` after TV re-export
- OOS (2026-03-01+) honesty check only after all training/sweeps frozen

### Key risks

1. **Pine compiled-size limit** — mitigation ladder above.
2. **Overfit** (941 params vs ~12k bars) — fold-mean Calmar, L2 anchor to pretrain, untouched OOS holdout; drop to 441 params if fold variance is high.
3. **MPS float32** — Phase 1 only; ES refines float64; artifact float64 is single source of truth.

### Reused without modification

`strategy_activation_scores.py` (`_prepare_features`, `_apply_trailing_stop`, `calculate_metrics`),
`library_activation_scores.py` (`calculate_positions`), `validate_strategy.py`,
`optimize_strategy.py` CPU path, `config.py` (WFO_FOLDS, dates), `check_pine.py`,
`compare_tv_trades.py` workflow.
