mirror of
https://github.com/BrentNeale1/fx-quant.git
synced 2026-08-02 21:37:44 +00:00
39a6536284
- S7 Liquidity Sweep: built, tested across 6 pairs, tight SL (1.0 ATR) on GBP_JPY is Phase 2 candidate (107 trades, OOS PF 1.39, gen ratio 1.81) - S8 Order Block: built, tested on GBP_JPY (watchlist, 32 trades, OOS PF 1.55) - S9 London Session: built, tested across 8 pairs with filter experiments GBP_USD (OOS PF 1.45) and GBP_AUD filtered (OOS PF 1.94) advance to Phase 2 - Added OBV indicator to technical.py - Added GBP_NZD to engine spread/pip config - Standalone OANDA fetcher (bypasses Supabase dependency) - Fetched EUR_GBP, EUR_USD, GBP_NZD H1 data (2021-2023) - Consolidated STRATEGY_LEARNINGS.md with full Phase 1 scorecard and 11 design principles - Phase 2 roster: S7/GBP_JPY, S9/GBP_USD, S9F/GBP_AUD, S4-F/EUR_AUD, S3/GBP_JPY Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
148 lines
6.4 KiB
Python
148 lines
6.4 KiB
Python
"""Quick S9 trade analysis for a single pair — runs backtest and analyzes trade log."""
|
|
import os, sys, io
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
|
|
|
import pandas as pd
|
|
import numpy as np
|
|
from src.indicators.technical import compute_all_indicators
|
|
from src.backtester.engine import Backtester
|
|
from src.strategies_pkg.s9_london_session import S9_London_Session
|
|
|
|
PROCESSED_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "processed")
|
|
|
|
pair = sys.argv[1] if len(sys.argv) > 1 else "GBP_AUD"
|
|
|
|
# Load and run
|
|
fp = os.path.join(PROCESSED_DIR, f"{pair}_H1.csv")
|
|
df = pd.read_csv(fp, index_col=0, parse_dates=True)
|
|
df.index.name = "timestamp"
|
|
df = compute_all_indicators(df)
|
|
|
|
bt = Backtester(data=df, strategy=S9_London_Session(), pair=pair, starting_equity=100_000.0, htf_data=df.copy())
|
|
bt.run()
|
|
trades = bt.get_trade_log_df()
|
|
|
|
print(f"\n{'='*70}")
|
|
print(f"S9 TRADE ANALYSIS — {pair} ({len(trades)} trades)")
|
|
print(f"{'='*70}")
|
|
|
|
if len(trades) == 0:
|
|
print("No trades!")
|
|
sys.exit(0)
|
|
|
|
trades["win"] = trades["pnl_pips"] > 0
|
|
|
|
# 1. Exit reason breakdown
|
|
print("\n1. EXIT REASON BREAKDOWN")
|
|
for reason, grp in trades.groupby("exit_reason"):
|
|
n = len(grp)
|
|
wr = grp["win"].mean() * 100
|
|
avg_pnl = grp["pnl_pips"].mean()
|
|
print(f" {reason:<12} {n:>4} trades ({n/len(trades)*100:5.1f}%) | WR {wr:5.1f}% | Avg PnL {avg_pnl:+7.1f}p")
|
|
|
|
# 2. Direction
|
|
print("\n2. WIN RATE BY DIRECTION")
|
|
for d, grp in trades.groupby("signal_direction"):
|
|
print(f" {d:<6} {len(grp):>4} trades, WR {grp['win'].mean()*100:5.1f}%, Avg PnL {grp['pnl_pips'].mean():+7.1f}p")
|
|
|
|
# 3. Day of week
|
|
print("\n3. WIN RATE BY DAY OF WEEK")
|
|
trades["dow"] = pd.to_datetime(trades["timestamp"]).dt.day_name()
|
|
for day in ["Monday","Tuesday","Wednesday","Thursday","Friday"]:
|
|
grp = trades[trades["dow"] == day]
|
|
if len(grp) > 0:
|
|
print(f" {day:<10} {len(grp):>4} trades, WR {grp['win'].mean()*100:5.1f}%, Avg PnL {grp['pnl_pips'].mean():+7.1f}p")
|
|
|
|
# 4. Hold time
|
|
print("\n4. HOLD TIME (minutes)")
|
|
winners = trades[trades["win"]]
|
|
losers = trades[~trades["win"]]
|
|
print(f" Winners: avg {winners['hold_time_minutes'].mean():.0f}m, median {winners['hold_time_minutes'].median():.0f}m")
|
|
print(f" Losers: avg {losers['hold_time_minutes'].mean():.0f}m, median {losers['hold_time_minutes'].median():.0f}m")
|
|
|
|
# 5. Confluence
|
|
print("\n5. CONFLUENCE SCORE")
|
|
for score, grp in trades.groupby("confluence_score"):
|
|
print(f" Score {score}: {len(grp):>4} trades, WR {grp['win'].mean()*100:5.1f}%, Avg PnL {grp['pnl_pips'].mean():+7.1f}p")
|
|
|
|
# 6. Entry hour
|
|
print("\n6. ENTRY HOUR")
|
|
for h, grp in trades.groupby("hour_of_day"):
|
|
print(f" Hour {h:>2}: {len(grp):>4} trades, WR {grp['win'].mean()*100:5.1f}%, Avg PnL {grp['pnl_pips'].mean():+7.1f}p")
|
|
|
|
# 7. RSI zones
|
|
print("\n7. RSI AT ENTRY")
|
|
bins = [0, 30, 40, 50, 60, 70, 100]
|
|
labels = ["<30", "30-40", "40-50", "50-60", "60-70", "70+"]
|
|
trades["rsi_bin"] = pd.cut(trades["rsi_at_entry"], bins=bins, labels=labels, include_lowest=True)
|
|
for b in labels:
|
|
grp = trades[trades["rsi_bin"] == b]
|
|
if len(grp) > 0:
|
|
print(f" RSI {b:<6} {len(grp):>4} trades, WR {grp['win'].mean()*100:5.1f}%, Avg PnL {grp['pnl_pips'].mean():+7.1f}p")
|
|
|
|
# RSI neutral vs momentum
|
|
neutral = trades[(trades["rsi_at_entry"] >= 40) & (trades["rsi_at_entry"] <= 60)]
|
|
momentum = trades[(trades["rsi_at_entry"] < 40) | (trades["rsi_at_entry"] > 60)]
|
|
print(f" RSI 40-60 (neutral): {len(neutral):>4} trades, WR {neutral['win'].mean()*100:5.1f}%, Avg PnL {neutral['pnl_pips'].mean():+7.1f}p")
|
|
print(f" RSI outside (momentum): {len(momentum):>4} trades, WR {momentum['win'].mean()*100:5.1f}%, Avg PnL {momentum['pnl_pips'].mean():+7.1f}p")
|
|
|
|
# 8. ADX buckets
|
|
print("\n8. ADX AT ENTRY")
|
|
adx_bins = [0, 20, 25, 30, 40, 100]
|
|
adx_labels = ["<20", "20-25", "25-30", "30-40", "40+"]
|
|
trades["adx_bin"] = pd.cut(trades["adx_at_entry"], bins=adx_bins, labels=adx_labels, include_lowest=True)
|
|
for b in adx_labels:
|
|
grp = trades[trades["adx_bin"] == b]
|
|
if len(grp) > 0:
|
|
print(f" ADX {b:<6} {len(grp):>4} trades, WR {grp['win'].mean()*100:5.1f}%, Avg PnL {grp['pnl_pips'].mean():+7.1f}p")
|
|
|
|
# 9. EMA50 distance
|
|
print("\n9. DISTANCE FROM EMA50 (absolute pips)")
|
|
trades["ema_dist_abs"] = trades["distance_from_ema50_pips"].abs()
|
|
dist_bins = [0, 20, 40, 60, 100, 500]
|
|
dist_labels = ["0-20", "20-40", "40-60", "60-100", "100+"]
|
|
trades["dist_bin"] = pd.cut(trades["ema_dist_abs"], bins=dist_bins, labels=dist_labels, include_lowest=True)
|
|
for b in dist_labels:
|
|
grp = trades[trades["dist_bin"] == b]
|
|
if len(grp) > 0:
|
|
print(f" {b:<8} {len(grp):>4} trades, WR {grp['win'].mean()*100:5.1f}%, Avg PnL {grp['pnl_pips'].mean():+7.1f}p")
|
|
|
|
# 10. Candle body ratio
|
|
print("\n10. CANDLE BODY RATIO")
|
|
br_bins = [0, 0.3, 0.5, 0.7, 1.01]
|
|
br_labels = ["<0.3", "0.3-0.5", "0.5-0.7", "0.7+"]
|
|
trades["br_bin"] = pd.cut(trades["candle_body_ratio"], bins=br_bins, labels=br_labels, include_lowest=True)
|
|
for b in br_labels:
|
|
grp = trades[trades["br_bin"] == b]
|
|
if len(grp) > 0:
|
|
print(f" {b:<8} {len(grp):>4} trades, WR {grp['win'].mean()*100:5.1f}%, Avg PnL {grp['pnl_pips'].mean():+7.1f}p")
|
|
|
|
# 11. Spread as % of avg win
|
|
avg_win_pips = winners["pnl_pips"].mean() if len(winners) > 0 else 0
|
|
avg_spread = trades["spread_at_entry"].mean()
|
|
print(f"\n11. SPREAD IMPACT")
|
|
print(f" Avg spread: {avg_spread:.1f} pips")
|
|
print(f" Avg win: {avg_win_pips:.1f} pips")
|
|
print(f" Spread/win: {avg_spread/avg_win_pips*100:.1f}%" if avg_win_pips > 0 else " N/A")
|
|
|
|
# 12. Yearly breakdown
|
|
print("\n12. YEARLY BREAKDOWN")
|
|
trades["year"] = pd.to_datetime(trades["timestamp"]).dt.year
|
|
for y, grp in trades.groupby("year"):
|
|
gw = grp[grp["pnl_pips"] > 0]["pnl_pips"].sum()
|
|
gl = abs(grp[grp["pnl_pips"] < 0]["pnl_pips"].sum())
|
|
pf = gw / gl if gl > 0 else 0
|
|
print(f" {y}: {len(grp):>4} trades, WR {grp['win'].mean()*100:5.1f}%, PF {pf:.2f}, PnL {grp['pnl_pips'].sum():+8.1f}p")
|
|
|
|
# 13. Session (if available)
|
|
if "session" in trades.columns:
|
|
print("\n13. SESSION")
|
|
for s, grp in trades.groupby("session"):
|
|
print(f" {s:<10} {len(grp):>4} trades, WR {grp['win'].mean()*100:5.1f}%, Avg PnL {grp['pnl_pips'].mean():+7.1f}p")
|
|
|
|
# Save trade log
|
|
out = os.path.join(os.path.dirname(os.path.dirname(__file__)), "results", "phase1", f"S9_{pair}_FULL_trades.csv")
|
|
trades.to_csv(out, index=False)
|
|
print(f"\nTrade log saved: {out}")
|