# Architecture Review & Design Spec
**Date:** 2026-03-22
**Prepared by:** Claude (based on full codebase read)
**Status:** Updated 2026-03-22 — all open questions resolved; QW-1/2/3/5 implemented

---

## Part 1 — Architecture Document (Current System As-Implemented)

### 1.1 Data Flow

```
TradingView export (CSV)
  → pre-calculated indicator columns (OHLCV + ~30 signal columns)
  → _prepare_features() — CPU normalisation to [-1, +1] per indicator
  → GPU kernel — weighted dot-product, threshold comparisons, P&L
  → CPU verification — re-run top-5000 GPU candidates at full precision
  → sweep_database.db — store all CPU-verified rows with P&L/DD > 0
  → mine_sweep_db.py — sign stability, tier analysis, lock suggestions
  → lock_params.py — narrow param JSON ranges based on DB evidence
```

### 1.2 Score Formula

```
score(t) = Σ [ normalize(indicator_i, t) × weight_i ] / Σ |weight_i| × 1000
```

Each indicator is normalised to \[-1, +1\] before weighting. Normalisation is 2-way (clip) or 3-way (sign) depending on indicator type. The result is scaled to \[-1000, +1000\].

### 1.3 Signal Logic — Two Variants Co-Existing

The code contains two entry and two exit strategies, switched by boolean free parameters. This is the source of significant complexity:

**Entry (controlled by `i_use_long_entry_confirmation`):**
- `false` (simple): 1-bar crossunder — `prev_score >= entry_thr AND score < entry_thr`
- `true` (confirmed): 2-bar crossunder + rising score + no exit on prior bar

**Exit (controlled by `i_use_long_exit_confirmation`):**
- `false` (stoch-based): 2-bar crossunder + `stoch_is_peaking`
- `true` (threshold-based): 2-bar crossunder + `score < exit_conf_threshold`

This means there are **4 effective strategy variants** being simultaneously searched. The optimizer can and does pick whichever variant fits the IS data best, which is a source of overfitting.

### 1.4 Free Parameter Count

| Category | Count | Examples |
|---|---|---|
| Weight params (continuous range) | ~24 | `i_w_stoch`, `i_w_macd_pred`, `i_w_spy`, … |
| Threshold params (continuous range) | 3 | entry, exit, exit_conf thresholds |
| Confirmation flags (binary) | 2 | `i_use_long_entry_confirmation`, `i_use_long_exit_confirmation` |
| Regime filter params | 2 | `i_regime_window`, `i_regime_entry_min_score` |
| Miscellaneous | 2 | `i_trailing_stop_threshold`, `i_m3_momentum_period` |
| **Total free** | **~33** | |
| Locked to zero / fixed range | ~7 | `i_w_osc`=0, `i_w_vix`=0, `i_w_macd_bullish` ≤0, etc. |

With ~2900 IS bars on BTC 1D (2017–2024), this is approximately **88 observations per free parameter** — borderline adequate. Shorter timeframes are fine (BTC 4H ≈ 530 bars/param). However, the 4-variant strategy switch described above effectively multiplies overfitting surface area.

### 1.5 Objective Function (As-Implemented)

**IS optimisation target (current, as of 2026-03-22):** `Sortino Ratio`
- Switched from `P&L/DD Ratio` via QW-1 (see Spec B below)
- Sortino is computed natively by the GPU kernel — no GPU/CPU metric mismatch
- **DB filter:** `P&L/DD Ratio > 0` — only results with positive return-to-drawdown ratio are stored. Sortino scores are also stored per row for sign stability analysis and optuna biasing.
- `MIN_SCORABLE_TRADES` raised from 10 → 30 to prevent Sortino gaming via cherry-picked trades

**History:** The metric evolved Sortino → Calmar → P&L/DD because high-metric solutions gamed the metric via few cherry-picked trades. The root cause was an insufficient minimum trade floor, not a problem with Sortino itself. Fix: raise MIN_SCORABLE_TRADES, revert to Sortino.

### 1.6 Regime Filter (As-Implemented)

A rolling-mean regime gate exists in the CPU path (`strategy_activation_scores.py:264-314`):
- When `i_regime_window > 0`: suppresses entries when the rolling mean of the score over the last N bars is below `i_regime_entry_min_score`
- **NOT implemented in the GPU kernel** — the GPU optimises without this gate firing
- Result: IS metrics computed by GPU assume regime filter is off; CPU verification correctly applies it; but since GPU ranks differ from CPU ranks when regime_window > 0, the regime filter param is only effectively optimised in the CPU verification layer (top 5000 candidates)

### 1.7 Indicator Inventory

