mirror of
https://github.com/BrentNeale1/fx-quant.git
synced 2026-08-19 04:58:07 +00:00
- Built event-driven backtesting engine with spread/slippage modeling, 3-TP partial closes, trailing stops, and rich trade logging (20+ features) - Implemented 5 strategy signal generators (MA Breakout, VWAP Reversal, Key Level Breakout, EMA Ribbon Scalp, Momentum Exhaustion) - Full indicator library (EMA, SMA, RSI, ATR, MACD, ADX, Stochastic, Session VWAP bands, swing points, key levels, RSI divergence) - Data pipeline: Dukascopy download, validation, 70/30 train/test split - Baseline results: all 5 strategies generate 200+ trades on training data (Jan 2021 - Aug 2023), best profit factors 0.82-0.96 on select pairs - Trade logs and reports saved for Phase 3 ML feature engineering Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
141 lines
4.6 KiB
Python
141 lines
4.6 KiB
Python
"""
|
|
Strategy 2: Session VWAP Reversal.
|
|
|
|
Entry TF: M15, No HTF filter needed.
|
|
|
|
Entry conditions (LONG):
|
|
- Price crosses below VWAP -2 sigma band
|
|
- RSI < 30 (oversold confirmation)
|
|
- Session filter: London/NY hours only (08:00-17:00 UTC)
|
|
- Minimum band width: vwap_std > 0.3 * ATR
|
|
|
|
Entry conditions (SHORT):
|
|
- Price crosses above VWAP +2 sigma band
|
|
- RSI > 70 (overbought confirmation)
|
|
|
|
TP1: Return to VWAP, TP2: Opposite 0.5 sigma band
|
|
SL: Beyond session high/low OR 1x ATR (whichever tighter)
|
|
"""
|
|
from typing import Optional
|
|
import numpy as np
|
|
import pandas as pd
|
|
from .base import BaseStrategy
|
|
|
|
|
|
class S2_VWAP_Reversal(BaseStrategy):
|
|
strategy_id = 2
|
|
name = "S2_Session_VWAP_Reversal"
|
|
|
|
def check_signal(self, data: pd.DataFrame, idx: int,
|
|
current: pd.Series,
|
|
htf_row: Optional[pd.Series] = None) -> Optional[dict]:
|
|
if idx < 30:
|
|
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:
|
|
return None
|
|
|
|
vwap = current.get("session_vwap", None)
|
|
upper_2 = current.get("vwap_upper_2", None)
|
|
lower_2 = current.get("vwap_lower_2", None)
|
|
|
|
if vwap is None or upper_2 is None or lower_2 is None:
|
|
return None
|
|
if any(np.isnan(v) for v in [vwap, upper_2, lower_2]):
|
|
return None
|
|
|
|
# Skip if bands are too tight (early in session, no deviation yet)
|
|
vwap_std = current.get("vwap_std", 0)
|
|
if vwap_std is None or np.isnan(vwap_std) or vwap_std <= 0:
|
|
return None
|
|
|
|
atr_val = current.get("atr_14", 0)
|
|
if atr_val <= 0 or np.isnan(atr_val):
|
|
return None
|
|
|
|
# Minimum band width: std must be meaningful relative to ATR
|
|
if vwap_std < 0.3 * atr_val:
|
|
return None
|
|
|
|
rsi = current.get("rsi_14", 50)
|
|
if np.isnan(rsi):
|
|
return None
|
|
|
|
close = current["close"]
|
|
prev = data.iloc[idx - 1]
|
|
|
|
# Session high/low for SL
|
|
start = max(0, idx - 80)
|
|
session_high = data["high"].iloc[start:idx + 1].max()
|
|
session_low = data["low"].iloc[start:idx + 1].min()
|
|
|
|
# LONG: price below -2 sigma + RSI < 30 + price just crossed below
|
|
if close < lower_2 and rsi < 30:
|
|
prev_lower_2 = prev.get("vwap_lower_2", None)
|
|
if prev_lower_2 is not None and not np.isnan(prev_lower_2):
|
|
if prev["close"] >= prev_lower_2:
|
|
return self._build_long(close, vwap, atr_val, vwap_std,
|
|
session_low, current)
|
|
|
|
# SHORT: price above +2 sigma + RSI > 70 + price just crossed above
|
|
if close > upper_2 and rsi > 70:
|
|
prev_upper_2 = prev.get("vwap_upper_2", None)
|
|
if prev_upper_2 is not None and not np.isnan(prev_upper_2):
|
|
if prev["close"] <= prev_upper_2:
|
|
return self._build_short(close, vwap, atr_val, vwap_std,
|
|
session_high, current)
|
|
|
|
return None
|
|
|
|
def _build_long(self, close, vwap, atr_val, vwap_std, session_low, current):
|
|
sl_session = session_low - 0.5 * atr_val
|
|
sl_atr = close - 1.5 * atr_val # Wider SL for mean reversion
|
|
sl = max(sl_session, sl_atr)
|
|
|
|
tp1 = vwap
|
|
tp2 = vwap + 0.5 * vwap_std
|
|
tp3 = vwap + 1.5 * vwap_std
|
|
|
|
confluence = 2
|
|
if current.get("adx_14", 30) < 25:
|
|
confluence += 1
|
|
macd_h = current.get("macd_hist", 0)
|
|
if macd_h > 0:
|
|
confluence += 1
|
|
|
|
return {
|
|
"direction": "LONG",
|
|
"sl": sl, "tp1": tp1, "tp2": tp2, "tp3": tp3,
|
|
"confluence": confluence,
|
|
"tp_splits": (0.50, 0.35, 0.15),
|
|
"trail_atr_mult": 1.0,
|
|
"max_bars": 80,
|
|
}
|
|
|
|
def _build_short(self, close, vwap, atr_val, vwap_std, session_high, current):
|
|
sl_session = session_high + 0.5 * atr_val
|
|
sl_atr = close + 1.5 * atr_val # Wider SL for mean reversion
|
|
sl = min(sl_session, sl_atr)
|
|
|
|
tp1 = vwap
|
|
tp2 = vwap - 0.5 * vwap_std
|
|
tp3 = vwap - 1.5 * vwap_std
|
|
|
|
confluence = 2
|
|
if current.get("adx_14", 30) < 25:
|
|
confluence += 1
|
|
macd_h = current.get("macd_hist", 0)
|
|
if macd_h < 0:
|
|
confluence += 1
|
|
|
|
return {
|
|
"direction": "SHORT",
|
|
"sl": sl, "tp1": tp1, "tp2": tp2, "tp3": tp3,
|
|
"confluence": confluence,
|
|
"tp_splits": (0.50, 0.35, 0.15),
|
|
"trail_atr_mult": 1.0,
|
|
"max_bars": 80,
|
|
}
|