mirror of
https://github.com/BrentNeale1/fx-quant.git
synced 2026-08-21 05:58:05 +00:00
Phase 1 complete: S3-S6 strategies, S4 variant analysis, learnings doc
- S3 Key Level Breakout: best performer (52-53% WR, PF ~1.0 on JPY crosses) - S4 EMA Ribbon: tested 7 variants (D/E/F/F-v2/G/G-Minimal), exhausted - Only EUR_AUD S4-F marginally profitable (PF 1.06) - Detailed filter funnel analysis revealed contradictory filter stacking - S5 Momentum Exhaustion: extended to 5 pairs, PF 0.43-0.77 - S6 EMA Bounce: 59-60% WR but PF 0.83-0.84, needs SL/TP restructuring - Added STRATEGY_LEARNINGS.md with design principles and next steps - Added M5 data downloader for 3-timeframe strategies - Updated README with full strategy scorecard Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
dce54845c2
commit
edbe359d1b
@@ -1,33 +1,375 @@
|
||||
"""
|
||||
Strategy 1: MA Breakout-Retest (Option 3 - no trendlines).
|
||||
Strategy 1: Trendline Breakout-Retest.
|
||||
|
||||
Uses MA structure + key level breaks + candle confirmation.
|
||||
Entry TF: M15, Filter TF: H1 (200 SMA directional filter).
|
||||
4-step sequence identified on H1 chart with M15 entry:
|
||||
1. Identify trendline on H1 (3+ swing touches, linear regression)
|
||||
2. Breakout: H1 close beyond trendline with conviction
|
||||
3. Move away: Price moves away from trendline (confirms real break)
|
||||
4. Retest + Entry: Price pulls back to broken trendline on M15 → engulfing candle
|
||||
|
||||
Entry conditions (LONG):
|
||||
- EMA 50 > EMA 100 > EMA 200 (trend alignment)
|
||||
- Price pulls back to EMA 50 zone (within 1.0x ATR)
|
||||
- Bullish confirmation candle (close > open, close > prev close, body > 30% range)
|
||||
- H1 close > H1 200 SMA (HTF filter)
|
||||
- Session filter: London/NY hours only (08:00-17:00 UTC)
|
||||
Entry conditions (all must be true):
|
||||
- State machine in RETEST phase
|
||||
- M15 engulfing candle
|
||||
- M15 close within 1.0x ATR of projected trendline price
|
||||
- M15 EMA 50 aligns with direction
|
||||
- Session: London/NY overlap (13:00-16:00 UTC)
|
||||
- Confluence >= 2
|
||||
|
||||
Boosters (confluence 0-5):
|
||||
- Volume above 20-period average
|
||||
- RSI between 40-60 (not overextended)
|
||||
- MACD histogram positive and rising
|
||||
- ADX > 20 (trending)
|
||||
- Price above session VWAP
|
||||
SL: Projected trendline price +/- 0.5x ATR (behind the trendline)
|
||||
TP1: Previous swing high/low (structure), fallback 1.5x ATR
|
||||
TP2: Next key level or 2.5x ATR
|
||||
TP3: 2x TP1 distance or 4x ATR (runner)
|
||||
"""
|
||||
from typing import Optional
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from .base import BaseStrategy
|
||||
from ..indicators.technical import (
|
||||
fit_trendline, project_trendline, swing_highs, swing_lows,
|
||||
is_bullish_engulfing, is_bearish_engulfing, identify_key_levels,
|
||||
)
|
||||
|
||||
|
||||
class S1_MA_Breakout(BaseStrategy):
|
||||
"""Trendline Breakout-Retest strategy (renamed from MA Breakout)."""
|
||||
strategy_id = 1
|
||||
name = "S1_MA_Breakout_Retest"
|
||||
name = "S1_Trendline_Breakout_Retest"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._trendlines = {"resistance": None, "support": None}
|
||||
self._tl_cache_idx = -1
|
||||
self._state = {
|
||||
"phase": "IDLE",
|
||||
"direction": None,
|
||||
"break_bar_idx": None,
|
||||
"break_price": None,
|
||||
"trendline": None,
|
||||
"max_dist": 0.0,
|
||||
"bars_since_break": 0,
|
||||
}
|
||||
# Performance: cached HTF timestamps for searchsorted
|
||||
self._htf_ts_cache = None
|
||||
self._last_htf_cutoff = -1 # tracks H1 bar changes for timeout
|
||||
|
||||
def _htf_cutoff(self, htf: pd.DataFrame, ts: pd.Timestamp) -> int:
|
||||
"""Return number of H1 bars strictly before ts, using searchsorted."""
|
||||
if self._htf_ts_cache is None:
|
||||
self._htf_ts_cache = htf.index
|
||||
# Normalize tz: strip tz from ts if HTF index is tz-naive, or vice versa
|
||||
if self._htf_ts_cache.tz is None and hasattr(ts, 'tz') and ts.tz is not None:
|
||||
ts = ts.tz_localize(None)
|
||||
elif self._htf_ts_cache.tz is not None and (not hasattr(ts, 'tz') or ts.tz is None):
|
||||
ts = ts.tz_localize(self._htf_ts_cache.tz)
|
||||
return int(self._htf_ts_cache.searchsorted(ts, side="left"))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Trendline Detection (runs on H1 data)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _detect_trendlines(self, htf: pd.DataFrame, n_valid: int):
|
||||
"""
|
||||
Detect resistance and support trendlines from H1 swing points.
|
||||
n_valid = number of H1 bars before current M15 timestamp.
|
||||
Recalculates every 20 H1 bars.
|
||||
"""
|
||||
if n_valid < 50:
|
||||
return
|
||||
|
||||
if (self._tl_cache_idx >= 0 and
|
||||
n_valid - self._tl_cache_idx < 20):
|
||||
return
|
||||
|
||||
self._tl_cache_idx = n_valid
|
||||
|
||||
# Use last 200 H1 bars (no lookahead: only first n_valid bars)
|
||||
start = max(0, n_valid - 200)
|
||||
window = htf.iloc[start:n_valid]
|
||||
offset = start # absolute index of window[0] in htf
|
||||
|
||||
# Detect swing highs and lows
|
||||
sh_mask = swing_highs(window, lookback=5)
|
||||
sl_mask = swing_lows(window, lookback=5)
|
||||
|
||||
# Resistance trendline from swing highs
|
||||
sh_indices = np.where(sh_mask.values)[0]
|
||||
if len(sh_indices) >= 3:
|
||||
recent_sh = sh_indices[-8:]
|
||||
sh_prices = window["high"].values[recent_sh]
|
||||
tl = fit_trendline(recent_sh, sh_prices)
|
||||
if tl is not None:
|
||||
tl["window_offset"] = offset
|
||||
self._trendlines["resistance"] = tl
|
||||
else:
|
||||
self._trendlines["resistance"] = None
|
||||
|
||||
# Support trendline from swing lows
|
||||
sl_indices_arr = np.where(sl_mask.values)[0]
|
||||
if len(sl_indices_arr) >= 3:
|
||||
recent_sl = sl_indices_arr[-8:]
|
||||
sl_prices = window["low"].values[recent_sl]
|
||||
tl = fit_trendline(recent_sl, sl_prices)
|
||||
if tl is not None:
|
||||
tl["window_offset"] = offset
|
||||
self._trendlines["support"] = tl
|
||||
else:
|
||||
self._trendlines["support"] = None
|
||||
|
||||
def _project_tl_at_htf_bar(self, tl: dict, htf_bar_idx: int) -> float:
|
||||
"""Project trendline price at a given absolute HTF bar index."""
|
||||
window_rel_idx = htf_bar_idx - tl["window_offset"]
|
||||
return tl["slope"] * window_rel_idx + tl["intercept"]
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# State Machine
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _update_state_machine(self, htf: pd.DataFrame, n_valid: int):
|
||||
"""
|
||||
Check for breakout transitions on H1 data.
|
||||
n_valid = number of H1 bars strictly before current M15 timestamp.
|
||||
"""
|
||||
if n_valid < 2:
|
||||
return
|
||||
|
||||
last_h1_idx = n_valid - 1
|
||||
last_h1 = htf.iloc[last_h1_idx]
|
||||
|
||||
# H1 ATR for thresholds
|
||||
h1_atr = last_h1.get("atr_14", 0)
|
||||
if h1_atr <= 0 or np.isnan(h1_atr):
|
||||
return
|
||||
|
||||
phase = self._state["phase"]
|
||||
|
||||
# Track H1 bar changes for timeout counter
|
||||
if phase != "IDLE":
|
||||
if last_h1_idx != self._last_htf_cutoff:
|
||||
self._last_htf_cutoff = last_h1_idx
|
||||
self._state["bars_since_break"] += 1
|
||||
if self._state["bars_since_break"] > 50:
|
||||
self._reset_state()
|
||||
return
|
||||
|
||||
if phase == "IDLE":
|
||||
# Check for breakout above resistance -> LONG
|
||||
res_tl = self._trendlines.get("resistance")
|
||||
if res_tl is not None:
|
||||
tl_price = self._project_tl_at_htf_bar(res_tl, last_h1_idx)
|
||||
threshold = tl_price + 0.3 * h1_atr
|
||||
h1_close = last_h1["close"]
|
||||
h1_open = last_h1["open"]
|
||||
body_low = min(h1_close, h1_open)
|
||||
if h1_close > threshold and body_low > tl_price:
|
||||
self._state = {
|
||||
"phase": "MOVE_AWAY",
|
||||
"direction": "LONG",
|
||||
"break_bar_idx": last_h1_idx,
|
||||
"break_price": h1_close,
|
||||
"trendline": res_tl.copy(),
|
||||
"max_dist": h1_close - tl_price,
|
||||
"bars_since_break": 0,
|
||||
"h1_atr": h1_atr,
|
||||
}
|
||||
self._last_htf_cutoff = last_h1_idx
|
||||
return
|
||||
|
||||
# Check for breakout below support -> SHORT
|
||||
sup_tl = self._trendlines.get("support")
|
||||
if sup_tl is not None:
|
||||
tl_price = self._project_tl_at_htf_bar(sup_tl, last_h1_idx)
|
||||
threshold = tl_price - 0.3 * h1_atr
|
||||
h1_close = last_h1["close"]
|
||||
h1_open = last_h1["open"]
|
||||
body_high = max(h1_close, h1_open)
|
||||
if h1_close < threshold and body_high < tl_price:
|
||||
self._state = {
|
||||
"phase": "MOVE_AWAY",
|
||||
"direction": "SHORT",
|
||||
"break_bar_idx": last_h1_idx,
|
||||
"break_price": h1_close,
|
||||
"trendline": sup_tl.copy(),
|
||||
"max_dist": tl_price - h1_close,
|
||||
"bars_since_break": 0,
|
||||
"h1_atr": h1_atr,
|
||||
}
|
||||
self._last_htf_cutoff = last_h1_idx
|
||||
return
|
||||
|
||||
elif phase == "MOVE_AWAY":
|
||||
tl = self._state["trendline"]
|
||||
tl_price = self._project_tl_at_htf_bar(tl, last_h1_idx)
|
||||
h1_close = last_h1["close"]
|
||||
state_atr = self._state.get("h1_atr", h1_atr)
|
||||
|
||||
if self._state["direction"] == "LONG":
|
||||
dist = h1_close - tl_price
|
||||
if dist > self._state["max_dist"]:
|
||||
self._state["max_dist"] = dist
|
||||
if self._state["max_dist"] >= 0.5 * state_atr and dist < self._state["max_dist"]:
|
||||
self._state["phase"] = "RETEST"
|
||||
else: # SHORT
|
||||
dist = tl_price - h1_close
|
||||
if dist > self._state["max_dist"]:
|
||||
self._state["max_dist"] = dist
|
||||
if self._state["max_dist"] >= 0.5 * state_atr and dist < self._state["max_dist"]:
|
||||
self._state["phase"] = "RETEST"
|
||||
|
||||
def _reset_state(self):
|
||||
self._state = {
|
||||
"phase": "IDLE",
|
||||
"direction": None,
|
||||
"break_bar_idx": None,
|
||||
"break_price": None,
|
||||
"trendline": None,
|
||||
"max_dist": 0.0,
|
||||
"bars_since_break": 0,
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Confluence Scoring (0-5)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _calc_confluence(self, data: pd.DataFrame, idx: int,
|
||||
current: pd.Series, direction: str,
|
||||
tl: dict) -> int:
|
||||
confluence = 0
|
||||
|
||||
# Trendline R-squared > 0.90
|
||||
if tl.get("r_squared", 0) > 0.90:
|
||||
confluence += 1
|
||||
|
||||
# Touch count >= 4
|
||||
if tl.get("touch_count", 0) >= 4:
|
||||
confluence += 1
|
||||
|
||||
# Volume above 20-period average
|
||||
if "volume" in current.index:
|
||||
vol_avg = data["volume"].iloc[max(0, idx - 20):idx].mean()
|
||||
if vol_avg > 0 and current["volume"] > vol_avg:
|
||||
confluence += 1
|
||||
|
||||
# RSI between 40-60
|
||||
rsi_val = current.get("rsi_14", 50)
|
||||
if not np.isnan(rsi_val) and 40 <= rsi_val <= 60:
|
||||
confluence += 1
|
||||
|
||||
# MACD histogram confirms direction
|
||||
macd_h = current.get("macd_hist", 0)
|
||||
if not np.isnan(macd_h):
|
||||
if direction == "LONG" and macd_h > 0:
|
||||
confluence += 1
|
||||
elif direction == "SHORT" and macd_h < 0:
|
||||
confluence += 1
|
||||
|
||||
return min(confluence, 5)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Find structure-based TP levels from H1 data
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _find_structure_tp(self, htf: pd.DataFrame, n_valid: int,
|
||||
direction: str, entry_price: float,
|
||||
atr_val: float) -> tuple:
|
||||
"""Find TP levels based on H1 swing structure and key levels."""
|
||||
if n_valid < 50:
|
||||
if direction == "LONG":
|
||||
return (entry_price + 1.5 * atr_val,
|
||||
entry_price + 2.5 * atr_val,
|
||||
entry_price + 4.0 * atr_val)
|
||||
else:
|
||||
return (entry_price - 1.5 * atr_val,
|
||||
entry_price - 2.5 * atr_val,
|
||||
entry_price - 4.0 * atr_val)
|
||||
|
||||
start = max(0, n_valid - 100)
|
||||
window = htf.iloc[start:n_valid]
|
||||
|
||||
if direction == "LONG":
|
||||
# TP1: previous swing high above entry
|
||||
sh_mask = swing_highs(window, lookback=5)
|
||||
sh_prices = window.loc[sh_mask, "high"]
|
||||
above = sh_prices[sh_prices > entry_price].sort_values()
|
||||
tp1 = above.iloc[0] if len(above) > 0 else entry_price + 1.5 * atr_val
|
||||
|
||||
# TP2: next key level above TP1, or 2.5x ATR
|
||||
levels = identify_key_levels(window, lookback=5, min_touches=2)
|
||||
level_prices = [lv[0] for lv in levels if lv[0] > tp1]
|
||||
tp2 = min(level_prices) if level_prices else entry_price + 2.5 * atr_val
|
||||
|
||||
# TP3: 2x TP1 distance or 4x ATR (runner)
|
||||
tp1_dist = tp1 - entry_price
|
||||
tp3 = entry_price + max(2.0 * tp1_dist, 4.0 * atr_val)
|
||||
|
||||
else: # SHORT
|
||||
sl_mask = swing_lows(window, lookback=5)
|
||||
sl_prices = window.loc[sl_mask, "low"]
|
||||
below = sl_prices[sl_prices < entry_price].sort_values(ascending=False)
|
||||
tp1 = below.iloc[0] if len(below) > 0 else entry_price - 1.5 * atr_val
|
||||
|
||||
levels = identify_key_levels(window, lookback=5, min_touches=2)
|
||||
level_prices = [lv[0] for lv in levels if lv[0] < tp1]
|
||||
tp2 = max(level_prices) if level_prices else entry_price - 2.5 * atr_val
|
||||
|
||||
tp1_dist = entry_price - tp1
|
||||
tp3 = entry_price - max(2.0 * tp1_dist, 4.0 * atr_val)
|
||||
|
||||
# Ensure TP ordering makes sense
|
||||
if direction == "LONG":
|
||||
tp1 = max(tp1, entry_price + 0.5 * atr_val)
|
||||
tp2 = max(tp2, tp1 + 0.3 * atr_val)
|
||||
tp3 = max(tp3, tp2 + 0.3 * atr_val)
|
||||
else:
|
||||
tp1 = min(tp1, entry_price - 0.5 * atr_val)
|
||||
tp2 = min(tp2, tp1 - 0.3 * atr_val)
|
||||
tp3 = min(tp3, tp2 - 0.3 * atr_val)
|
||||
|
||||
return tp1, tp2, tp3
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Reversal Pattern Detection
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _detect_reversal_pattern(self, data: pd.DataFrame, idx: int,
|
||||
current: pd.Series, direction: str) -> str:
|
||||
"""
|
||||
Check for reversal patterns at the retest candle.
|
||||
Returns pattern name ('engulfing', 'pin_bar', 'strong_close') or None.
|
||||
"""
|
||||
o, h, l, c = current["open"], current["high"], current["low"], current["close"]
|
||||
body = abs(c - o)
|
||||
full_range = h - l
|
||||
if full_range <= 0:
|
||||
return None
|
||||
|
||||
if direction == "LONG":
|
||||
# 1. Bullish engulfing
|
||||
if is_bullish_engulfing(data, idx):
|
||||
return "engulfing"
|
||||
# 2. Bullish pin bar: lower wick >= 2x body AND close in upper 25%
|
||||
lower_wick = min(o, c) - l
|
||||
if body > 0 and lower_wick >= 2 * body and c >= l + 0.75 * full_range:
|
||||
return "pin_bar"
|
||||
# 3. Strong bullish close: body > 60% of range AND close > open
|
||||
if body > 0.60 * full_range and c > o:
|
||||
return "strong_close"
|
||||
else: # SHORT
|
||||
# 1. Bearish engulfing
|
||||
if is_bearish_engulfing(data, idx):
|
||||
return "engulfing"
|
||||
# 2. Bearish pin bar: upper wick >= 2x body AND close in lower 25%
|
||||
upper_wick = h - max(o, c)
|
||||
if body > 0 and upper_wick >= 2 * body and c <= l + 0.25 * full_range:
|
||||
return "pin_bar"
|
||||
# 3. Strong bearish close: body > 60% of range AND close < open
|
||||
if body > 0.60 * full_range and c < o:
|
||||
return "strong_close"
|
||||
|
||||
return None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Main Signal Check
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def check_signal(self, data: pd.DataFrame, idx: int,
|
||||
current: pd.Series,
|
||||
@@ -35,143 +377,95 @@ class S1_MA_Breakout(BaseStrategy):
|
||||
if idx < 50:
|
||||
return None
|
||||
|
||||
# Session filter: only London + NY (08:00-17:00 UTC)
|
||||
hour = current.name.hour if hasattr(current.name, 'hour') else 0
|
||||
if hour < 8 or hour >= 17:
|
||||
htf = self.htf_data
|
||||
if htf is None or len(htf) < 50:
|
||||
return None
|
||||
|
||||
current_ts = current.name
|
||||
|
||||
# Efficient HTF cutoff via searchsorted
|
||||
n_valid = self._htf_cutoff(htf, current_ts)
|
||||
if n_valid < 50:
|
||||
return None
|
||||
|
||||
# Detect trendlines and update state machine (always, for tracking)
|
||||
self._detect_trendlines(htf, n_valid)
|
||||
self._update_state_machine(htf, n_valid)
|
||||
|
||||
# Session filter: London + NY overlap (08:00-16:00 UTC)
|
||||
hour = current_ts.hour if hasattr(current_ts, 'hour') else 0
|
||||
if hour < 8 or hour >= 16:
|
||||
return None
|
||||
|
||||
atr_val = current.get("atr_14", 0)
|
||||
if atr_val <= 0 or np.isnan(atr_val):
|
||||
return None
|
||||
|
||||
ema_50 = current.get("ema_50", np.nan)
|
||||
ema_100 = current.get("ema_100", np.nan)
|
||||
ema_200 = current.get("ema_200", np.nan)
|
||||
if any(np.isnan(v) for v in [ema_50, ema_100, ema_200]):
|
||||
# Only generate signals in RETEST phase
|
||||
if self._state["phase"] != "RETEST":
|
||||
return None
|
||||
|
||||
direction = self._state["direction"]
|
||||
tl = self._state["trendline"]
|
||||
|
||||
# Project trendline price at current H1 bar
|
||||
current_htf_idx = n_valid - 1
|
||||
tl_price = self._project_tl_at_htf_bar(tl, current_htf_idx)
|
||||
|
||||
close = current["close"]
|
||||
open_p = current["open"]
|
||||
prev = data.iloc[idx - 1]
|
||||
prev_close = prev["close"]
|
||||
|
||||
# Candle body filter: body must be > 30% of range (no dojis)
|
||||
body = abs(close - open_p)
|
||||
full_range = current["high"] - current["low"]
|
||||
if full_range <= 0 or body / full_range < 0.3:
|
||||
# M15 close within 1.5x ATR of projected trendline
|
||||
dist_to_tl = abs(close - tl_price)
|
||||
if dist_to_tl > 1.5 * atr_val:
|
||||
return None
|
||||
|
||||
# LONG setup
|
||||
if ema_50 > ema_100 > ema_200:
|
||||
# HTF filter
|
||||
if htf_row is not None:
|
||||
htf_sma200 = htf_row.get("sma_200", np.nan)
|
||||
if not np.isnan(htf_sma200) and htf_row.get("close", 0) <= htf_sma200:
|
||||
return None
|
||||
# M15 reversal pattern confirmation (engulfing, pin bar, or strong close)
|
||||
entry_pattern = self._detect_reversal_pattern(data, idx, current, direction)
|
||||
if entry_pattern is None:
|
||||
return None
|
||||
|
||||
# Pullback to EMA 50 zone (within 1.0x ATR - tightened from 1.5x)
|
||||
dist_to_ema50 = close - ema_50
|
||||
if dist_to_ema50 < 0 or dist_to_ema50 > 1.0 * atr_val:
|
||||
# EMA 50 alignment
|
||||
ema_50 = current.get("ema_50", np.nan)
|
||||
if np.isnan(ema_50):
|
||||
return None
|
||||
if direction == "LONG" and close <= ema_50:
|
||||
return None
|
||||
if direction == "SHORT" and close >= ema_50:
|
||||
return None
|
||||
|
||||
# Confluence scoring
|
||||
confluence = self._calc_confluence(data, idx, current, direction, tl)
|
||||
if confluence < 2:
|
||||
return None
|
||||
|
||||
# SL: behind the trendline (0.5x ATR past TL)
|
||||
if direction == "LONG":
|
||||
sl = tl_price - 0.5 * atr_val
|
||||
# Validate SL is below entry (TL may have drifted above price)
|
||||
if sl >= close:
|
||||
return None
|
||||
else:
|
||||
sl = tl_price + 0.5 * atr_val
|
||||
if sl <= close:
|
||||
return None
|
||||
|
||||
# Bullish confirmation candle
|
||||
if not (close > open_p and close > prev_close):
|
||||
return None
|
||||
# TP levels: structure-based from H1 data
|
||||
tp1, tp2, tp3 = self._find_structure_tp(
|
||||
htf, n_valid, direction, close, atr_val
|
||||
)
|
||||
|
||||
# Not too far from EMAs (avoid chasing)
|
||||
if close - ema_200 > 5 * atr_val:
|
||||
return None
|
||||
# Reset state after generating signal
|
||||
self._reset_state()
|
||||
|
||||
confluence = self._calc_confluence(data, idx, current, "LONG")
|
||||
|
||||
# Require minimum confluence of 2
|
||||
if confluence < 2:
|
||||
return None
|
||||
|
||||
sl = current["low"] - 0.5 * atr_val
|
||||
tp1 = close + 1.5 * atr_val
|
||||
tp2 = close + 2.5 * atr_val
|
||||
tp3 = close + 4.0 * atr_val
|
||||
|
||||
return {
|
||||
"direction": "LONG",
|
||||
"sl": sl, "tp1": tp1, "tp2": tp2, "tp3": tp3,
|
||||
"confluence": confluence,
|
||||
"tp_splits": (0.50, 0.30, 0.20),
|
||||
"trail_atr_mult": 1.5,
|
||||
"max_bars": 200,
|
||||
}
|
||||
|
||||
# SHORT setup
|
||||
if ema_50 < ema_100 < ema_200:
|
||||
if htf_row is not None:
|
||||
htf_sma200 = htf_row.get("sma_200", np.nan)
|
||||
if not np.isnan(htf_sma200) and htf_row.get("close", 0) >= htf_sma200:
|
||||
return None
|
||||
|
||||
dist_to_ema50 = ema_50 - close
|
||||
if dist_to_ema50 < 0 or dist_to_ema50 > 1.0 * atr_val:
|
||||
return None
|
||||
|
||||
if not (close < open_p and close < prev_close):
|
||||
return None
|
||||
|
||||
if ema_200 - close > 5 * atr_val:
|
||||
return None
|
||||
|
||||
confluence = self._calc_confluence(data, idx, current, "SHORT")
|
||||
|
||||
if confluence < 2:
|
||||
return None
|
||||
|
||||
sl = current["high"] + 0.5 * atr_val
|
||||
tp1 = close - 1.5 * atr_val
|
||||
tp2 = close - 2.5 * atr_val
|
||||
tp3 = close - 4.0 * atr_val
|
||||
|
||||
return {
|
||||
"direction": "SHORT",
|
||||
"sl": sl, "tp1": tp1, "tp2": tp2, "tp3": tp3,
|
||||
"confluence": confluence,
|
||||
"tp_splits": (0.50, 0.30, 0.20),
|
||||
"trail_atr_mult": 1.5,
|
||||
"max_bars": 200,
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def _calc_confluence(self, data, idx, current, direction):
|
||||
confluence = 0
|
||||
|
||||
# Volume above average
|
||||
if "volume" in current.index:
|
||||
vol_avg = data["volume"].iloc[max(0, idx - 20):idx].mean()
|
||||
if vol_avg > 0 and current["volume"] > vol_avg:
|
||||
confluence += 1
|
||||
|
||||
# RSI between 40-60
|
||||
rsi = current.get("rsi_14", 50)
|
||||
if 40 <= rsi <= 60:
|
||||
confluence += 1
|
||||
|
||||
# MACD histogram confirmation
|
||||
macd_h = current.get("macd_hist", 0)
|
||||
prev_macd_h = data.iloc[idx - 1].get("macd_hist", 0)
|
||||
if direction == "LONG" and macd_h > 0 and macd_h > prev_macd_h:
|
||||
confluence += 1
|
||||
elif direction == "SHORT" and macd_h < 0 and macd_h < prev_macd_h:
|
||||
confluence += 1
|
||||
|
||||
# ADX > 20
|
||||
if current.get("adx_14", 0) > 20:
|
||||
confluence += 1
|
||||
|
||||
# VWAP alignment
|
||||
vwap = current.get("session_vwap", 0)
|
||||
if vwap:
|
||||
if direction == "LONG" and current["close"] > vwap:
|
||||
confluence += 1
|
||||
elif direction == "SHORT" and current["close"] < vwap:
|
||||
confluence += 1
|
||||
|
||||
return min(confluence, 5)
|
||||
return {
|
||||
"direction": direction,
|
||||
"sl": sl,
|
||||
"tp1": tp1,
|
||||
"tp2": tp2,
|
||||
"tp3": tp3,
|
||||
"confluence": confluence,
|
||||
"entry_pattern": entry_pattern,
|
||||
"tp_splits": (0.50, 0.30, 0.20),
|
||||
"trail_atr_mult": 1.5,
|
||||
"max_bars": 200,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user