**Fast/technical (bar-level resolution):**
- `i_w_stoch` — stochastic RSI normalised
- `i_w_macd_pred` — MACD line prediction sign
- `i_w_osc` — oscillator (currently locked to 0)
- `i_w_macd_bullish` — MACD crossover flag (locked negative)
- `i_w_m3_momentum` — 3-period M3 momentum
- `i_w_m2_tiny` — M2 momentum with tiny offset
- `i_w_m2_div_osc` / `i_w_m2_div_osc_noOffset` — M2 vs oscillator divergence (2 variants)
- `i_w_m3_div_osc` — M3 vs oscillator divergence
- `i_w_stoch_div_osc` — stochastic vs oscillator divergence
- `i_w_stoch_peaking` / `i_w_stoch_bottoming` — stochastic extreme state
- `i_w_vwap_div_osc` — VWAP divergence vs oscillator
- `i_w_rsid_osc` — RSI divergence oscillator

**Candlestick patterns (bar-level, mostly locked to 0):**
- `i_w_bullish_hammer`, `i_w_bullish_engulfing` (locked ≤0), `i_w_bearish_engulfing` (locked ≤0), `i_w_shooting_star` (locked 0)

**Slow/macro (daily–monthly resolution, updated at bar frequency but represent slower phenomena):**
- `i_w_btc_spx_corr` — BTC/SPX 30-day rolling correlation
- `i_w_dxy` — Dollar index rate-of-change direction
- `i_w_vix` — VIX percentile rank inverted (locked 0)
- `i_w_btc_dom` — BTC dominance ROC sign
- `i_w_us10y` — US 10Y yield ROC inverted sign (locked ≥+5)
- `i_w_spy` — SPY vs 200-EMA
- `i_w_gold` — Gold ROC percentile rank

**On-chain / regime (weekly–monthly cadence, exported as daily bars):**
- `i_w_mvrv` / `i_w_mvrv_cont` — MVRV Z-score, discrete band-state and continuous magnitude (complementary, not redundant — see Spec D)
- `i_w_nupl` — Net Unrealised Profit/Loss normalised
- `i_w_fed_net_liq` — Fed net liquidity direction — **permanently 0 in current exports**. Pine script uses `FRED:WALCL`, `FRED:WTREGEN`, `FRED:RRPONTSYD` data sources which require a paid TradingView plan. These return NA on basic/pro plans, causing `fed_net_liq_sign` to default to 0.0. **Locked to `values: [0.0]` in all 20 params JSONs as of 2026-03-22.** Will remain locked until TradingView plan is upgraded or an alternative data source is found.
- `i_w_gc_position` — Gaussian channel position (currently 0 pending re-export)

### 1.8 Discrepancies Between Intended and Actual Design

| # | Intended | Actual | Impact |
|---|---|---|---|
| 1 | GPU optimises for same metric as CPU | GPU uses Calmar/Sortino; CPU ranks by P&L/DD | Candidate selection noise |
| 2 | Regime filter gates entries during search | Regime filter bypassed in GPU kernel | Regime params only optimised over top-5000 CPU candidates |
| 3 | Optuna trial history persists across runs | No native Optuna persistence; DB used as proxy | Biased sampling works but no native Optuna convergence metrics |
| 4 | Single coherent strategy being optimised | 4 implicit strategy variants (2×2 boolean flags) | Overfitting surface area doubled |
| 5 | mvrv_cont was locked (stale col) | Unlocked in latest params but column may still be zero | Wasted parameter if export not updated |

---

## Part 2 — Technical Specifications

### Spec A: Phased Optimisation (Weights First, Then Thresholds)

**Problem:** With ~33 free parameters simultaneously, the optimiser can achieve good IS scores by over-fitting the threshold values to the specific IS bar sequence rather than finding genuinely informative signal weights. Thresholds directly control trade count and timing in a way that weights do not — they are more susceptible to bar-specific overfitting.

**Evidence:** The 2-bar vs 1-bar crossunder choice (confirmation flags) selects whichever timing mode best fits the IS window. With entry and exit thresholds spanning 600+ unit ranges, the optimiser has enormous freedom to cherry-pick trade timing.

**Recommendation:** Two-phase optimisation:

*Phase 1 — Weight search (fixed thresholds):*
- Fix `i_long_entry_activation_threshold`, `i_long_exit_activation_threshold`, `i_long_exit_activation_confirmation_threshold` to empirically reasonable values derived from current DB top results
- Fix `i_use_long_entry_confirmation = false` and `i_use_long_exit_confirmation = false` (simplest, most consistent variant — see Spec C)
- Free parameter count drops from ~33 to ~26
- Run 2–4 hour random search; populate DB for all combos
- Goal: identify true signal weights with thresholds held neutral

