mirror of
https://github.com/BrentNeale1/fx-quant.git
synced 2026-08-26 08:28:04 +00:00
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>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
5d7f6c60a9
commit
dce54845c2
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user