mirror of
https://github.com/BrentNeale1/fx-quant.git
synced 2026-08-06 15:17:45 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user