*Phase 2 — Threshold tuning (fixed weights from Phase 1):*
- Lock the top-5 weights found in Phase 1 (signs and approximate magnitudes)
- Free the 3 threshold params with tighter ranges (e.g., ±100 around Phase 1 winner values)
- Optuna is well-suited for this: low-dimensional, continuous, no binary flags
- Run 1–2 hours; converges quickly at this dimensionality

**Implementation:**
- Add `--phase` argument to `run_marathon.py` or create `run_phased.py`
- Phase 1: set thresholds to `values: [250]`, `values: [50]`, `values: [-50]` in params JSON temporarily
- Phase 2: set weight ranges to `values: [winner_value]` for top-5 locked weights

**Effort:** M (2–3 days)
**Risk if skipped:** Current winners may be threshold-overfit; OOS performance degradation is partly explained by threshold values that are calibrated to 2017–2024 market microstructure.

---

### Spec B: Fix GPU/CPU Metric Mismatch ✅ DONE (2026-03-22)

**Problem:** GPU optimises via Calmar Ratio; CPU verification and DB insertion rank by P&L/DD Ratio. These are different metrics. The top GPU candidate may not be the top CPU-verified result.

**Resolution:** Switched to `Sortino Ratio` as `OPTIMIZATION_METRIC` in `auto_optimize_loop.py:27`. Also raised `MIN_SCORABLE_TRADES` from 10 → 30 in `strategy_activation_scores.py:27` to prevent Sortino gaming via few cherry-picked trades. DB schema version NOT bumped — existing rows have sortino_ratio stored and are still useful for optuna biasing.

**Why P&L/DD was chosen originally:** The metric evolution was Sortino → Calmar → P&L/DD because high-metric solutions gamed earlier metrics via few cherry-picked trades. Root cause was the minimum trade floor (10 in scoring window) being too low, not a problem with Sortino itself.

**Effort:** S (completed)
**Status:** Active as of 2026-03-22

---

### Spec C: Fix the Strategy Variant Problem (Confirmation Flags) ✅ DONE (2026-03-22)

**Problem:** `i_use_long_entry_confirmation` and `i_use_long_exit_confirmation` are both free binary parameters. This means the optimiser is simultaneously searching 4 different strategy variants:
- Variant 1: simple entry + stoch exit
- Variant 2: simple entry + threshold exit
- Variant 3: confirmed entry + stoch exit
- Variant 4: confirmed entry + threshold exit

Each variant has different risk/reward characteristics. The IS winner may be whichever variant happened to fit the IS period, not necessarily the most generalisable.

**Evidence:** Looking at DB top results, the `i_use_long_exit_confirmation` and `i_use_long_entry_confirmation` flags likely split roughly evenly across top performers (or cluster on one variant), but without separate analysis we can't tell which variant is actually better — the IS winner mixes them.

**Recommendation:**
Analysis of 56,144 top-quartile v3 rows:

| entry_conf | exit_conf | n | avg_sortino | avg_pnl_dd |
|---|---|---|---|---|
| 1 | 0 | 16,811 | **2.202** | 1,053 |
| 1 | 1 | 13,360 | 2.201 | 1,084 |
| 0 | 1 | 12,457 | 2.199 | 1,209 |
| 0 | 0 | 13,516 | 2.133 | 1,039 |

Key finding: all four variants differ by only 0.07 Sortino on IS data. The optimizer switches between them at near-zero IS cost, making the IS winner essentially arbitrary. **Lock `entry_conf=1, exit_conf=0`** (best Sortino, most rows in top quartile). This is confirmed entry (2-bar crossunder + rising score) + stoch_is_peaking exit. Also eliminates `i_long_exit_activation_confirmation_threshold` as a free param.

1. Set both flags to `values: [true]` / `values: [false]` in all 20 params JSONs — **DONE 2026-03-22**
2. Lock `i_long_exit_activation_confirmation_threshold = values: [0]` (unused by selected variant) — pending
3. Net reduction: 3 free parameters (2 flags + 1 now-irrelevant threshold)

**Effort:** S — completed for flags; threshold lock pending
**Status:** Flags locked in all 20 params JSONs as of 2026-03-22

---

### Spec D: Indicator Redundancy Pruning

**Problem:** Several indicator groups share the same underlying source data, adding parameter dimensions without independent signal. Key redundant pairs:

**Group 1: M2/M3 family (5 indicators)**
- `i_w_m2_tiny`, `i_w_m2_div_osc`, `i_w_m2_div_osc_noOffset`, `i_w_m3_div_osc`, `i_w_m3_momentum`
- All derived from the same M2/M3 global liquidity data
- The `div_osc` and `div_osc_noOffset` variants are two normalisations of the same divergence

