Refine Phase 2 portfolio: data-driven filters, drop S4F and S9

S7: add RSI floor (RSI<40 = 0% WR) and ATR percentile cap (high-vol
regime = worse RR). IS flips from PF 0.68 to 1.52, OOS holds at 1.80.

S3: add confluence gate (C>=4) and skip hours 09-10 (0% WR). IS PF
1.06 -> 1.22, OOS PF 1.07 -> 1.23.

S9_Filtered: add skip_monday (unreliable Asian ranges after weekend
gaps). IS PF 1.10 -> 1.31, OOS holds strong at 2.26.

Drop S9/GBP_USD (negative PF across all param combos) and S4F/EUR_AUD
(overfit: IS 1.43 collapses to OOS 0.48). 3-strategy portfolio: all
PASS generalization, IS PF 1.29, OOS PF 1.55, Gen 1.46.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Brent Neale
2026-02-20 10:23:59 +10:00
parent 4f911b2072
commit 9c5d55ec46
11 changed files with 328 additions and 456 deletions
+2 -4
View File
@@ -40,15 +40,13 @@ OOS_END = "2023-08-31"
WARMUP_DAYS = 60
# Phase 2 strategy-pair configurations
# S9/GBP_USD dropped: negative PF across all param combos on IS (best 0.86)
# S4F/EUR_AUD dropped: classic overfit — IS PF 1.43 collapses to OOS PF 0.48
CONFIGS = [
{"name": "S7_Tight", "pair": "GBP_JPY", "tf": "H1", "htf_tf": "H1",
"factory": lambda: S7_Liquidity_Sweep()},
{"name": "S9", "pair": "GBP_USD", "tf": "H1", "htf_tf": "H1",
"factory": lambda: S9_London_Session()},
{"name": "S9_Filtered", "pair": "GBP_AUD", "tf": "H1", "htf_tf": "H1",
"factory": lambda: S9_London_Session(pair="GBP_AUD", filtered=True)},
{"name": "S4F", "pair": "EUR_AUD", "tf": "M15", "htf_tf": "H1",
"factory": lambda: S4F_EMA_Ribbon()},
{"name": "S3", "pair": "GBP_JPY", "tf": "H1", "htf_tf": "H1",
"factory": lambda: S3_KeyLevel_Breakout()},
]
@@ -36,6 +36,11 @@ class S3_KeyLevel_Breakout(BaseStrategy):
KEY_LEVEL_TOLERANCE = 0.75
KEY_LEVEL_MIN_TOUCHES = 3
# Refinement filters (data-driven from IS/OOS trade analysis)
MIN_CONFLUENCE = 4 # C<4 loses money in both IS and OOS
SKIP_HOURS = (9, 10) # 0% WR in IS; avoid early London before NY flow
SKIP_DAYS = () # disabled — removing 2/5 days too aggressive
def __init__(self):
super().__init__()
self._cached_levels = None
@@ -52,6 +57,16 @@ class S3_KeyLevel_Breakout(BaseStrategy):
if hour < 8 or hour >= 16:
return None
# Skip underperforming hours (morning London before NY overlap)
if self.SKIP_HOURS and hour in self.SKIP_HOURS:
return None
# Skip underperforming days of week
if self.SKIP_DAYS:
dow = current.name.dayofweek if hasattr(current.name, 'dayofweek') else 0
if dow in self.SKIP_DAYS:
return None
atr_val = current.get("atr_14", 0)
if atr_val <= 0 or np.isnan(atr_val):
return None
@@ -110,6 +125,8 @@ class S3_KeyLevel_Breakout(BaseStrategy):
confluence = self._calc_confluence(current, data, idx,
"LONG", touch_count, vol)
if confluence < self.MIN_CONFLUENCE:
continue
sl = level_price - self.SL_ATR_MULT * atr_val
tp1 = close + self.TP1_ATR_MULT * atr_val
@@ -139,6 +156,8 @@ class S3_KeyLevel_Breakout(BaseStrategy):
confluence = self._calc_confluence(current, data, idx,
"SHORT", touch_count, vol)
if confluence < self.MIN_CONFLUENCE:
continue
sl = level_price + self.SL_ATR_MULT * atr_val
tp1 = close - self.TP1_ATR_MULT * atr_val
+17
View File
@@ -43,6 +43,10 @@ class S7_Liquidity_Sweep(BaseStrategy):
OBV_LOOKBACK = 20 # Lookback for OBV divergence detection
MAX_BARS = 40
# Regime filters (data-driven: consistent losers across IS + OOS)
MIN_RSI = 40 # RSI<40 = 0% WR in 2021, 33% OOS — no reversal fuel
MAX_ATR_PERCENTILE = 50 # above-median ATR loses in every period (wider SL, worse RR)
def _find_swing_levels(self, data, idx):
"""Find significant swing highs and lows within lookback window."""
start = max(0, idx - self.SWING_HISTORY)
@@ -150,6 +154,15 @@ class S7_Liquidity_Sweep(BaseStrategy):
if atr_val <= 0 or np.isnan(atr_val):
return None
# ATR regime filter: skip high-volatility environments (worse RR)
if self.MAX_ATR_PERCENTILE < 100:
lookback_start = max(0, idx - 100)
atr_window = data["atr_14"].iloc[lookback_start:idx].dropna()
if len(atr_window) >= 20:
threshold = np.percentile(atr_window, self.MAX_ATR_PERCENTILE)
if atr_val > threshold:
return None
# HTF trend alignment
if htf_row is None:
return None
@@ -170,6 +183,10 @@ class S7_Liquidity_Sweep(BaseStrategy):
if np.isnan(rsi_val):
rsi_val = 50
# RSI floor filter: RSI<40 = no reversal fuel (0% WR in 2021, 33% OOS)
if rsi_val < self.MIN_RSI:
return None
# Find swing levels
swing_highs, swing_lows = self._find_swing_levels(data, idx)
+16 -5
View File
@@ -71,6 +71,7 @@ class S9_London_Session(BaseStrategy):
"GBP_AUD": {
"min_adx": 25, # require ADX > 25
"skip_friday": True, # drop Friday trades
"skip_monday": True, # Monday Asian ranges unreliable after weekend gaps
"min_ema50_dist_pips": 40, # require 40+ pips from EMA50
},
}
@@ -131,11 +132,12 @@ class S9_London_Session(BaseStrategy):
if hour < start_hour or hour >= self.ENTRY_END_HOUR:
return None
# Friday filter (GBP_AUD: Friday position squaring kills breakouts)
if self._pair_cfg.get("skip_friday", False):
dow = current.name.dayofweek if hasattr(current.name, 'dayofweek') else 0
if dow == 4: # Friday
return None
# Day-of-week filters
dow = current.name.dayofweek if hasattr(current.name, 'dayofweek') else 0
if self._pair_cfg.get("skip_friday", False) and dow == 4:
return None
if self._pair_cfg.get("skip_monday", False) and dow == 0:
return None
atr_val = current.get("atr_14", 0)
if atr_val <= 0 or np.isnan(atr_val):
@@ -212,6 +214,15 @@ class S9_London_Session(BaseStrategy):
if direction is None:
return None
# RSI directional filter: skip trades against RSI extremes
if self._pair_cfg.get("rsi_directional_filter", False):
rsi_val = current.get("rsi_14", 50)
if not np.isnan(rsi_val):
if direction == "LONG" and rsi_val > 60:
return None
if direction == "SHORT" and rsi_val < 40:
return None
# HTF trend alignment (soft: adds confluence but doesn't block)
htf_aligned = False
if htf_row is not None: