mirror of
https://github.com/BrentNeale1/fx-quant.git
synced 2026-08-18 12:38:06 +00:00
Add S10 VWAP Mean Reversion and S11 ADX Trend Pullback strategies, drop both from portfolio
Implemented two M15 intraday strategies to diversify the portfolio: - S10: VWAP mean reversion in ranging markets (ADX<30, RSI(9) extremes) - S11: ADX trend pullback to 20 EMA in strong trends (ADX>30, rising) Added rsi_9 and atr_10 to the indicator pipeline for both strategies. Backtested on IS (2021-2022) and OOS (2023): both strategies produced insufficient trade counts on M15 and failed generalization. S10 best result was EUR_GBP at Gen 0.65 (WARN). S11 collapsed to 0% WR OOS across all param sweep combos. Both dropped from active portfolio — 3-strategy core (S7_Tight, S9_Filtered, S3) remains unchanged. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
410a10acb6
commit
bdab78ee48
@@ -95,32 +95,128 @@
|
|||||||
"verdict": "PASS"
|
"verdict": "PASS"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"_portfolio": {
|
"S10_EUR_USD": {
|
||||||
|
"pair": "EUR_USD",
|
||||||
|
"timeframe": "M15",
|
||||||
"is_metrics": {
|
"is_metrics": {
|
||||||
"trades": 149,
|
"trades": 13,
|
||||||
"wr": 57.7,
|
"wr": 53.8,
|
||||||
"pf": 1.29,
|
"pf": 1.67,
|
||||||
"sharpe": 1.68,
|
"sharpe": 3.17,
|
||||||
"pnl_pips": 865.8,
|
"pnl_pips": 20.1,
|
||||||
"max_dd_pips": -597.5,
|
"max_dd_pips": -13.1,
|
||||||
"expectancy": 5.81
|
"expectancy": 1.55
|
||||||
},
|
},
|
||||||
"oos_metrics": {
|
"oos_metrics": {
|
||||||
"trades": 56,
|
"trades": 6,
|
||||||
"wr": 64.3,
|
"wr": 33.3,
|
||||||
"pf": 1.55,
|
"pf": 1.19,
|
||||||
"sharpe": 3.03,
|
"sharpe": 0.97,
|
||||||
"pnl_pips": 553.3,
|
"pnl_pips": 1.5,
|
||||||
"max_dd_pips": -182.9,
|
"max_dd_pips": -7.9,
|
||||||
"expectancy": 9.88
|
"expectancy": 0.25
|
||||||
},
|
},
|
||||||
"generalization": {
|
"generalization": {
|
||||||
"composite": 1.455,
|
"composite": 0.45,
|
||||||
"detail": {
|
"detail": {
|
||||||
"wr": 1.114,
|
"wr": 0.619,
|
||||||
"pf": 1.202,
|
"pf": 0.713,
|
||||||
"expectancy": 1.701,
|
"expectancy": 0.161,
|
||||||
"sharpe": 1.804
|
"sharpe": 0.306
|
||||||
|
},
|
||||||
|
"verdict": "FAIL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"S10_USD_JPY": {
|
||||||
|
"pair": "USD_JPY",
|
||||||
|
"timeframe": "M15",
|
||||||
|
"is_metrics": {
|
||||||
|
"trades": 20,
|
||||||
|
"wr": 50.0,
|
||||||
|
"pf": 2.08,
|
||||||
|
"sharpe": 4.18,
|
||||||
|
"pnl_pips": 51.5,
|
||||||
|
"max_dd_pips": -19.3,
|
||||||
|
"expectancy": 2.58
|
||||||
|
},
|
||||||
|
"oos_metrics": {
|
||||||
|
"trades": 9,
|
||||||
|
"wr": 33.3,
|
||||||
|
"pf": 0.76,
|
||||||
|
"sharpe": -1.48,
|
||||||
|
"pnl_pips": -10.1,
|
||||||
|
"max_dd_pips": -9.8,
|
||||||
|
"expectancy": -1.13
|
||||||
|
},
|
||||||
|
"generalization": {
|
||||||
|
"composite": 0.258,
|
||||||
|
"detail": {
|
||||||
|
"wr": 0.666,
|
||||||
|
"pf": 0.365,
|
||||||
|
"expectancy": 0.0,
|
||||||
|
"sharpe": 0.0
|
||||||
|
},
|
||||||
|
"verdict": "FAIL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"S10_EUR_GBP": {
|
||||||
|
"pair": "EUR_GBP",
|
||||||
|
"timeframe": "M15",
|
||||||
|
"is_metrics": {
|
||||||
|
"trades": 15,
|
||||||
|
"wr": 33.3,
|
||||||
|
"pf": 0.98,
|
||||||
|
"sharpe": -0.14,
|
||||||
|
"pnl_pips": -0.8,
|
||||||
|
"max_dd_pips": -29.2,
|
||||||
|
"expectancy": -0.06
|
||||||
|
},
|
||||||
|
"oos_metrics": {
|
||||||
|
"trades": 7,
|
||||||
|
"wr": 42.9,
|
||||||
|
"pf": 1.28,
|
||||||
|
"sharpe": 1.61,
|
||||||
|
"pnl_pips": 3.5,
|
||||||
|
"max_dd_pips": -9.4,
|
||||||
|
"expectancy": 0.49
|
||||||
|
},
|
||||||
|
"generalization": {
|
||||||
|
"composite": 0.649,
|
||||||
|
"detail": {
|
||||||
|
"wr": 1.288,
|
||||||
|
"pf": 1.306,
|
||||||
|
"expectancy": 0,
|
||||||
|
"sharpe": 0
|
||||||
|
},
|
||||||
|
"verdict": "WARN"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_portfolio": {
|
||||||
|
"is_metrics": {
|
||||||
|
"trades": 197,
|
||||||
|
"wr": 54.8,
|
||||||
|
"pf": 1.3,
|
||||||
|
"sharpe": 1.58,
|
||||||
|
"pnl_pips": 936.6,
|
||||||
|
"max_dd_pips": -597.5,
|
||||||
|
"expectancy": 4.75
|
||||||
|
},
|
||||||
|
"oos_metrics": {
|
||||||
|
"trades": 78,
|
||||||
|
"wr": 56.4,
|
||||||
|
"pf": 1.51,
|
||||||
|
"sharpe": 2.52,
|
||||||
|
"pnl_pips": 548.2,
|
||||||
|
"max_dd_pips": -208.7,
|
||||||
|
"expectancy": 7.03
|
||||||
|
},
|
||||||
|
"generalization": {
|
||||||
|
"composite": 1.316,
|
||||||
|
"detail": {
|
||||||
|
"wr": 1.029,
|
||||||
|
"pf": 1.162,
|
||||||
|
"expectancy": 1.48,
|
||||||
|
"sharpe": 1.595
|
||||||
},
|
},
|
||||||
"verdict": "PASS"
|
"verdict": "PASS"
|
||||||
}
|
}
|
||||||
|
|||||||
+951
-1143
File diff suppressed because it is too large
Load Diff
@@ -386,9 +386,11 @@ def compute_all_indicators(df: pd.DataFrame) -> pd.DataFrame:
|
|||||||
|
|
||||||
# RSI
|
# RSI
|
||||||
df["rsi_14"] = rsi(df["close"], 14)
|
df["rsi_14"] = rsi(df["close"], 14)
|
||||||
|
df["rsi_9"] = rsi(df["close"], 9)
|
||||||
|
|
||||||
# ATR
|
# ATR
|
||||||
df["atr_14"] = atr(df, 14)
|
df["atr_14"] = atr(df, 14)
|
||||||
|
df["atr_10"] = atr(df, 10)
|
||||||
|
|
||||||
# MACD
|
# MACD
|
||||||
df["macd"], df["macd_signal"], df["macd_hist"] = macd(df["close"])
|
df["macd"], df["macd_signal"], df["macd_hist"] = macd(df["close"])
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ SWEEP_CONFIGS = [
|
|||||||
"KEY_LEVEL_TOLERANCE": [0.5, 0.75],
|
"KEY_LEVEL_TOLERANCE": [0.5, 0.75],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
# S10 dropped: insufficient trade counts on M15, no pair passes generalization
|
||||||
|
# S11 dropped: OOS collapsed across all param combos (0% WR, Gen=0.00)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ CONFIGS = [
|
|||||||
"factory": lambda: S9_London_Session(pair="GBP_AUD", filtered=True)},
|
"factory": lambda: S9_London_Session(pair="GBP_AUD", filtered=True)},
|
||||||
{"name": "S3", "pair": "GBP_JPY", "tf": "H1", "htf_tf": "H1",
|
{"name": "S3", "pair": "GBP_JPY", "tf": "H1", "htf_tf": "H1",
|
||||||
"factory": lambda: S3_KeyLevel_Breakout()},
|
"factory": lambda: S3_KeyLevel_Breakout()},
|
||||||
|
# S10 dropped: insufficient trade counts on M15, no pair passes generalization
|
||||||
|
# S11 dropped: OOS collapsed across all param combos (0% WR, Gen=0.00)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
"""
|
||||||
|
Strategy S10: VWAP Mean Reversion (M15).
|
||||||
|
|
||||||
|
Concept: Fade extreme deviations from session VWAP in ranging markets.
|
||||||
|
Price stretched beyond 2 SD from VWAP with RSI(9) at extremes signals
|
||||||
|
mean reversion back to VWAP.
|
||||||
|
|
||||||
|
Entry conditions (ALL must be true):
|
||||||
|
1. Session filter: 08:00-16:00 UTC
|
||||||
|
2. Ranging regime: ADX(14) < 25
|
||||||
|
3. Price >= 2 SD from session VWAP (z-score)
|
||||||
|
4. RSI(9) at extreme: <25 for longs, >75 for shorts
|
||||||
|
5. Volume declining (fading momentum)
|
||||||
|
|
||||||
|
Exit:
|
||||||
|
- SL: Tighter of VWAP 3-SD band and 1.5x ATR(10)
|
||||||
|
- TP: Session VWAP (100% exit at mean)
|
||||||
|
- Time stop: 8 bars (2 hours on M15)
|
||||||
|
|
||||||
|
Target pairs: EUR_USD, USD_JPY, EUR_GBP (range-bound majors)
|
||||||
|
"""
|
||||||
|
from typing import Optional
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from .base import BaseStrategy
|
||||||
|
|
||||||
|
|
||||||
|
class S10_VWAP_MeanReversion(BaseStrategy):
|
||||||
|
strategy_id = 10
|
||||||
|
name = "S10_VWAP_MeanReversion"
|
||||||
|
|
||||||
|
# Tunable parameters
|
||||||
|
VWAP_SD_ENTRY = 2.0 # Min SD from VWAP to enter
|
||||||
|
VWAP_SD_SL = 3.0 # SL at this SD band
|
||||||
|
SL_ATR_MULT = 1.5 # Alternative SL: 1.5x ATR(10)
|
||||||
|
MAX_ADX = 30 # Ranging regime gate
|
||||||
|
RSI9_LONG_THRESH = 25 # RSI(9) < 25 for longs
|
||||||
|
RSI9_SHORT_THRESH = 75 # RSI(9) > 75 for shorts
|
||||||
|
MAX_BARS = 8 # Time stop: 8 bars = 2 hours on M15
|
||||||
|
|
||||||
|
def check_signal(self, data: pd.DataFrame, idx: int,
|
||||||
|
current: pd.Series,
|
||||||
|
htf_row: Optional[pd.Series] = None) -> Optional[dict]:
|
||||||
|
# Warmup
|
||||||
|
if idx < 50:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Session filter: 08:00-16:00 UTC
|
||||||
|
hour = current.name.hour if hasattr(current.name, 'hour') else 0
|
||||||
|
if hour < 8 or hour >= 16:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Regime gate: ranging market only
|
||||||
|
adx_val = current.get("adx_14", 50)
|
||||||
|
if np.isnan(adx_val) or adx_val >= self.MAX_ADX:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# VWAP data required
|
||||||
|
vwap = current.get("session_vwap", np.nan)
|
||||||
|
vwap_std = current.get("vwap_std", np.nan)
|
||||||
|
if np.isnan(vwap) or np.isnan(vwap_std) or vwap_std <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
price = current["close"]
|
||||||
|
atr_10 = current.get("atr_10", 0)
|
||||||
|
if atr_10 <= 0 or np.isnan(atr_10):
|
||||||
|
return None
|
||||||
|
|
||||||
|
rsi9 = current.get("rsi_9", 50)
|
||||||
|
if np.isnan(rsi9):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Z-score: how many SDs from VWAP
|
||||||
|
z_score = (price - vwap) / vwap_std
|
||||||
|
|
||||||
|
# Volume declining check (fading momentum)
|
||||||
|
vol = current.get("volume", 0)
|
||||||
|
vol_window = data["volume"].iloc[max(0, idx - 3):idx]
|
||||||
|
if len(vol_window) == 0:
|
||||||
|
return None
|
||||||
|
vol_avg = vol_window.mean()
|
||||||
|
if vol_avg <= 0 or vol >= vol_avg:
|
||||||
|
return None # Volume not declining
|
||||||
|
|
||||||
|
# HTF trend for confluence (soft — not a gate)
|
||||||
|
htf_bullish = False
|
||||||
|
htf_bearish = False
|
||||||
|
if htf_row is not None:
|
||||||
|
htf_close = htf_row.get("close", np.nan)
|
||||||
|
htf_ema200 = htf_row.get("ema_200", np.nan)
|
||||||
|
if not np.isnan(htf_close) and not np.isnan(htf_ema200):
|
||||||
|
htf_bullish = htf_close > htf_ema200
|
||||||
|
htf_bearish = htf_close < htf_ema200
|
||||||
|
|
||||||
|
direction = None
|
||||||
|
|
||||||
|
# LONG: price well below VWAP, RSI oversold
|
||||||
|
if z_score <= -self.VWAP_SD_ENTRY and rsi9 < self.RSI9_LONG_THRESH:
|
||||||
|
direction = "LONG"
|
||||||
|
# SHORT: price well above VWAP, RSI overbought
|
||||||
|
elif z_score >= self.VWAP_SD_ENTRY and rsi9 > self.RSI9_SHORT_THRESH:
|
||||||
|
direction = "SHORT"
|
||||||
|
|
||||||
|
if direction is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Confluence scoring
|
||||||
|
confluence = 2 # Base: VWAP deviation + RSI extreme
|
||||||
|
if abs(z_score) >= 2.5:
|
||||||
|
confluence += 1 # Deeper deviation
|
||||||
|
if (direction == "LONG" and htf_bullish) or \
|
||||||
|
(direction == "SHORT" and htf_bearish):
|
||||||
|
confluence += 1 # HTF aligns with mean reversion direction
|
||||||
|
if vol_avg > 0 and vol < 0.7 * vol_avg:
|
||||||
|
confluence += 1 # Strong fade signal
|
||||||
|
|
||||||
|
# Exit levels
|
||||||
|
if direction == "LONG":
|
||||||
|
sl_vwap = vwap - self.VWAP_SD_SL * vwap_std
|
||||||
|
sl_atr = price - self.SL_ATR_MULT * atr_10
|
||||||
|
sl = max(sl_vwap, sl_atr) # Tighter (closer to price) for longs
|
||||||
|
tp = vwap # Mean reversion target
|
||||||
|
else:
|
||||||
|
sl_vwap = vwap + self.VWAP_SD_SL * vwap_std
|
||||||
|
sl_atr = price + self.SL_ATR_MULT * atr_10
|
||||||
|
sl = min(sl_vwap, sl_atr) # Tighter (closer to price) for shorts
|
||||||
|
tp = vwap
|
||||||
|
|
||||||
|
return {
|
||||||
|
"direction": direction,
|
||||||
|
"sl": sl,
|
||||||
|
"tp1": tp,
|
||||||
|
"tp2": tp,
|
||||||
|
"tp3": tp,
|
||||||
|
"confluence": confluence,
|
||||||
|
"entry_pattern": f"vwap_mean_reversion_{direction.lower()}",
|
||||||
|
"tp_splits": (1.0, 0.0, 0.0),
|
||||||
|
"trail_atr_mult": 0,
|
||||||
|
"max_bars": self.MAX_BARS,
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
"""
|
||||||
|
Strategy S11: ADX Trend Pullback to 20 EMA (M15).
|
||||||
|
|
||||||
|
Concept: In strong trending environments (ADX>30, rising), enter on pullbacks
|
||||||
|
to the 20 EMA when RSI(9) has reset to a neutral zone, confirmed by a
|
||||||
|
candle closing in the trend direction.
|
||||||
|
|
||||||
|
Entry conditions (ALL must be true):
|
||||||
|
1. Session filter: 08:00-16:00 UTC
|
||||||
|
2. Strong trend: ADX(14) > 30 and rising
|
||||||
|
3. Trend direction: EMA(20) > EMA(50) for uptrend (opposite for downtrend)
|
||||||
|
4. HTF alignment: close above/below EMA(200)
|
||||||
|
5. Pullback to EMA(20): price within 0.3x ATR(14)
|
||||||
|
6. Candle confirmation: close in trend direction
|
||||||
|
7. RSI(9) reset: 40-55 for longs, 45-60 for shorts
|
||||||
|
8. Volume declining during pullback
|
||||||
|
|
||||||
|
Exit:
|
||||||
|
- SL: Tighter of EMA(50) and 1.5x ATR(10)
|
||||||
|
- TP1: 1.0x risk (50%), TP2: 1.8x risk (50%)
|
||||||
|
- Trail: 1.0x ATR after TP1
|
||||||
|
- Time stop: 40 bars (~10 hours on M15)
|
||||||
|
|
||||||
|
Target pairs: USD_JPY, GBP_USD, GBP_JPY
|
||||||
|
"""
|
||||||
|
from typing import Optional
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from .base import BaseStrategy
|
||||||
|
|
||||||
|
|
||||||
|
class S11_ADX_TrendPullback(BaseStrategy):
|
||||||
|
strategy_id = 11
|
||||||
|
name = "S11_ADX_TrendPullback"
|
||||||
|
|
||||||
|
# Tunable parameters
|
||||||
|
MIN_ADX = 30 # Strong trend gate
|
||||||
|
EMA_PROXIMITY_ATR = 0.3 # Pullback within 0.3x ATR of EMA(20)
|
||||||
|
SL_ATR_MULT = 1.5 # SL: 1.5x ATR(10) or beyond EMA(50)
|
||||||
|
TP_RR_MULT = 1.8 # TP2: 1.8x risk distance
|
||||||
|
RSI9_LONG_LOW = 40 # RSI(9) reset zone for longs
|
||||||
|
RSI9_LONG_HIGH = 55
|
||||||
|
RSI9_SHORT_LOW = 45 # RSI(9) reset zone for shorts
|
||||||
|
RSI9_SHORT_HIGH = 60
|
||||||
|
MAX_BARS = 40 # ~10 hours on M15
|
||||||
|
TRAIL_ATR_MULT = 1.0 # Trail after 1x risk profit
|
||||||
|
|
||||||
|
def check_signal(self, data: pd.DataFrame, idx: int,
|
||||||
|
current: pd.Series,
|
||||||
|
htf_row: Optional[pd.Series] = None) -> Optional[dict]:
|
||||||
|
# Warmup
|
||||||
|
if idx < 50:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Session filter: 08:00-16:00 UTC
|
||||||
|
hour = current.name.hour if hasattr(current.name, 'hour') else 0
|
||||||
|
if hour < 8 or hour >= 16:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ADX gate: strong and rising
|
||||||
|
adx_val = current.get("adx_14", 0)
|
||||||
|
if np.isnan(adx_val) or adx_val <= self.MIN_ADX:
|
||||||
|
return None
|
||||||
|
prev_adx = data["adx_14"].iloc[idx - 1] if idx > 0 else 0
|
||||||
|
if np.isnan(prev_adx) or adx_val <= prev_adx:
|
||||||
|
return None # ADX must be rising
|
||||||
|
|
||||||
|
# EMA values
|
||||||
|
ema_20 = current.get("ema_20", np.nan)
|
||||||
|
ema_50 = current.get("ema_50", np.nan)
|
||||||
|
if np.isnan(ema_20) or np.isnan(ema_50):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Trend direction from EMA alignment
|
||||||
|
uptrend = ema_20 > ema_50
|
||||||
|
downtrend = ema_20 < ema_50
|
||||||
|
if not uptrend and not downtrend:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# HTF alignment required
|
||||||
|
if htf_row is None:
|
||||||
|
return None
|
||||||
|
htf_close = htf_row.get("close", np.nan)
|
||||||
|
htf_ema200 = htf_row.get("ema_200", np.nan)
|
||||||
|
if np.isnan(htf_close) or np.isnan(htf_ema200):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if uptrend and htf_close <= htf_ema200:
|
||||||
|
return None
|
||||||
|
if downtrend and htf_close >= htf_ema200:
|
||||||
|
return None
|
||||||
|
|
||||||
|
price = current["close"]
|
||||||
|
open_price = current["open"]
|
||||||
|
atr_14 = current.get("atr_14", 0)
|
||||||
|
atr_10 = current.get("atr_10", 0)
|
||||||
|
if atr_14 <= 0 or np.isnan(atr_14) or atr_10 <= 0 or np.isnan(atr_10):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Pullback proximity: price near EMA(20)
|
||||||
|
if abs(price - ema_20) > self.EMA_PROXIMITY_ATR * atr_14:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# RSI(9) reset check
|
||||||
|
rsi9 = current.get("rsi_9", 50)
|
||||||
|
if np.isnan(rsi9):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Volume declining during pullback
|
||||||
|
vol = current.get("volume", 0)
|
||||||
|
vol_window = data["volume"].iloc[max(0, idx - 3):idx]
|
||||||
|
if len(vol_window) == 0:
|
||||||
|
return None
|
||||||
|
vol_avg = vol_window.mean()
|
||||||
|
if vol_avg <= 0 or vol >= vol_avg:
|
||||||
|
return None
|
||||||
|
|
||||||
|
direction = None
|
||||||
|
|
||||||
|
if uptrend:
|
||||||
|
# Candle closes bullish
|
||||||
|
if price <= open_price:
|
||||||
|
return None
|
||||||
|
# RSI(9) in reset zone for longs
|
||||||
|
if self.RSI9_LONG_LOW <= rsi9 <= self.RSI9_LONG_HIGH:
|
||||||
|
direction = "LONG"
|
||||||
|
elif downtrend:
|
||||||
|
# Candle closes bearish
|
||||||
|
if price >= open_price:
|
||||||
|
return None
|
||||||
|
# RSI(9) in reset zone for shorts
|
||||||
|
if self.RSI9_SHORT_LOW <= rsi9 <= self.RSI9_SHORT_HIGH:
|
||||||
|
direction = "SHORT"
|
||||||
|
|
||||||
|
if direction is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Confluence scoring
|
||||||
|
confluence = 3 # Base: ADX strong + pullback to EMA + RSI reset
|
||||||
|
# HTF alignment already confirmed above
|
||||||
|
confluence += 1
|
||||||
|
if adx_val > 40:
|
||||||
|
confluence += 1 # Very strong trend
|
||||||
|
# Volume spike on confirmation candle (relative to longer window)
|
||||||
|
vol_avg_20 = data["volume"].iloc[max(0, idx - 20):idx].mean()
|
||||||
|
if vol_avg_20 > 0 and vol > 1.5 * vol_avg_20:
|
||||||
|
confluence += 1
|
||||||
|
|
||||||
|
# Exit levels
|
||||||
|
if direction == "LONG":
|
||||||
|
sl_ema = ema_50
|
||||||
|
sl_atr = price - self.SL_ATR_MULT * atr_10
|
||||||
|
sl = max(sl_ema, sl_atr) # Tighter: whichever is closer to price
|
||||||
|
risk = price - sl
|
||||||
|
if risk <= 0:
|
||||||
|
return None
|
||||||
|
tp1 = price + 1.0 * risk # Breakeven target
|
||||||
|
tp2 = price + self.TP_RR_MULT * risk # Full target
|
||||||
|
else:
|
||||||
|
sl_ema = ema_50
|
||||||
|
sl_atr = price + self.SL_ATR_MULT * atr_10
|
||||||
|
sl = min(sl_ema, sl_atr) # Tighter: whichever is closer to price
|
||||||
|
risk = sl - price
|
||||||
|
if risk <= 0:
|
||||||
|
return None
|
||||||
|
tp1 = price - 1.0 * risk
|
||||||
|
tp2 = price - self.TP_RR_MULT * risk
|
||||||
|
|
||||||
|
return {
|
||||||
|
"direction": direction,
|
||||||
|
"sl": sl,
|
||||||
|
"tp1": tp1,
|
||||||
|
"tp2": tp2,
|
||||||
|
"tp3": tp2,
|
||||||
|
"confluence": confluence,
|
||||||
|
"entry_pattern": f"adx_trend_pullback_{direction.lower()}",
|
||||||
|
"tp_splits": (0.50, 0.50, 0.0),
|
||||||
|
"trail_atr_mult": self.TRAIL_ATR_MULT,
|
||||||
|
"max_bars": self.MAX_BARS,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user