# MLP Exit Model Roadmap

Three progressively deeper approaches to improving hold/exit decisions.
Approach A is implemented; B and C are designed but not yet built.

---

## Approach A — Exit score smoothing (IMPLEMENTED)

**Param:** `i_exit_score_window` (int, 1–5, default 1; 1 = identity / no smoothing)

**What it does:** Applies a rolling mean over the last N bars of `mlp_score` before
evaluating the exit crossunder. Entry continues to use the raw score. A window of 2–4
requires sustained score weakness before triggering an exit, filtering single-bar noise
during sideways consolidation.

**Pine:** `mlp_score_exit = ta.sma(mlp_score, i_exit_score_window)` (when window > 1)
**Python:** `score.rolling(window, min_periods=1).mean()` applied before exit crossunder

**How to sweep:** `i_exit_score_window` is included in the deep sweep candidate
generator with an overweight on 1 (≈43% of draws) so the existing behaviour is well
represented. The optimizer compares Calmar across windows without any code changes.

**Success criterion:** At least 2 of 5 BTC TFs show improved Calmar with window > 1
after a full deep sweep, without degrading OOS trades count.

---

## Approach B — Position-aware MLP features (DESIGNED, not built)

**What it adds:** Two new input features appended to the 50-feature vector:
- `in_long_position` — 0.0 or 1.0 (currently in a trade)
- `bars_held_norm` — bars since entry / 100 (capped at 1.0)

**Why:** The model currently treats every bar identically. When in a position, a
sideways score near the exit threshold is very different from the same score when flat
— the model should learn that "score 90 while in trade for 8 bars going sideways ≠
same score at entry time."

**Implementation sketch:**
1. Export from Pine: `plotchar(strategy.position_size > 0 ? 1.0 : 0.0, "in_long_position", ...)` and `plotchar(math.min(bars_since_entry / 100.0, 1.0), "bars_held_norm", ...)`
2. Add both to `FEATURE_COLS` in `strategy_mlp_scores.py` (features 51 and 52)
3. Update Pine static section: arrays → 52 features, loop bounds, stride (Phase 3 upgrade)
4. Retrain all assets — the TV re-export cycle produces the `in_long_position` and `bars_held_norm` columns, which are derived from the *current* model's decisions (one-cycle circularity — acceptable, resolved iteratively)
5. Run parity check

**One-cycle circularity:** The training data's `in_long_position` values come from the
deployed model's decisions, not the new model. This is fine — on the first retrain with
these features, the model sees position state from the old policy. After deploying and
re-exporting, a second retrain sees position state from the improved policy. This
converges quickly in practice.

**Blocking prerequisite:** Approach A sweep results should confirm that smoothing alone
is insufficient, or that position-awareness is orthogonal (i.e., both can be active).
Do NOT start Phase 3 with mixed architectures — run `/mlp-status` first.

---

## Approach C — Separate exit MLP (DESIGNED, not built)

**What it adds:** A second small MLP trained specifically on "while in position" bars,
with a richer set of position-context features:
- All 50 current features
- `in_long_position` (always 1.0 in training — the exit MLP only fires when in a trade)
- `bars_held_norm` — normalized hold duration
- `unrealized_pnl_norm` — (close - entry_price) / entry_price, capped ±1.0
- `drawdown_from_peak_norm` — peak_since_entry to current close, normalized

**Why:** Entry and exit objectives are fundamentally different. The entry MLP is trained
on "is this a good place to open?" The exit model would be trained on "should I close
now, or will this recover?" These are different functions of the same features and
benefit from separate parameter spaces.

**Architecture:** Same 50→16→8→1 shape, separate weight JSON. Pine would run two
forward passes: entry MLP on every bar, exit MLP only when `strategy.position_size > 0`.

**Training objective:** Instead of IS Calmar on the full equity curve, the exit model
is trained on "trades that would improve if held N more bars vs. closed now" — requires
a look-ahead label generation step not present in the current pipeline.

**Complexity:** Doubles artifact count and Pine forward-pass cost. The look-ahead label
generation is a meaningful engineering task. Start here only after B is validated.

**When to build:** When B shows consistent improvement and the primary remaining source
of loss is premature exits (diagnose via trade-level analysis: how many trades were
profitable N bars after the exit signal but closed at a loss?).

---

## Approach A — Sweep results (2026-06-19, dead-zone guard active)

Two sweeps were run. The first (without dead-zone guard) found a 1D window=5 winner that
was a false attractor — the SMA was already below exit threshold at entry, so the smoothed
exit never fired. The dead-zone guard was added (switches to raw-score exit when the SMA
is already below threshold at entry), and a full re-sweep was run.

BTC deep sweep: 80k samples × 5 TFs, all bb50 seed artifacts, dead-zone guard active.

| TF | Window chosen | Old Calmar | New Calmar | IS PnL % | Win Rate | Promoted |
|---|---|---|---|---|---|---|
| 4H | 1 | 0.229 | 0.229 | +2,122% | 40% | no — old winner still best |
| 6H | 1 | 0.425 | 0.472 | +13,440% | 57% | yes (+11% Calmar) |
| 8H | 1 | 0.704 | 0.704 | +13,108% | 61% | yes (better Composite/WFO) |
| 12H | 1 | 0.813 | 0.869 | +8,835% | 62% | yes (+7% Calmar) |
| **1D** | **5** | **1.099** | **1.991** | **+2,786%** | **54%** | **yes (+81% Calmar, guard-clean)** |

**Verdict: 1 of 5 TFs selected window > 1 with honest simulation. Approach B gate not met.**

