Files
fx-quant/src/strategies_pkg/s2_vwap_reversal.py
T
Brent NealeandClaude Opus 4.6 dce54845c2 Phase 1: Event-driven backtester, 5 strategies, and baseline results
- 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>
2026-02-18 06:04:40 +10:00

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,
}