**Group 2: MVRV — complementary, NOT redundant**
- `i_w_mvrv` reads `mvrv_zscore_value` — a discrete 6-state signal `{-1, -0.5, -0.25, 0.25, 0.5, 1}` with hysteresis (only changes on MVRV band crossovers). Carries persistent regime-level state.
- `i_w_mvrv_cont` reads the raw `zscore` column, normalised to [-1, +1] around the 50th percentile. Carries exact position within bands at bar frequency.
- These are **complementary representations of the same underlying data**, not redundant. Discrete provides persistence and band-crossing signals; continuous provides magnitude and exact positioning.
- **Recommendation: keep both.** The original suggestion to lock one to 0 was incorrect.

**Group 3: Stochastic family (3–4 indicators)**
- `i_w_stoch`, `i_w_stoch_div_osc`, `i_w_stoch_peaking`, `i_w_stoch_bottoming`
- All from the same stochastic oscillator; the divergence, peak, and bottom signals are all derived quantities

**Group 4: Timescale mismatch**
- Fast signals (RSI, stoch, MACD) update meaningfully bar-to-bar
- Slow signals (M2, M3, DXY, MVRV, NUPL, Fed liquidity) update weekly/monthly but are interpolated to bar frequency
- At 4H timeframes, Fed liquidity or M2 slope is essentially flat for hundreds of bars — it reads as a constant offset, not a signal. The weight is absorbing a bias term, not a genuine dynamic signal.

**Recommendation — Ablation process:**
1. Use the Optuna importance reports already in `results/reports/optuna_importance_*.md` to rank indicators by contribution
2. For any indicator with importance < 1% across all assets/TFs AND sign stability showing near-50% in the DB: candidate for removal
3. For `m2_div_osc` / `m2_div_osc_noOffset` specifically: these are two normalisations of the same M2 divergence — keep only the one with better sign stability in the DB
4. For timescale-mismatched slow signals at sub-daily TFs: either (a) compute them on a weekly resampled series before merging, or (b) only include them in 1D+ TF params

**Note on mvrv/mvrv_cont:** These are complementary, not redundant (see Group 2 above). Keep both. Do NOT lock either to 0.

**Effort:** S for MVRV dedup; M for full ablation study
**Risk if skipped:** Each redundant pair wastes 2× parameter budget on one signal; slow signals at fast TFs may be absorbing biases that degrade OOS performance.

---

### Spec E: Improve DB Logging for Analysis

**Problem:** The current sweep DB saves only results with P&L/DD > 0, meaning:
- No convergence curve (score vs trial number) — don't know if search is converging
- No distribution of rejected candidates — can't tell how much of search space is "bad"
- No trial sequence number — can't replay or analyse search trajectory
- Weight correlation matrix across trials could reveal co-varying parameters, but only computed from winners (biased sample)

**Missing columns that would enable post-run analysis:**
- `trial_number` (sequential within run) — needed for convergence plots
- `iteration_number` (which of the 4 auto_optimize_loop iterations) — needed to see improvement across iterations
- `gpu_metric_score` (what the GPU ranked it by) — needed to understand GPU/CPU mismatch
- `is_biased_sample` (flag: was this drawn from biased ranges or pure random?) — needed to evaluate optuna effectiveness

**Recommendation:**
1. Add `trial_number` (sequential counter per run) and `iteration_number` to the schema. **No filter change needed** — just add to write_to_sweep_db().
2. Add `gpu_metric_score` to the verified result dict in `_verify_one()` (already partially present as `GPU_Score` field, just not written to DB).
3. Optionally: log a sample of rejected candidates (e.g., 1% of all GPU trials) to a separate `sweep_samples` table for distribution analysis. This table would have no P&L/DD filter — just raw score distributions. Very low write overhead at 1% sampling.

**Effort:** S (all metadata changes, 1 day)
**Risk if skipped:** Cannot analyse whether the search is converging, whether optuna sampling is actually concentrated in good regions, or whether the objective function has a healthy distribution of scores.

---

### Spec F: Regime Filter in GPU Kernel

**Problem:** The regime filter (`i_regime_window`, `i_regime_entry_min_score`) is a free parameter that only takes effect in CPU verification, not during the GPU scan. The GPU evaluates 50–180M candidates without applying this gate. The regime filter can only be optimised across the top 5000 CPU-verified candidates, not across the full search space.

**Current code:** `strategy_activation_scores.py:264-314` (CPU), GPU kernel at `strategy_activation_scores.py:606-778` (no regime filter).

**Recommendation:** Either:

*Option A (simplest):* Fix `i_regime_window = values: [0]` — disable the regime filter entirely. The current evidence from the marathon (where i_regime_window is a free param) shows it doesn't reliably help OOS. With only 5000 candidates evaluated, its optimisation is too noisy to be reliable. Free this parameter budget for something else.

