# State-aware MLP action experiment — 2026-09-21

## Decision

**Document the fixed-policy variants as a dead end; do not port these variants to TradingView.** All eight variants reduced in-sample calendar Calmar on all five BTC timeframes (40/40 comparisons). This rejects these particular timers and the MVRV veto on the existing winners, not state-aware learning in general. No winner, weights, presets, or Pine files were changed by this experiment.

## Feasibility and experiment definition

The existing network produces one continuous score, not three categorical actions. `_score_to_signals` translates its crossunders into entry/exit opportunities and already tracks whether it is long. Doing nothing while long implicitly holds; doing nothing while flat implicitly stays out. Simply adding names for those actions cannot improve performance.

The useful hypothesis is that memory changes decisions. This experiment adds an opt-in replay policy with four explicit actions (`enter_long`, `exit_long`, `hold_long`, `stay_flat`) and state for position age, pending score exit, last exit cooldown, and macro block expiry. It consumes the current winner's exact raw signal opportunities, with weights and thresholds fixed. This isolates policy effects before investing in a learned multi-output network.

The fixed ablations, declared before the first completed run, are:

| Variant | Rule |
|---|---|
| baseline | New replay with all controls disabled; must exactly reproduce existing positions and executions |
| macro_now | Block entry while exported MVRV regime is bearish |
| macro_memory_4 / 12 | Block bearish bars and the next 4 / 12 bars; each bearish bar refreshes expiry |
| cooldown_4 / 12 | After an executed exit, block the next 4 / 12 bars |
| hold_4 / 12 | Defer score exits until position age reaches 4 / 12 bars |
| combined_4 | Combine all three 4-bar rules |

At 6H, 4 bars is one day and 12 bars is three days. Other timeframes use the same bar counts, not equal wall-clock durations. A hold latches a score exit until eligible rather than discarding a one-bar crossunder and accidentally holding indefinitely. Trailing stops override holds; the Pine trading window still gates all executions. Macro risk is entry-only. Blocked entry opportunities are discarded, never queued. Exiting and re-entering on the same decision bar is disallowed.

This is technically cheap to implement in Pine using persistent counters and existing signals, with no new `request.security` or plot slots. A learned alternative is also feasible, but requires position-conditioned targets and sequential rollouts: separate entry-versus-wait and exit-versus-hold advantages, including transaction costs, age and entry-price information. Four generic softmax labels alone would not establish those targets.

## Results

The run evaluates 9 policies × 5 timeframes × 8 windows = **360 metric rows**, including the all-disabled control. Control positions, entries, and exits match the existing replay exactly on all five datasets.

Calendar Calmar, full in-sample window:

| Timeframe | Baseline | Macro memory 4 | Cooldown 4 | Hold 4 |
|---|---:|---:|---:|---:|
| 4H | 2.161 | 1.007 | 2.122 | 2.091 |
| 6H | 1.004 | 0.743 | 0.929 | 0.850 |
| 8H | 3.118 | 2.325 | 2.862 | 2.947 |
| 12H | 2.025 | 1.107 | 1.323 | 1.146 |
| 1D | 1.596 | 0.383 | 0.873 | 1.030 |

BTC 6H illustrates the cost. Baseline P&L is 2,846.94% with a 50.50% maximum drawdown. Four-bar macro memory reduces P&L to 1,459.94% and worsens drawdown to 53.19%; cooldown reduces P&L to 2,284.23% without improving drawdown; minimum holding reduces P&L to 2,401.03% and worsens drawdown to 56.18%. These are historical simulation numbers, not live returns.

The six configured historical regime windows are useful diagnostics, **not true walk-forward validation**: the promoted weights were fitted using the broader in-sample history. Among paired regime windows where both control and candidate meet the repository's entry-count floor, cooldown_4 improves Calmar in 7/15; macro_memory_4 in 0/12; hold_4 in 2/16. Those isolated gains do not overcome deterioration over the full history.

The nominal OOS window begins 2026-03-01. No policy/timeframe pair meets the repository's trade-coverage floor. Baseline new-entry counts are 6, 0, 3, 3, 3 for 4H, 6H, 8H, 12H, 1D respectively. All 6H policies are flat throughout this window. Several entry vetoes avoid losses elsewhere by remaining flat; this is not enough evidence of predictive benefit. Previously inspected OOS history is also not a pristine holdout.

Machine-readable evidence:

