mirror of
https://github.com/BrentNeale1/fx-quant.git
synced 2026-08-17 03:58:07 +00:00
Add Phase 2 backtesting pipeline: IS/OOS split, param sweep, generalization scoring
Externalize hardcoded params in S4F (5 params) and S3 (9 params) as class attributes for sweep compatibility. Add unified backtest runner with IS/OOS validation and generalization scores, plus parameter grid sweep (90 combos) with OOS validation. S7/S9/S9_Filtered pass generalization; S4F/S3 confirm defaults are near-optimal. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
9d16939eaa
commit
4f911b2072
@@ -25,6 +25,17 @@ class S3_KeyLevel_Breakout(BaseStrategy):
|
||||
strategy_id = 3
|
||||
name = "S3_Key_Level_Breakout"
|
||||
|
||||
# Tunable parameters (defaults match original hardcoded values)
|
||||
BODY_RATIO_MIN = 0.50
|
||||
VOLUME_MULT = 1.5
|
||||
SL_ATR_MULT = 0.5
|
||||
TP1_ATR_MULT = 1.5
|
||||
TP2_ATR_MULT = 2.5
|
||||
TP3_ATR_MULT = 4.0
|
||||
MIN_ADX = 20
|
||||
KEY_LEVEL_TOLERANCE = 0.75
|
||||
KEY_LEVEL_MIN_TOUCHES = 3
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._cached_levels = None
|
||||
@@ -47,21 +58,21 @@ class S3_KeyLevel_Breakout(BaseStrategy):
|
||||
|
||||
# ADX filter: require trending market
|
||||
adx_val = current.get("adx_14", 0)
|
||||
if adx_val < 20:
|
||||
if adx_val < self.MIN_ADX:
|
||||
return None
|
||||
|
||||
# Strong close: candle body > 50% of range
|
||||
close = current["close"]
|
||||
body = abs(close - current["open"])
|
||||
full_range = current["high"] - current["low"]
|
||||
if full_range <= 0 or body / full_range < 0.50:
|
||||
if full_range <= 0 or body / full_range < self.BODY_RATIO_MIN:
|
||||
return None
|
||||
|
||||
# Volume spike: current volume > 1.5x 20-bar average
|
||||
vol = current.get("volume", 0)
|
||||
if vol > 0 and idx >= 20:
|
||||
vol_avg = data["volume"].iloc[idx - 20:idx].mean()
|
||||
if vol_avg > 0 and vol < 1.5 * vol_avg:
|
||||
if vol_avg > 0 and vol < self.VOLUME_MULT * vol_avg:
|
||||
return None
|
||||
|
||||
prev_close = data.iloc[idx - 1]["close"]
|
||||
@@ -74,7 +85,9 @@ class S3_KeyLevel_Breakout(BaseStrategy):
|
||||
start = max(0, idx - 1000)
|
||||
window = data.iloc[start:idx] # exclude current bar
|
||||
self._cached_levels = identify_key_levels(
|
||||
window, lookback=5, tolerance_atr_mult=0.75, min_touches=3
|
||||
window, lookback=5,
|
||||
tolerance_atr_mult=self.KEY_LEVEL_TOLERANCE,
|
||||
min_touches=self.KEY_LEVEL_MIN_TOUCHES,
|
||||
)
|
||||
self._cache_idx = idx
|
||||
|
||||
@@ -98,10 +111,10 @@ class S3_KeyLevel_Breakout(BaseStrategy):
|
||||
confluence = self._calc_confluence(current, data, idx,
|
||||
"LONG", touch_count, vol)
|
||||
|
||||
sl = level_price - 0.5 * atr_val
|
||||
tp1 = close + 1.5 * atr_val
|
||||
tp2 = close + 2.5 * atr_val
|
||||
tp3 = close + 4.0 * atr_val
|
||||
sl = level_price - self.SL_ATR_MULT * atr_val
|
||||
tp1 = close + self.TP1_ATR_MULT * atr_val
|
||||
tp2 = close + self.TP2_ATR_MULT * atr_val
|
||||
tp3 = close + self.TP3_ATR_MULT * atr_val
|
||||
|
||||
return {
|
||||
"direction": "LONG",
|
||||
@@ -127,10 +140,10 @@ class S3_KeyLevel_Breakout(BaseStrategy):
|
||||
confluence = self._calc_confluence(current, data, idx,
|
||||
"SHORT", touch_count, vol)
|
||||
|
||||
sl = level_price + 0.5 * atr_val
|
||||
tp1 = close - 1.5 * atr_val
|
||||
tp2 = close - 2.5 * atr_val
|
||||
tp3 = close - 4.0 * atr_val
|
||||
sl = level_price + self.SL_ATR_MULT * atr_val
|
||||
tp1 = close - self.TP1_ATR_MULT * atr_val
|
||||
tp2 = close - self.TP2_ATR_MULT * atr_val
|
||||
tp3 = close - self.TP3_ATR_MULT * atr_val
|
||||
|
||||
return {
|
||||
"direction": "SHORT",
|
||||
|
||||
@@ -21,6 +21,13 @@ class S4F_EMA_Ribbon(BaseStrategy):
|
||||
strategy_id = 4
|
||||
name = "S4F_Trend_Context"
|
||||
|
||||
# Tunable parameters (defaults match original hardcoded values)
|
||||
SL_ATR_MULT = 2.0
|
||||
TP_ATR_MULT = 3.0
|
||||
VOLUME_MULT = 1.2
|
||||
COMPRESSION_ATR_MULT = 1.0
|
||||
HTF_EMA_DIST_ATR = 1.5
|
||||
|
||||
def check_signal(self, data: pd.DataFrame, idx: int,
|
||||
current: pd.Series,
|
||||
htf_row: Optional[pd.Series] = None) -> Optional[dict]:
|
||||
@@ -67,7 +74,7 @@ class S4F_EMA_Ribbon(BaseStrategy):
|
||||
|
||||
# ------- PRICE WITHIN 1.5 ATR OF 1H 50 EMA -------
|
||||
price = current["close"]
|
||||
if abs(price - htf_ema50) > 1.5 * atr_val:
|
||||
if abs(price - htf_ema50) > self.HTF_EMA_DIST_ATR * atr_val:
|
||||
return None
|
||||
|
||||
# ------- M15 RIBBON COMPRESSION -> EXPANSION -------
|
||||
@@ -78,7 +85,7 @@ class S4F_EMA_Ribbon(BaseStrategy):
|
||||
return None
|
||||
|
||||
ribbon_width = max(ema_20, ema_50, ema_100) - min(ema_20, ema_50, ema_100)
|
||||
compression_threshold = 1.0 * atr_val
|
||||
compression_threshold = self.COMPRESSION_ATR_MULT * atr_val
|
||||
|
||||
was_compressed = False
|
||||
min_compression_width = float('inf')
|
||||
@@ -119,16 +126,16 @@ class S4F_EMA_Ribbon(BaseStrategy):
|
||||
return None
|
||||
vol = current["volume"]
|
||||
vol_avg = data["volume"].iloc[max(0, idx - 20):idx].mean()
|
||||
if vol_avg <= 0 or vol <= 1.2 * vol_avg:
|
||||
if vol_avg <= 0 or vol <= self.VOLUME_MULT * vol_avg:
|
||||
return None
|
||||
|
||||
# ------- EXIT LEVELS -------
|
||||
if direction == "LONG":
|
||||
sl = price - 2.0 * atr_val
|
||||
tp1 = price + 3.0 * atr_val
|
||||
sl = price - self.SL_ATR_MULT * atr_val
|
||||
tp1 = price + self.TP_ATR_MULT * atr_val
|
||||
else:
|
||||
sl = price + 2.0 * atr_val
|
||||
tp1 = price - 3.0 * atr_val
|
||||
sl = price + self.SL_ATR_MULT * atr_val
|
||||
tp1 = price - self.TP_ATR_MULT * atr_val
|
||||
|
||||
return {
|
||||
"direction": direction,
|
||||
|
||||
Reference in New Issue
Block a user