*Option B (correct implementation):* Add rolling mean computation to the CUDA kernel. The kernel already loops over bars; adding a running mean accumulator is ~10 lines. Add regime_window and regime_min as 2 additional values in the `configs` array (currently N×2 for entry/exit thresholds; expand to N×4).

**Resolution:** Option A implemented — `i_regime_window = values: [0]` locked in all 20 params JSONs (2026-03-22). No prior OOS comparison of regime_window=0 vs >0 exists. Sign stability in DB shows these params have low stability (noise). Disabling frees 2 params and removes GPU/CPU discrepancy for regime params.

**Effort:** S — completed
**Status:** ✅ DONE (2026-03-22)

---

### Spec G: Hierarchical Regime Architecture (Phase 4 Path)

**Proposed architecture from the discussion:**
```
Layer 1 (Slow macro): DXY, M2, M3 → weekly/monthly regime score → {bull, neutral, bear}
    ↓
Layer 2 (Medium HMM): MVRV, realised vol, funding rates → daily regime classification
    ↓
Layer 3 (Fast weighted score): existing i_w_* system → entry/exit signals
```

**Feasibility assessment:**

*Layer 1 is feasible but requires a design decision:*
- M2 and M3 already exist as indicator columns in the CSV
- DXY already exists as `dxy_roc_norm`
- A "macro regime" could be computed as a simple score: weighted sum of these 3 → threshold
- The challenge: these update monthly. At 4H bars, the same regime label repeats for ~180 consecutive bars. An HMM or threshold classifier applied to bar-frequency data that changes monthly is mathematically a constant between change points — not a dynamic regime.
- **Correct approach:** Resample to weekly series, compute regime classification on weekly data, then forward-fill to bar frequency. This requires a small preprocessing step before `_prepare_features()`.

*Layer 2 (HMM) is feasible but more complex:*
- MVRV and NUPL are already in the CSV
- Realised volatility can be computed from OHLCV (no export needed)
- Funding rates: NOT currently in the CSV — would require a new TradingView export
- The HMM would need to be trained separately and its state exported to a column or computed in Python before the strategy runs
- A simpler alternative that doesn't require HMM: use a scored regime gate (e.g., MVRV Z-score > X → bull, < Y → bear) as a binary or ternary signal

*Layer 3 integration points in current code:*
- `strategy_activation_scores.py:264-314` — existing regime gate — could be replaced or augmented
- `_prepare_features()` — add regime_state column as an additional feature
- Alternatively: use separate `bull_params` / `bear_params` JSON files and select at runtime based on regime state

**Recommended phasing for Phase 4:**
1. Start with a simple binary macro filter (no HMM): if MVRV Z-score > threshold, use bull weights; otherwise suppress entries or use bear weights
2. Train bull params and bear params as separate optimisation runs on filtered IS data
3. Evaluate OOS with the filter applied; compare to unfiltered baseline
4. Only add HMM complexity if the simple filter shows OOS improvement

**What would change in the codebase:**
- `run_marathon.py` / `run_all_crypto.py`: add `--regime bull|bear|all` flag to pre-filter CSV rows
- `strategies/params/`: add `_bull.json` / `_bear.json` variants per combo
- `strategies/validate_strategy.py`: add runtime regime classification
- `strategies/strategy_activation_scores.py`: regime state lookup and param switching

**Effort:** M for simple binary filter; L for full HMM hierarchy
**Dependencies:** Spec A (phased optimisation) should be done first — training bull/bear params separately is itself a form of phased optimisation.

---

## Part 3 — Backlog Items

### [OPT-A] Phase the optimisation: weights first, thresholds second ✅ DONE 2026-03-22

**Problem it solves:** The current simultaneous search allows threshold values to absorb bar-specific noise, masking genuine signal weight discovery.

**Resolution:** Created `run_phased.py`.
- Phase 1: locks threshold params to range midpoints, searches all weights freely. Regime filter (`--regime bull/bear`) can be applied here for Phase 4a integration.
- Phase 2: takes phase 1 winner weights, locks all `i_w_*` to winner values, frees thresholds in a narrowed ±2-step range around winner. Uses optuna for low-dimensional threshold search.
- Added `--params` arg to `auto_optimize_loop.py` so custom phase-specific param files can be passed.

**Usage:**
```bash
python3 run_phased.py --data data/COINBASE_BTCUSD-1D.csv --hours 4      # 2h each phase
python3 run_phased.py --data data/COINBASE_ETHUSD-6H.csv --hours 4 --regime bull
```

**Effort:** M — completed

---

### [OPT-B] Switch primary IS metric from P&L/DD to Sortino

**Problem it solves:** GPU optimises Calmar/Sortino; CPU re-ranks by P&L/DD. Metric mismatch means GPU candidate selection is sub-optimal. P&L/DD has a cliff discontinuity at 40% DD that creates a false peak in the objective surface.

