Files
Trading-Bot/scripts/research/walk_forward_stops.py
DaM f85c522f22 feat: finalize portfolio system and quantitative validation- Finalized MA_Crossover(30,100) and TrendFiltered_MA(30,100,ADX=15)
- Implemented portfolio engine with risk-based allocation (50/50)
- Added equity-based metrics for system-level evaluation
- Validated portfolio against standalone strategies
- Reduced max drawdown and volatility at system level
- Quantitative decision closed before paper trading phase
2026-02-02 14:38:05 +01:00

223 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import sys
from pathlib import Path
from datetime import datetime, timedelta
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from dotenv import load_dotenv
# --------------------------------------------------
# Path setup
# --------------------------------------------------
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from src.utils.logger import log
from src.data.storage import StorageManager
from src.core.engine import Engine
from src.strategies import MovingAverageCrossover
from src.risk.sizing.percent_risk import PercentRiskSizer
from src.risk.stops.fixed_stop import FixedStop
from src.risk.stops.trailing_stop import TrailingStop
from src.risk.stops.atr_stop import ATRStop
# --------------------------------------------------
# CONFIG
# --------------------------------------------------
SYMBOL = "BTC/USDT"
TIMEFRAME = "1h"
TRAIN_DAYS = 120
TEST_DAYS = 30
STEP_DAYS = 30
INITIAL_CAPITAL = 10_000
COMMISSION = 0.001
SLIPPAGE = 0.0005
RISK_FRACTION = 0.01
OUT_DIR = Path("scripts/research/output/wf_stops") / f"{SYMBOL.replace('/', '_')}_{TIMEFRAME}"
OUT_DIR.mkdir(parents=True, exist_ok=True)
STOPS = {
"Fixed 2%": FixedStop(0.02),
"Trailing 2%": TrailingStop(0.02),
"ATR 14 x 2.0": ATRStop(14, 2.0),
}
# --------------------------------------------------
# Helpers
# --------------------------------------------------
def make_strategy():
return MovingAverageCrossover(
fast_period=10,
slow_period=30,
ma_type="ema",
use_adx=False,
adx_threshold=20.0,
)
def setup_env():
env_path = Path(__file__).parent.parent.parent / "config" / "secrets.env"
load_dotenv(env_path)
def load_data():
setup_env()
storage = StorageManager(
db_host=os.getenv("DB_HOST"),
db_port=int(os.getenv("DB_PORT", 5432)),
db_name=os.getenv("DB_NAME"),
db_user=os.getenv("DB_USER"),
db_password=os.getenv("DB_PASSWORD"),
)
data = storage.load_ohlcv(
symbol=SYMBOL,
timeframe=TIMEFRAME,
use_cache=True,
)
storage.close()
if data.empty:
raise RuntimeError("No data loaded")
return data
# --------------------------------------------------
# Walk Forward
# --------------------------------------------------
def run():
log.info("=" * 80)
log.info("🔁 WALK-FORWARD STOP COMPARISON (120/30/30, 1h)")
log.info("=" * 80)
data = load_data()
wf_results = []
equity_curves = {name: [] for name in STOPS.keys()}
start_time = data.index[0]
end_time = data.index[-1]
window_id = 0
current_train_start = start_time
while True:
train_end = current_train_start + timedelta(days=TRAIN_DAYS)
test_start = train_end
test_end = test_start + timedelta(days=TEST_DAYS)
if test_end > end_time:
break
train_df = data.loc[current_train_start:train_end]
test_df = data.loc[test_start:test_end]
if len(test_df) < 50:
break
window_id += 1
print()
print(
f"WF Window {window_id:02d} | "
f"TRAIN {train_df.index[0].date()}{train_df.index[-1].date()} | "
f"TEST {test_df.index[0].date()}{test_df.index[-1].date()} | "
f"bars_test={len(test_df)}"
)
for stop_name, stop in STOPS.items():
engine = Engine(
strategy=make_strategy(),
initial_capital=INITIAL_CAPITAL,
commission=COMMISSION,
slippage=SLIPPAGE,
position_sizer=PercentRiskSizer(RISK_FRACTION),
stop_loss=stop,
)
res = engine.run(test_df)
wf_results.append({
"window": window_id,
"stop": stop_name,
"train_start": train_df.index[0],
"train_end": train_df.index[-1],
"test_start": test_df.index[0],
"test_end": test_df.index[-1],
"trades": res["total_trades"],
"max_dd_pct": res["max_drawdown_pct"],
"return_pct": res["total_return_pct"],
"final_equity": res["final_equity"],
})
equity_curves[stop_name].append(
pd.Series(res["equity_curve"], index=res["timestamps"])
)
print(
f" {stop_name:<13} | "
f"Trades: {res['total_trades']:>3} | "
f"MaxDD: {res['max_drawdown_pct']:>7.2f}% | "
f"Return: {res['total_return_pct']:>7.2f}%"
)
current_train_start += timedelta(days=STEP_DAYS)
# --------------------------------------------------
# Save results
# --------------------------------------------------
df = pd.DataFrame(wf_results)
df.to_csv(OUT_DIR / "wf_results.csv", index=False)
print()
print("=" * 80)
print("📊 WF SUMMARY (aggregated)")
print("=" * 80)
summary = (
df.groupby("stop")
.agg(
windows=("window", "nunique"),
trades_avg=("trades", "mean"),
max_dd_worst=("max_dd_pct", "min"),
return_mean=("return_pct", "mean"),
return_median=("return_pct", "median"),
)
.round(2)
)
print(summary)
print("=" * 80)
# --------------------------------------------------
# Plot equity curves (visual comparison)
# --------------------------------------------------
plt.figure(figsize=(14, 7))
for stop_name, curves in equity_curves.items():
if not curves:
continue
concat_curve = pd.concat(curves)
plt.plot(concat_curve.index, concat_curve.values, label=stop_name)
plt.title(f"WF Equity Comparison {SYMBOL} {TIMEFRAME}")
plt.xlabel("Time")
plt.ylabel("Equity (per-window)")
plt.legend()
plt.grid(alpha=0.3)
plt.tight_layout()
plt.savefig(OUT_DIR / "wf_equity_comparison.png")
plt.close()
if __name__ == "__main__":
run()