Files
XauBot/tests/test_modules.py
T
Vanszs a55148f232 feat: multi-TF SMC scalping pipeline + critical leakage fixes
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.
2026-06-06 17:33:35 +07:00

376 lines
11 KiB
Python

"""
Module Test Script
==================
Tests all modules to ensure they work correctly.
"""
# Run from project root: python tests/test_modules.py
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import polars as pl
import numpy as np
from datetime import datetime, timedelta
def create_test_data(n: int = 500) -> pl.DataFrame:
"""Create synthetic OHLCV data for testing."""
np.random.seed(42)
base_price = 2000.0
returns = np.random.randn(n) * 0.002
prices = base_price * np.exp(np.cumsum(returns))
return pl.DataFrame({
"time": [datetime.now() - timedelta(minutes=15*i) for i in range(n-1, -1, -1)],
"open": prices,
"high": prices * (1 + np.abs(np.random.randn(n)) * 0.001),
"low": prices * (1 - np.abs(np.random.randn(n)) * 0.001),
"close": prices * (1 + np.random.randn(n) * 0.0005),
"volume": np.random.randint(1000, 10000, n),
})
def test_config():
"""Test configuration module."""
print("\n" + "="*60)
print("Testing: src/config.py")
print("="*60)
from src.config import TradingConfig, CapitalMode
# Test small account
config_small = TradingConfig(capital=5000)
assert config_small.capital_mode == CapitalMode.SMALL
assert config_small.risk.risk_per_trade == 1.0
print(f"✓ Small account config: {config_small.capital_mode.value}")
# Test medium account
config_medium = TradingConfig(capital=50000)
assert config_medium.capital_mode == CapitalMode.MEDIUM
assert config_medium.risk.risk_per_trade == 0.5
print(f"✓ Medium account config: {config_medium.capital_mode.value}")
# Test position sizing
lot = config_small.calculate_position_size(2000, 1995)
assert lot > 0
print(f"✓ Position sizing: {lot} lots")
print("✓ Config module: PASSED")
def test_smc_polars():
"""Test SMC Polars module."""
print("\n" + "="*60)
print("Testing: src/smc_polars.py")
print("="*60)
from src.smc_polars import SMCAnalyzer, calculate_smc_summary
df = create_test_data(500)
analyzer = SMCAnalyzer(swing_length=5)
# Test swing points
df = analyzer.calculate_swing_points(df)
assert "swing_high" in df.columns
assert "swing_low" in df.columns
print(f"✓ Swing points calculated")
# Test FVG
df = analyzer.calculate_fvg(df)
assert "is_fvg_bull" in df.columns
assert "is_fvg_bear" in df.columns
print(f"✓ FVG calculated")
# Test Order Blocks
df = analyzer.calculate_order_blocks(df)
assert "ob" in df.columns
print(f"✓ Order Blocks calculated")
# Test BOS/CHoCH
df = analyzer.calculate_bos_choch(df)
assert "bos" in df.columns
assert "choch" in df.columns
print(f"✓ BOS/CHoCH calculated")
# Summary
summary = calculate_smc_summary(df)
print(f" - Swing Highs: {summary['swing_highs']}")
print(f" - Swing Lows: {summary['swing_lows']}")
print(f" - Bullish FVG: {summary['bullish_fvg']}")
print(f" - Bearish FVG: {summary['bearish_fvg']}")
print("✓ SMC Polars module: PASSED")
def test_feature_eng():
"""Test feature engineering module."""
print("\n" + "="*60)
print("Testing: src/feature_eng.py")
print("="*60)
from src.feature_eng import FeatureEngineer
df = create_test_data(200)
fe = FeatureEngineer()
# Test RSI
df = fe.calculate_rsi(df, period=14)
assert "rsi" in df.columns
rsi_range = df["rsi"].drop_nulls()
assert rsi_range.min() >= 0 and rsi_range.max() <= 100
print(f"✓ RSI calculated (range: {rsi_range.min():.1f} - {rsi_range.max():.1f})")
# Test ATR
df = fe.calculate_atr(df, period=14)
assert "atr" in df.columns
assert df["atr"].drop_nulls().min() >= 0
print(f"✓ ATR calculated")
# Test MACD
df = fe.calculate_macd(df)
assert "macd" in df.columns
assert "macd_signal" in df.columns
print(f"✓ MACD calculated")
# Test Bollinger Bands
df = fe.calculate_bollinger_bands(df)
assert "bb_upper" in df.columns
assert "bb_lower" in df.columns
print(f"✓ Bollinger Bands calculated")
# Test ML features
df = fe.calculate_ml_features(df)
assert "returns_1" in df.columns
assert "volatility_20" in df.columns
print(f"✓ ML features calculated")
# Get feature columns
feature_cols = fe.get_feature_columns(df)
print(f" - Total features: {len(feature_cols)}")
print("✓ Feature Engineering module: PASSED")
def test_regime_detector():
"""Test regime detector module."""
print("\n" + "="*60)
print("Testing: src/regime_detector.py")
print("="*60)
from src.regime_detector import MarketRegimeDetector, FlashCrashDetector
df = create_test_data(500)
# Test HMM detector
detector = MarketRegimeDetector(n_regimes=3)
detector.fit(df.head(400))
assert detector.fitted
print(f"✓ HMM fitted")
# Predict
df_pred = detector.predict(df)
assert "regime_name" in df_pred.columns
print(f"✓ Regime prediction")
# Get current state
state = detector.get_current_state(df)
print(f" - Current regime: {state.regime.value}")
print(f" - Confidence: {state.confidence:.2%}")
print(f" - Recommendation: {state.recommendation}")
# Test flash crash detector
fc_detector = FlashCrashDetector(threshold_percent=1.0)
is_flash, move = fc_detector.detect(df.tail(10))
print(f"✓ Flash crash detector (flash={is_flash}, move={move:.2f}%)")
print("✓ Regime Detector module: PASSED")
def test_risk_engine():
"""Test risk engine module."""
print("\n" + "="*60)
print("Testing: src/risk_engine.py")
print("="*60)
from src.config import TradingConfig
from src.risk_engine import RiskEngine
config = TradingConfig(capital=5000)
engine = RiskEngine(config)
# Test position sizing
result = engine.calculate_position_size(
entry_price=2000.0,
stop_loss_price=1995.0,
take_profit_price=2010.0,
account_balance=5000.0,
win_rate=0.55,
avg_win_loss_ratio=2.0,
)
assert result.lot_size > 0
print(f"✓ Position sizing: {result.lot_size} lots")
print(f" - Risk: ${result.risk_amount:.2f} ({result.risk_percent:.2f}%)")
# Test order validation
valid, reason = engine.validate_order(
order_type="BUY",
entry_price=2000.0,
stop_loss=1995.0,
take_profit=2010.0,
lot_size=result.lot_size,
current_price=2000.0,
account_balance=5000.0,
)
assert valid
print(f"✓ Order validation: {reason}")
# Test risk check
metrics = engine.check_risk(
account_balance=5000.0,
account_equity=4950.0,
open_positions=pl.DataFrame({"ticket": [], "volume": [], "symbol": []}),
current_price=2000.0,
)
print(f"✓ Risk check: can_trade={metrics.can_trade}")
print("✓ Risk Engine module: PASSED")
def test_ml_model():
"""Test ML model module."""
print("\n" + "="*60)
print("Testing: src/ml_model.py")
print("="*60)
from src.ml_model import TradingModel
# Create synthetic data with features
np.random.seed(42)
n = 500
df = pl.DataFrame({
"rsi": np.random.uniform(20, 80, n),
"atr": np.random.uniform(0.5, 2.0, n),
"macd": np.random.randn(n) * 0.001,
"returns_1": np.random.randn(n) * 0.01,
})
# Create target
target = ((df["rsi"].to_numpy() > 50).astype(int) * 0.5 +
np.random.randint(0, 2, n) * 0.5)
target = (target > 0.5).astype(int)
df = df.with_columns([pl.Series("target", target)])
# Test model
model = TradingModel(confidence_threshold=0.6)
feature_cols = ["rsi", "atr", "macd", "returns_1"]
model.fit(df, feature_cols, "target")
assert model.fitted
print(f"✓ Model trained")
# Test prediction
prediction = model.predict(df, feature_cols)
print(f"✓ Prediction: {prediction.signal} ({prediction.confidence:.2%})")
# Test feature importance
importance = model.get_feature_importance(3)
print(f"✓ Feature importance: {list(importance.keys())}")
print("✓ ML Model module: PASSED")
def test_utils():
"""Test utility module."""
print("\n" + "="*60)
print("Testing: src/utils.py")
print("="*60)
from src.utils import (
validate_ohlcv_data,
resample_ohlcv,
calculate_trade_statistics,
create_synthetic_data,
)
# Test synthetic data
df = create_synthetic_data(100)
assert len(df) == 100
print(f"✓ Synthetic data created")
# Test validation
is_valid, issues = validate_ohlcv_data(df)
assert is_valid
print(f"✓ Data validation: valid={is_valid}")
# Test resampling
df_resampled = resample_ohlcv(df, "1h")
assert len(df_resampled) < len(df)
print(f"✓ Resampling: {len(df)} -> {len(df_resampled)} bars")
# Test trade statistics
trades = [
{"pnl": 100, "is_win": True},
{"pnl": -50, "is_win": False},
{"pnl": 75, "is_win": True},
]
stats = calculate_trade_statistics(trades)
assert stats["total_trades"] == 3
print(f"✓ Trade statistics: win_rate={stats['win_rate']:.2%}")
print("✓ Utils module: PASSED")
def run_all_tests():
"""Run all module tests."""
print("\n" + "="*60)
print("SMART AUTOMATIC TRADING BOT + AI - MODULE TESTS")
print("="*60)
tests = [
("Config", test_config),
("SMC Polars", test_smc_polars),
("Feature Engineering", test_feature_eng),
("Regime Detector", test_regime_detector),
("Risk Engine", test_risk_engine),
("ML Model", test_ml_model),
("Utils", test_utils),
]
results = []
for name, test_func in tests:
try:
test_func()
results.append((name, True, None))
except Exception as e:
results.append((name, False, str(e)))
print(f"✗ {name} module: FAILED - {e}")
# Summary
print("\n" + "="*60)
print("TEST SUMMARY")
print("="*60)
passed = sum(1 for _, success, _ in results if success)
total = len(results)
for name, success, error in results:
status = "✓ PASSED" if success else f"✗ FAILED: {error}"
print(f" {name}: {status}")
print("-"*60)
print(f"Total: {passed}/{total} tests passed")
print("="*60)
return passed == total
if __name__ == "__main__":
success = run_all_tests()
sys.exit(0 if success else 1)