4H is the outlier — worst by every metric (40% win rate, IS PnL 6–7× lower than 6H/8H).
The dead-zone guard sweep found nothing better for 4H (best sweep candidate: Calmar 0.232,
essentially tied with current 0.229). The 4H winner predates the `i_exit_score_window`
parameter and defaults to window=1.

**Next diagnostic:** Profile 4H trade outcomes to determine whether the primary P&L leak
is bad entries or premature exits. This determines whether Approach B is worth building.

### SMA dead-zone failure condition (important for future window>1 winners)

Exit smoothing creates a "dead-zone" when these conditions are simultaneously true at entry:

1. The entry threshold and exit threshold are far apart (e.g., entry=−80, exit=−300)
2. The score drops sharply through BOTH thresholds in a single bar

When this happens, the SMA at entry time is already below the exit threshold. The smoothed
exit crossunder requires `SMA[prior] >= exit_thr AND SMA[current] < exit_thr`, but since the
SMA never rises above exit_thr post-entry, the crossunder never fires. The trailing stop
becomes the only exit — which may take weeks or months.

**Detection heuristic (add to post-sweep validation for any window>1 winner):**
```python
# After running generate_signals(), check all IS entry bars:
# For each entry bar T: if sma[T] < exit_threshold → dead-zone entry
# If >10% of trades enter in dead-zone state → flag the window>1 winner as risky
```

This condition is NOT caught by WFO stability gates, Fragile scores, or sub-fold Calmar — it
is a latent structural property that only manifests when the score's single-bar drop magnitude
exceeds the entry-to-exit threshold gap.

Next steps before revisiting Approach A:
- Consider profiling trade outcomes on the weak TFs (4H especially) to determine if
  premature exits or poor entries are the primary P&L leak — if premature exits dominate,
  position-aware features (Approach B) may still be worth building even without the
  smoothing signal

---

## Approach B — Probe results (2026-06-19, feature branch `feature/approach-b-4h-probe`)

### Trade-outcome profiling (4H IS window 2017-12-01 → 2025-09-30)

| Bucket | Count | % of losers | Approach B fixes? |
|---|---|---|---|
| Quick exits (≤2 bars, any MFE) | 25 | 64% | No — over before feature is useful |
| "Never had it" (MFE < 0.5%) | 16 | 41% | No — bad entry quality |
| "Gave it back" long (>5 bars, MFE>2%) | 10 | 26% | Yes — Approach B target |
| "Gave it back" quick (≤5 bars, MFE>2%) | 3 | 8% | No — within-bar spike |

The 10 long "gave it back" trades average MFE +10.3% and represent −60.7% combined
P&L (the biggest individual losses: −14.81%, −17.67%). Approach B target = 11% of all IS trades.

### Training probe result

Probe: single seed 42, `--fold-objective robust`, 52 features (50 base + `in_long_position` +
`bars_held_norm` computed from old-policy position state / one-cycle circularity).

| Metric | Baseline seed606 | Approach B probe |
|---|---|---|
| IS Calmar | 0.229 | 0.152 |
| IS Max DD % | −29.5% | −70.89% |
| IS Trades | 94 | 51 |
| Phase 1 early stop | — | epoch 22 (val MSE 0.243) |

**Weight analysis:** `bars_held_norm` ranked 10/52 in Phase 1 W1 column norms (0.72 vs median
0.61) — meaningfully above median, suggesting the model found it useful for predicting forward
returns. `in_long_position` ranked 45/52 (below median, less useful).

**Confounds:** Single seed (baseline used seed606 selected from many); Phase 1 converged very
early (epoch 22 vs expected hundreds); probe weights are globally weaker (median norm 0.61 vs
0.87 for baseline), suggesting underfitting. Position features from old policy introduce
circularity noise that degrades sweep accuracy.

### Verdict: Approach B is NOT recommended at this time

The 4H problem is primarily **bad entries** (41% "never had it") and **immediate whipsaws**
(64% of losers exit ≤2 bars) — neither of which position-aware features can fix. The 10
"gave it back" long trades are addressable by Approach B, but they're only 11% of IS trades.

The probe Calmar (0.152) is 34% below baseline (0.229) — a negative result. While confounds
exist (single seed, early convergence), the signal is weak enough that the full Phase 2 upgrade
(Pine forward-pass changes, re-export of all 20 asset/TF data files, parity checks) is not
justified.

**Primary bottleneck for 4H is entry quality, not exit quality.**

### Path forward for 4H

1. **More seeds on standard 50-feature arch** — the current best (seed606) emerged from
   multi-seed training. Running seeds 700–900 (3–6 seeds, `--fold-objective robust`) may find
   a better 4H model without any architectural complexity.

2. **Accept 4H as the weakest TF** — deploy 6H/8H/12H/1D (Calmar 0.47–1.99) for production
   and treat 4H as experimental until a better model emerges.

3. **Revisit Approach B after more seeds** — if the best achievable 4H Calmar with many seeds
   stays near 0.23, profiling those trades may show a different balance (fewer "never had it"
   trades, more "gave it back"). Only then does Approach B become the right next experiment.

---

## Decision tree

```
A sweep done?
├── No → run deep sweep, check Calmar improvement across window values
└── Yes
    ├── A improves ≥2 TFs → keep A, consider B as additive
    └── A neutral → either B addresses a different gap (position-awareness) or exit
        logic isn't the bottleneck (check entry timing instead)

B retrain done?
├── No → retrain all TFs with 52-feature arch, compare Calmar to A-only baseline
└── Yes
    ├── B improves on A → deploy, set as new baseline
    └── B neutral → exit timing may not be the primary loss source; profile trade
        outcomes instead

C is worth building when:
- B is deployed and validated
- Trade-level diagnosis confirms premature exits are the primary remaining P&L leak
- Team has bandwidth for the look-ahead label generation pipeline
```
