a55148f232
Add M1+M15 multi-timeframe SMC scalping training pipeline (GPU XGBoost), then fix data-leakage and non-stationarity issues found in a skeptical audit. Pipeline: - src/triple_barrier.py: TP/SL/time labeling (ATR-scaled, asymmetric RR) - src/multi_tf_dataset.py: M1 base + M15 HTF context, point-in-time join_asof (only CLOSED M15 candles visible to each M1 bar - proven no leakage) - src/economic_calendar.py: point-in-time forecast/actual/surprise provider - src/smc_polars.py: add premium/discount + displacement SMC features - scripts/train_multitf_scalper.py: GPU (device=cuda) training + walk-forward - scripts/download_training_data.py: 1y data downloader Leakage / robustness fixes (audit): - CRITICAL: order block signal was written to the ORIGIN bar (future info); now assigned at the CONFIRMATION bar -> matches live conditions - replace non-stationary absolute features (ema_9/21, macd*) with scale-free forms (ema*_dist_atr, ema_spread_atr, macd_*_bps) -> valid at any price level - drop constant-zero calendar features from defaults (recurring provider has no real values); re-add when a real calendar CSV is configured - walk-forward + train/test now embargo the max_holding label horizon and drop warmup rows (NaN->0 artifacts) - news calendar features remain point-in-time (actual only at/after release) Honest result: after fixes the spurious +2.35% edge collapses to ~random (AUC 0.49). The prior edge was caused by the order-block look-ahead. Pipeline is now leakage-free; a real edge still needs more M1 history / better features. Also: test infra (pytest.ini asyncio, hmmlearn), TRAIN_BARS, cleanup of dead modules. 14 tests pass.
220 lines
7.2 KiB
Python
220 lines
7.2 KiB
Python
"""
|
|
Test Phase 8 (Risk Metrics) and Phase 9 (Macro Data) Modules
|
|
=============================================================
|
|
|
|
Quick validation that both modules work correctly.
|
|
|
|
Usage:
|
|
python tests/test_phase8_phase9.py
|
|
|
|
Author: AI Assistant
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Add project root to path
|
|
project_root = Path(__file__).parent.parent
|
|
sys.path.insert(0, str(project_root))
|
|
|
|
import asyncio
|
|
import numpy as np
|
|
from loguru import logger
|
|
|
|
from src.risk_metrics import RiskAnalytics, quick_sharpe, quick_var, quick_max_drawdown
|
|
from src.macro_connector import MacroDataConnector, get_quick_macro_score
|
|
|
|
|
|
def test_risk_metrics():
|
|
"""Test risk metrics module."""
|
|
print("\n" + "=" * 60)
|
|
print("TEST 1: RISK METRICS MODULE")
|
|
print("=" * 60)
|
|
|
|
# Simulate equity curve (100 trades)
|
|
np.random.seed(42)
|
|
equity = [5000]
|
|
returns = []
|
|
|
|
for _ in range(100):
|
|
# Simulate realistic trading returns
|
|
# 55% win rate, avg win $8, avg loss $4
|
|
if np.random.rand() < 0.55:
|
|
profit = np.random.normal(8, 3) # Win
|
|
else:
|
|
profit = np.random.normal(-4, 2) # Loss
|
|
|
|
returns.append(profit)
|
|
equity.append(equity[-1] + profit)
|
|
|
|
print(f"\nSimulated Equity Curve:")
|
|
print(f" Starting Capital: ${equity[0]:,.2f}")
|
|
print(f" Ending Capital: ${equity[-1]:,.2f}")
|
|
print(f" Net P&L: ${equity[-1] - equity[0]:,.2f}")
|
|
print(f" Total Trades: {len(returns)}")
|
|
|
|
# Test 1: Quick functions
|
|
print("\n--- Quick Functions ---")
|
|
sharpe = quick_sharpe(returns)
|
|
var_95 = quick_var(returns, 0.95)
|
|
max_dd = quick_max_drawdown(equity)
|
|
|
|
print(f"Sharpe Ratio: {sharpe:.2f}")
|
|
print(f"VaR 95%: ${var_95:.2f}")
|
|
print(f"Max Drawdown: {max_dd:.2%}")
|
|
|
|
assert isinstance(sharpe, float), "Sharpe should be float"
|
|
assert isinstance(var_95, float), "VaR should be float"
|
|
assert isinstance(max_dd, float), "Max DD should be float"
|
|
print("[OK] Quick functions work correctly")
|
|
|
|
# Test 2: Comprehensive report
|
|
print("\n--- Comprehensive Report ---")
|
|
analytics = RiskAnalytics(risk_free_rate=0.04)
|
|
report = analytics.get_comprehensive_report(
|
|
equity_curve=equity,
|
|
trade_returns=returns,
|
|
periods_per_year=252
|
|
)
|
|
|
|
assert "error" not in report, "Report should not have errors"
|
|
assert "sharpe_ratio" in report, "Missing Sharpe ratio"
|
|
assert "sortino_ratio" in report, "Missing Sortino ratio"
|
|
assert "calmar_ratio" in report, "Missing Calmar ratio"
|
|
assert "win_rate" in report, "Missing win rate"
|
|
assert "profit_factor" in report, "Missing profit factor"
|
|
print("[OK] Comprehensive report generated")
|
|
|
|
# Test 3: Formatted output
|
|
print("\n--- Formatted Report ---")
|
|
formatted = analytics.format_report(report)
|
|
assert len(formatted) > 100, "Formatted report too short"
|
|
assert "RISK ANALYTICS REPORT" in formatted, "Missing header"
|
|
print("[OK] Report formatting works")
|
|
|
|
# Display key metrics
|
|
print(f"\nKey Metrics:")
|
|
print(f" Sharpe Ratio: {report['sharpe_ratio']:.2f}")
|
|
print(f" Sortino Ratio: {report['sortino_ratio']:.2f}")
|
|
print(f" Win Rate: {report['win_rate']:.1%}")
|
|
print(f" Profit Factor: {report['profit_factor']:.2f}")
|
|
print(f" Max Drawdown: {report['max_drawdown']:.2%}")
|
|
|
|
print("\n[PASS] Risk Metrics Module: ALL TESTS PASSED")
|
|
return True
|
|
|
|
|
|
async def test_macro_connector():
|
|
"""Test macro data connector module."""
|
|
print("\n" + "=" * 60)
|
|
print("TEST 2: MACRO DATA CONNECTOR MODULE")
|
|
print("=" * 60)
|
|
|
|
connector = MacroDataConnector()
|
|
|
|
# Test 1: Individual metrics
|
|
print("\n--- Individual Metrics ---")
|
|
dxy = await connector.get_dxy_index()
|
|
vix = await connector.get_vix_index()
|
|
real_yields = await connector.get_real_yields()
|
|
fed_funds = await connector.get_fed_funds_rate()
|
|
|
|
print(f"DXY (US Dollar Index): {dxy}")
|
|
print(f"VIX (Volatility Index): {vix}")
|
|
print(f"Real Yields (10Y TIPS): {real_yields}")
|
|
print(f"Fed Funds Rate: {fed_funds}")
|
|
|
|
# At least DXY and VIX should work (no API key needed)
|
|
assert dxy is None or isinstance(dxy, float), "DXY should be None or float"
|
|
assert vix is None or isinstance(vix, float), "VIX should be None or float"
|
|
print("[OK] Individual metric fetching works")
|
|
|
|
# Test 2: Macro score calculation
|
|
print("\n--- Macro Score Calculation ---")
|
|
macro_score, components = await connector.calculate_macro_score()
|
|
|
|
print(f"Macro Score: {macro_score:.2f} (0=Bearish, 0.5=Neutral, 1=Bullish)")
|
|
print(f"Components: {components}")
|
|
|
|
assert 0.0 <= macro_score <= 1.0, "Macro score out of range"
|
|
assert "dxy" in components, "Missing DXY component"
|
|
assert "vix" in components, "Missing VIX component"
|
|
print("[OK] Macro score calculation works")
|
|
|
|
# Test 3: Quick macro score function
|
|
print("\n--- Quick Macro Score ---")
|
|
quick_score = await get_quick_macro_score()
|
|
print(f"Quick Score: {quick_score:.2f}")
|
|
assert 0.0 <= quick_score <= 1.0, "Quick score out of range"
|
|
print("[OK] Quick macro score works")
|
|
|
|
# Test 4: Human-readable context
|
|
print("\n--- Macro Context Summary ---")
|
|
summary = await connector.get_macro_context()
|
|
assert len(summary) > 50, "Summary too short"
|
|
assert "MACRO CONTEXT" in summary, "Missing header"
|
|
print("[OK] Context summary generation works")
|
|
|
|
# Skip printing summary to avoid unicode issues in Windows console
|
|
# print("\n" + summary)
|
|
print(" (Summary generated successfully, length: {} chars)".format(len(summary)))
|
|
|
|
# Test 5: Caching mechanism
|
|
print("\n--- Cache Test ---")
|
|
print("Fetching DXY again (should use cache)...")
|
|
import time
|
|
start = time.time()
|
|
dxy_cached = await connector.get_dxy_index()
|
|
elapsed = time.time() - start
|
|
print(f"Second fetch took {elapsed*1000:.2f}ms")
|
|
# Only validate cache speed when the first fetch actually returned a value.
|
|
# If offline (dxy is None), nothing is cached and this check is moot.
|
|
if dxy is not None:
|
|
assert elapsed < 0.5, "Cache not working (took too long)"
|
|
assert dxy_cached == dxy, "Cached value different"
|
|
print("[OK] Caching mechanism works")
|
|
else:
|
|
print("[SKIP] DXY unavailable (offline) - cache timing not asserted")
|
|
|
|
print("\n[PASS] Macro Data Connector Module: ALL TESTS PASSED")
|
|
return True
|
|
|
|
|
|
async def main():
|
|
"""Run all tests."""
|
|
print("\n")
|
|
print("=" * 60)
|
|
print("TESTING PHASE 8 & PHASE 9 MODULES")
|
|
print("=" * 60)
|
|
print("Phase 8: Risk Metrics")
|
|
print("Phase 9: Macro Data Integration")
|
|
print("=" * 60)
|
|
|
|
try:
|
|
# Test 1: Risk Metrics
|
|
test_risk_metrics()
|
|
|
|
# Test 2: Macro Connector
|
|
await test_macro_connector()
|
|
|
|
print("\n" + "=" * 60)
|
|
print("[SUCCESS] ALL TESTS PASSED - MODULES READY FOR USE")
|
|
print("=" * 60)
|
|
print("\nUsage:")
|
|
print(" 1. Generate risk report: python scripts/generate_risk_report.py")
|
|
print(" 2. Check market + macro: python scripts/check_market.py")
|
|
print("=" * 60)
|
|
|
|
except Exception as e:
|
|
print(f"\n[FAIL] TEST FAILED: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
if __name__ == "__main__":
|
|
success = asyncio.run(main())
|
|
sys.exit(0 if success else 1)
|