- `results/mlp_experiments/state_policy/metrics.csv`: all 360 rows, including exposure, blocked opportunities, deferred exit bars, entry/exit counts and coverage.
- `results/mlp_experiments/state_policy/manifest.json`: all policy definitions, winner parameters, input/weight/source SHA-256 hashes, actual date spans, warnings and excluded incomplete rows.

## Measurement and data limitations

The runner preserves the repository's close-to-close return convention and 0.5% fee per side. It measures calendar annualization rather than the canonical metric's assumption that each row represents one day. Drawdown includes initial equity. It includes the entire window end date and retains carry-in position P&L even when no new entries occur. Therefore its Calmar values are not interchangeable with the old experiment tables or promotion scores. It does not promote anything.

A timestamp bug in `validate_chart_data.py` treated Unix seconds as nanoseconds, putting 4H/6H/8H dates in 1970 and weakening coverage checks. This was fixed and regression-tested before evaluating results. Validation now reports zero hard errors for all five inputs, with warnings for gaps, abbreviated warmup and/or stale exports. All five exports have a missing MVRV value on their final row; the runner excludes that incomplete tail row consistently for control and candidates, and rejects missing/invalid MVRV elsewhere.

4H/6H/8H exports extend to August 21; 12H/1D exports extend to June 25, 2026, before the final-row exclusion. The manifest records exact usable endpoints. Export validation does not prove feature vintages are causal, or that the files match today's TradingView state. This screen makes neither claim.

The baseline was optimized, while the policies were not, so deterioration does not disprove the existence of a better jointly trained policy. Timeframe results are correlated observations of BTC, not five independent assets. No threshold sweep or training was run. `sync_params.py` passes; `check_pine.py` currently exits 1 on the existing activation strategy's 64-slot budget. This must be resolved before any future training/sweep under the project rules, but this fixed Python replay changes no Pine slots.

TradingView uses a broker emulator with execution timing that can differ from the Python close-price approximation. Any later candidate must be reconciled on orders and fills with identical chart data/settings, not merely matching scores. See [TradingView strategy documentation](https://www.tradingview.com/pine-script-docs/concepts/strategies/).

## Reproduce and verification

```bash
.venv/bin/python tools/validate_chart_data.py
.venv/bin/python tools/run_mlp_state_policy_experiment.py
.venv/bin/python -m pytest tests/ -q
```

The runner has no promotion path and only writes its output directory. `--tfs 6H` supports a targeted rerun; use `--output` to preserve an earlier result set.

Tests cover exact N-bar expiry, refreshed macro memory, post-exit cooldown, pending score exits, stop precedence, time-window precedence, prefix causality, invalid durations, null-policy equivalence across smoothing/trailing branches, integration with real score crossunders, Unix timestamp coverage, inclusive dates, and carry-in losses without new entries. Final full fast suite: **153 passed, 2 slow tests skipped**, with three existing warnings. `git diff --check` passes. A separate audit verified all 360 result rows, all five input/weight/winner hashes, source hashes, all 40 negative IS Calmar comparisons, and the lack of OOS trade coverage.

Review was performed locally, in sequence, preserving the user's pre-existing edits. Reuse/quality/efficiency review retained the separate experimental replay: merging it into the production loop would widen the risk for a rejected candidate. Code review checked state transitions, boundary bars, deferred exits, time gating, metrics, artifact provenance and default-path isolation. A CSV interoperability issue found during analysis was fixed by naming the control `baseline` rather than `null`, which pandas interprets as missing data. No independent cross-model review was performed. Work remains uncommitted alongside pre-existing work.

## What to do next

Archive this result and retain the opt-in harness for reuse. Do not promote these rules, widen timer sweeps on the already inspected data, or spend time on a Pine port of them.

If revisiting the broader idea, the next distinct experiment should learn **position-conditioned exit-versus-hold value**, compared against this exact frozen baseline and a timer control. Include time in trade and unrealized return, preserve a hard stop, and compare any held-out improvement after costs. Train separately before each evaluation boundary, purge overlapping forward-label horizons, select on training/validation only, and reserve a later untouched period. Then test an entry-versus-wait head with a causal macro state if the exit head earns its complexity. Scaling, partial exits and adaptive position sizing should remain separate later experiments because they confound the binary exposure question tested here.

Only a candidate with sufficient unseen trades and consistent risk-adjusted improvement should move to a separate experimental Pine strategy. Re-export chart data and trades together, validate coverage, check score parity using `--asset/--tf`, and reconcile trades before judging TradingView backtest agreement.