**Proposed solution:** Set `OPTIMIZATION_METRIC = "Sortino Ratio"` in `auto_optimize_loop.py`. Bump DB_SCHEMA_VER to 4. Keep P&L/DD stored for reference but not as the primary objective.

**Effort:** S

**Dependencies:** None, but bumping schema_ver requires updating all three files that declare it (`auto_optimize_loop.py`, `optimize_strategy.py`, `mine_sweep_db.py`).

**Risk if not addressed:** Wasted GPU cycles; best Sortino result ≠ best P&L/DD result; systematic under-selection of globally good candidates.

---

### [OPT-C] Fix strategy variant proliferation (confirmation flags)

**Problem it solves:** Two free binary flags create 4 co-optimised strategy variants, doubling overfitting surface area and making IS winners uninterpretable (which variant won?).

**Proposed solution:** Query DB to identify which variant (combination of flag values) has the best average OOS Sortino across all combos. Fix both flags to that variant; remove them from params JSON. This also removes or simplifies `i_long_exit_activation_confirmation_threshold`.

**Effort:** S (SQL query + JSON edit)

**Dependencies:** None.

**Risk if not addressed:** 75% of search time evaluates strategy variants that will ultimately be discarded; winners are unstable between runs because variant selection is noisy.

---

### [OPT-D] MVRV representation analysis (REVISED — not a dedup)

**Original assumption:** `i_w_mvrv` and `i_w_mvrv_cont` are the same signal — one should be locked to 0.

**Actual finding (confirmed by code inspection 2026-03-22):**
- `i_w_mvrv` → `mvrv_zscore_value` column: discrete 6-state `{-1, -0.5, -0.25, 0.25, 0.5, 1}`, only changes on MVRV band crossovers (hysteresis). Carries persistent regime state.
- `i_w_mvrv_cont` → `zscore` column: raw continuous MVRV Z-score normalised to [-1, +1] around median. Bar-frequency exact position.
- These are **complementary**: discrete = band-level regime state, continuous = exact magnitude. Having both is useful.

**Revised recommendation:** Keep both. Run importance analysis via optuna reports to see which has higher contribution, but do not lock either to 0.

**Effort:** None required — no code change needed.

**Risk:** None — original "risk" was based on incorrect assumption of redundancy.

---

### [OPT-E] Ablation study: prune low-contribution indicators ✅ DONE 2026-03-22

**Problem it solves:** ~5 M2/M3 family signals and ~4 stochastic family signals likely have high inter-correlation. Keeping redundant signals wastes parameter budget and increases overfitting surface area.

**Resolution (sign stability analysis on 56,144 top-quartile v3 DB rows):**

Per-asset sign stability results:

| Param | LINKUSD | SOLUSD | BTCUSD | ETHUSD | ALL | Action |
|---|---|---|---|---|---|---|
| i_w_rsid_osc | 14% | 25% | 13% | 18% | 16% | Lock to 0 (all 20) |
| i_w_spy | 63% | 79% | 92% | 83% | 85% | Lock positive start=5 (all 20) |
| i_w_nupl | 47% | 54% | 50% | 41% | 49% | Lock to 0 (pure noise) |
| i_w_stoch_bottoming | 46% | 46% | 53% | 47% | 49% | Lock to 0 (pure noise) |
| i_w_stoch_peaking | 46% | 52% | 39% | 45% | 43% | Lock to 0 (noisy) |
| i_w_btc_dom | 0% | 0% | 100% | 0% | 56% | BTC=positive, alts=0 (BTC 4H was broken — fixed) |
| i_w_osc | 80% | 69% | 73% | 70% | 73% | Lock positive start=0 (was wrongly 0 in BTC 1D/4H/8H) |

All locks applied to all 20 combo params JSONs via script. Notable corrections:
- `i_w_btc_dom` BTC 4H: was `values:[0]` (wrong) → now `start=5, stop=100, step=5` (positive like other BTC TFs)
- `i_w_osc` BTC 1D/4H/8H: was `values:[0]` (wrongly locked) → now `start=0, stop=100, step=5`
- `i_w_rsid_osc`: previously locked only in BTC 8H — now locked in all 20 combos
- `i_w_spy`: was start=-60 → now start=5 in all 20 combos

**Effort:** M — completed

---

### [OPT-F] Add trial metadata to sweep DB (convergence logging) ✅ DONE 2026-03-22

**Resolution:** Added `iteration_number INTEGER` and `gpu_score REAL` to DB schema. `write_to_sweep_db()` now accepts and stores these. `verify_top_results()` passes `iteration_number=i` from main loop. No schema version bump required — schema migration auto-adds columns to existing DBs.

