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
This commit is contained in:
DaM
2026-02-02 14:38:05 +01:00
parent c569170fcc
commit f85c522f22
53 changed files with 2389 additions and 104 deletions

View File

@@ -0,0 +1,275 @@
# scripts/research/wf_compare_strategies.py
import os
import sys
from pathlib import Path
from dotenv import load_dotenv
from datetime import timedelta
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from src.core.engine import Engine
from src.data.storage import StorageManager
from src.risk.sizing.percent_risk import PercentRiskSizer
from src.risk.stops.trailing_stop import TrailingStop
from src.strategies.moving_average import MovingAverageCrossover
from src.strategies.trend_filtered import TrendFilteredMACrossover
from src.strategies.breakout import DonchianBreakout
from src.strategies.mean_reversion import RSIMeanReversion
from src.utils.logger import log
# --------------------------------------------------
# CONFIG
# --------------------------------------------------
SYMBOL = "BTC/USDT"
TIMEFRAME = "1h"
TRAIN_DAYS = 120
TEST_DAYS = 30
STEP_DAYS = 30
INITIAL_CAPITAL = 10_000
RISK = PercentRiskSizer(0.01)
STOP = TrailingStop(0.02)
OUTPUT_DIR = Path(__file__).parent / "output/wf_strategies"
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
# --------------------------------------------------
# STRATEGIES
# --------------------------------------------------
STRATEGIES = {
"MA_Crossover": lambda: MovingAverageCrossover(20, 50, "ema", False),
"TrendFiltered_MA": lambda: TrendFilteredMACrossover(20, 50, "ema", 20),
"Donchian": lambda: DonchianBreakout(20),
"RSI_Reversion": lambda: RSIMeanReversion(14, 30, 70),
}
# --------------------------------------------------
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, TIMEFRAME, use_cache=True)
storage.close()
if data.empty:
raise RuntimeError("No data loaded")
return data
# --------------------------------------------------
def compute_percentiles(curves, n_points=100):
x_common = np.linspace(0, 1, n_points)
interpolated = []
for curve in curves:
x = np.linspace(0, 1, len(curve))
interpolated.append(np.interp(x_common, x, curve.values))
arr = np.vstack(interpolated)
return (
x_common,
np.percentile(arr, 10, axis=0),
np.percentile(arr, 50, axis=0),
np.percentile(arr, 90, axis=0),
)
# --------------------------------------------------
def run():
log.info("🧪 WF MULTI-STRATEGY TEST (FULL VISUAL + STATS)")
data = load_data()
results_summary = []
all_accumulated = {}
all_dispersion_percentiles = {}
for name, strat_factory in STRATEGIES.items():
log.info(f"▶ Strategy: {name}")
capital = INITIAL_CAPITAL
equity_accumulated = []
dispersion_curves = []
window_returns = []
worst_dd = 0.0
start = data.index[0]
while True:
train_end = start + timedelta(days=TRAIN_DAYS)
test_end = train_end + timedelta(days=TEST_DAYS)
if test_end > data.index[-1]:
break
test_data = data.loc[train_end:test_end]
engine = Engine(
strategy=strat_factory(),
initial_capital=capital,
position_sizer=RISK,
stop_loss=STOP,
commission=0.001,
slippage=0.0005,
)
res = engine.run(test_data)
# ---- accumulated
capital *= (1 + res["total_return_pct"] / 100)
equity_accumulated.append((res["timestamps"][-1], capital))
# ---- dispersion (normalized reset)
eq = pd.Series(res["equity_curve"], index=res["timestamps"])
dispersion_curves.append(eq / eq.iloc[0])
window_returns.append(res["total_return_pct"])
worst_dd = min(worst_dd, res["max_drawdown_pct"])
start += timedelta(days=STEP_DAYS)
# ===============================
# STORE ACCUMULATED
# ===============================
acc_df = pd.DataFrame(equity_accumulated, columns=["time", "capital"]).set_index("time")
all_accumulated[name] = acc_df
# ===============================
# 1⃣ DISPERSION RAW (OLD)
# ===============================
plt.figure(figsize=(12, 5))
for eq in dispersion_curves:
plt.plot(eq.index, eq.values, alpha=0.3)
plt.title(f"WF Equity Dispersion (Raw) {name}")
plt.ylabel("Normalized Equity")
plt.grid(alpha=0.3)
path = OUTPUT_DIR / f"wf_equity_dispersion_raw_{name}.png"
plt.tight_layout()
plt.savefig(path)
plt.close()
# ===============================
# 2⃣ DISPERSION PERCENTILES (NEW)
# ===============================
x, p10, p50, p90 = compute_percentiles(dispersion_curves)
all_dispersion_percentiles[name] = (x, p10, p50, p90)
plt.figure(figsize=(12, 5))
plt.plot(x, p50, label="P50", linewidth=2)
plt.fill_between(x, p10, p90, alpha=0.3, label="P10P90")
plt.title(f"WF Equity Dispersion (Percentiles) {name}")
plt.ylabel("Normalized Equity")
plt.xlabel("Window Progress")
plt.legend()
plt.grid(alpha=0.3)
path = OUTPUT_DIR / f"wf_equity_dispersion_percentiles_{name}.png"
plt.tight_layout()
plt.savefig(path)
plt.close()
# ===============================
# 3⃣ RETURN DISTRIBUTION (OLD)
# ===============================
plt.figure(figsize=(10, 5))
plt.hist(window_returns, bins=20, density=True, alpha=0.7)
plt.title(f"WF Return Distribution {name}")
plt.xlabel("Return per window (%)")
plt.ylabel("Density")
plt.grid(alpha=0.3)
path = OUTPUT_DIR / f"wf_return_distribution_{name}.png"
plt.tight_layout()
plt.savefig(path)
plt.close()
# ===============================
# SUMMARY
# ===============================
results_summary.append({
"strategy": name,
"windows": len(window_returns),
"return_mean": round(np.mean(window_returns), 2),
"return_median": round(np.median(window_returns), 2),
"max_dd_worst": round(worst_dd, 2),
"final_capital": round(capital, 2),
})
# ===============================
# 4⃣ ACCUMULATED EQUITY ALL
# ===============================
plt.figure(figsize=(13, 6))
for name, acc_df in all_accumulated.items():
plt.plot(acc_df.index, acc_df["capital"], linewidth=2, label=name)
plt.title("WF Accumulated Equity Strategy Comparison")
plt.ylabel("Capital")
plt.legend()
plt.grid(alpha=0.3)
path = OUTPUT_DIR / "wf_equity_accumulated_ALL.png"
plt.tight_layout()
plt.savefig(path)
plt.close()
# ===============================
# 5⃣ DISPERSION COMPARISON P50
# ===============================
plt.figure(figsize=(12, 6))
for name, (x, _, p50, _) in all_dispersion_percentiles.items():
plt.plot(x, p50, linewidth=2, label=name)
plt.title("WF Dispersion Comparison (Median P50)")
plt.ylabel("Normalized Equity")
plt.xlabel("Window Progress")
plt.legend()
plt.grid(alpha=0.3)
path = OUTPUT_DIR / "wf_dispersion_comparison_P50.png"
plt.tight_layout()
plt.savefig(path)
plt.close()
# ===============================
# FINAL TABLE
# ===============================
summary_df = pd.DataFrame(results_summary).set_index("strategy")
print("\n" + "=" * 80)
print("📊 WF SUMMARY (ACCUMULATED)")
print("=" * 80)
print(summary_df)
print("=" * 80)
if __name__ == "__main__":
run()