**Not implemented:** `is_biased_sample` (requires changes inside optimize_strategy.py's sampling loop) — deferred, lower priority than iteration tracking.

---

### [ARCH-A] Add regime filter to GPU kernel (or disable) ✅ DONE (2026-03-22)

**Problem it solves:** `i_regime_window` and `i_regime_entry_min_score` are free params that only fire during CPU verification (top 5000 candidates), not during the GPU scan.

**Resolution:** Disabled — `i_regime_window = values: [0]` locked in all 20 params JSONs. No prior OOS evidence that regime filter helps; sign stability shows it fits noise. Will revisit when Phase 4 (MVRV-based regime suppression) is implemented properly.

**Effort:** S — completed

---

### [ARCH-B] Phase 4: Simple binary macro regime filter ✅ DONE 2026-03-22 (training infrastructure)

**Problem it solves:** Strategy is trained on 2017–2024 data mixing bull and bear periods. Separate param sets per regime may generalise better.

**Owner clarification:** Phase 4 = suppress entries in bear regime (not add short entries).

**Important MVRV note:** The `zscore` column (0–100 percentile scale) in the CSV shows:
- True bear markets: 2018 (median 9.9), 2022 (median 6.6) — clearly below threshold=15
- **2025: median 28.2, min 17.5 — NOT a MVRV bear market by this measure**
- Conclusion: ARCH-B training would have helped for 2018/2022 OOS. The 2025 OOS underperformance has a different cause (possibly SPY/macro correlation change or BTC-specific dynamics post-halving). ARCH-B is still worth running but won't retroactively explain 2025.

**What was implemented:**
1. `auto_optimize_loop.py --regime bull/bear --regime-threshold 15.0` — pre-filters IS training CSV to regime-matched bars before passing to optimize_strategy.py subprocess
2. `run_marathon.py --regime bull/bear --regime-threshold 15.0` — passes regime filter to all combos
3. `run_phased.py --regime bull/bear` — applies regime filter to Phase 1 (weight search on regime data)

**What remains (runtime switching — ARCH-C territory):**
- `validate_strategy.py`: add runtime MVRV regime classification per bar
- Switch to bull/bear params file at runtime based on current regime state

**Usage (training separate param sets):**
```bash
# Train bull params for all combos (zscore >= 15)
python3 run_marathon.py --hours 48 --regime bull
# Train bear params for all combos (zscore < 15)
python3 run_marathon.py --hours 20 --regime bear   # fewer bear bars → shorter run
```

**Effort:** M for training infra — completed; L for runtime switching — pending [ARCH-C]

**Dependencies:** [OPT-A] now done; run_phased.py + --regime can be used together for cleanest approach.

---

### [ARCH-C] Phase 4b: Dual-weight system with runtime switching ✅ PARTIAL 2026-03-22

**Problem it solves:** Bull and bear regimes likely reward different signals (trend-following in bull, mean-reversion or suppression in bear). Single weight set cannot be optimal for both.

**What was implemented (runtime entry suppression):**
Added `--regime-suppress {bull,bear,none}` and `--regime-threshold FLOAT` args to `validate_strategy.py`.
After `generate_signals()`, bars where the regime doesn't match have `execute_entry` zeroed out.
Uses the `zscore` column (0–100 percentile MVRV scale) already present in the data CSVs.

```bash
# Only enter when MVRV zscore >= 15 (bull bars):
python strategies/validate_strategy.py --data data/COINBASE_BTCUSD-1D.csv \
  --params_file results/winners/optimization_winner_activation_scores_COINBASE_BTCUSD_1D.csv \
  --regime-suppress bull --regime-threshold 15

# Only enter when MVRV zscore < 15 (bear bars):
python strategies/validate_strategy.py --data data/COINBASE_BTCUSD-1D.csv \
  --params_file results/winners/optimization_winner_activation_scores_COINBASE_BTCUSD_1D.csv \
  --regime-suppress bear --regime-threshold 15
```

**What remains (full dual-weight system — deferred):**
- Train separate bull/bear winner CSVs via `run_phased.py --regime bull/bear`
- Load both CSVs; apply per-bar regime classifier; switch weight sets dynamically
- This requires [ARCH-B] to first demonstrate OOS benefit from regime-aware training

**Dependencies:** [ARCH-B] must show OOS benefit before warranting the dual-weight investment.

**Effort:** L total; S partial (done) + M remaining (deferred)

---

## Part 4 — Quick Wins

These can be implemented before or during the next marathon run with minimal risk:

### QW-1: Switch objective to Sortino [OPT-B] ✅ DONE 2026-03-22
`OPTIMIZATION_METRIC = "Sortino Ratio"` in `auto_optimize_loop.py`. `MIN_SCORABLE_TRADES` raised 10→30 in `strategy_activation_scores.py`. Schema version NOT bumped — existing rows have sortino_ratio column populated.

### QW-2: Fix confirmation flags [OPT-C] ✅ DONE 2026-03-22
DB query showed `entry_conf=1, exit_conf=0` has best avg Sortino (2.202, 16,811 top-quartile rows). Both flags locked in all 20 params JSONs. Pending: lock `i_long_exit_activation_confirmation_threshold = values: [0]` (unused by selected variant).

### QW-3: Lock mvrv_cont or mvrv [OPT-D] ✅ N/A — NOT a dedup
Investigation confirmed `i_w_mvrv` and `i_w_mvrv_cont` are **complementary**: discrete 6-state band signal vs continuous z-score magnitude. Keep both. See OPT-D for details.

### QW-4: Add iteration_number and gpu_score to DB schema [OPT-F] ✅ DONE 2026-03-22
Added `iteration_number INTEGER` and `gpu_score REAL` to `init_sweep_db()` CREATE TABLE. Schema migration auto-adds them to existing DBs. `write_to_sweep_db()` now accepts `iteration_number` and stores `GPU_Score` from verified results. Main loop passes `iteration_number=i`. Enables convergence analysis: query `SELECT iteration_number, AVG(sortino_ratio) FROM sweep_results WHERE run_id=? GROUP BY iteration_number`.

### QW-5: Disable regime filter for next marathon [ARCH-A] ✅ DONE 2026-03-22
`i_regime_window = values: [0]` locked in all 20 params JSONs. Also locked `i_w_fed_net_liq = values: [0.0]` (FRED data requires paid TradingView plan; outputs 0.0 in current exports).

---

## Summary Table

| Item | Category | Effort | Priority | Status |
|---|---|---|---|---|
| QW-1: Switch to Sortino | Objective function | S | ~~Now~~ | ✅ Done 2026-03-22 |
| QW-2: Fix confirmation flags | Strategy variants | S | ~~Now~~ | ✅ Done 2026-03-22 |
| QW-3: Dedup mvrv/mvrv_cont | Indicator redundancy | — | ~~Now~~ | ✅ N/A — not a dedup |
| QW-4: Add trial metadata to DB | Observability | S | ~~Before next marathon~~ | ✅ Done 2026-03-22 |
| QW-5: Disable regime filter + fed_net_liq | Architecture | S | ~~Now~~ | ✅ Done 2026-03-22 |
| [OPT-A] Phased optimisation | Architecture | M | ~~High~~ | ✅ Done 2026-03-22 (run_phased.py) |
| [OPT-E] Ablation study | Indicator pruning | M | Medium | ✅ Done 2026-03-22 — locks applied to all 20 combos |
| [ARCH-B] Phase 4a regime training infra | Regime | M | ~~High~~ | ✅ Done 2026-03-22 (--regime flag) |
| [ARCH-C] Phase 4b runtime regime switching | Regime | L | Medium | ✅ Partial 2026-03-22 — entry suppression done; dual-weight deferred (needs ARCH-B OOS proof) |

---

## Open Questions — RESOLVED 2026-03-22

| # | Question | Resolution |
|---|---|---|
| ✅1 | Why was P&L/DD chosen over Sortino? | Metric evolved Sortino→Calmar→P&L/DD to avoid few-trade gaming. Root cause was MIN_SCORABLE_TRADES=10 being too low. Fixed: MIN_SCORABLE_TRADES=30, switched back to Sortino. |
| ✅2 | Is `i_w_mvrv_cont` reading different data from `i_w_mvrv`? | YES — different data. `i_w_mvrv` = discrete 6-state band signal with hysteresis; `i_w_mvrv_cont` = raw continuous z-score. Complementary, not redundant. Keep both. |
| ✅3 | Any prior OOS comparison of regime_window=0 vs >0? | No prior comparison exists. Disabled regime filter (locked to 0) — will revisit if Phase 4 needs a bar-level gate. |
| ✅4 | Will `fed_net_liq` be populated in next TV re-export? | No — Pine script uses `FRED:WALCL`, `FRED:WTREGEN`, `FRED:RRPONTSYD` which require paid TradingView plan. Returns NA on current plan. Locked `i_w_fed_net_liq = values: [0.0]` in all params JSONs until TV plan upgraded. |
| ✅5 | Phase 4: suppress entries or add short entries in bear regime? | **Suppress entries only** (not short). Confirmed by owner. Simplest implementation: MVRV Z-score threshold → bull/bear classification → gate all new longs when bearish. |

---

_Last updated: 2026-03-22 (session 9 — QW-4, OPT-A (run_phased.py), ARCH-B training infra implemented)_
