Fix: test_all.py - all 174 tests passing
Browse files- test_all.py +817 -0
test_all.py
ADDED
|
@@ -0,0 +1,817 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Comprehensive Test Suite for Trading Intelligence System
|
| 4 |
+
=========================================================
|
| 5 |
+
Tests each module independently, then runs full integration.
|
| 6 |
+
Every assertion is checked, every output is validated.
|
| 7 |
+
"""
|
| 8 |
+
import sys, os, time, traceback, warnings
|
| 9 |
+
warnings.filterwarnings('ignore')
|
| 10 |
+
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', buffering=1)
|
| 11 |
+
sys.path.insert(0, '/app')
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
import pandas as pd
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
PASS = 0
|
| 18 |
+
FAIL = 0
|
| 19 |
+
|
| 20 |
+
def test(name, condition, detail=""):
|
| 21 |
+
global PASS, FAIL
|
| 22 |
+
if condition:
|
| 23 |
+
PASS += 1
|
| 24 |
+
print(f" β
{name}")
|
| 25 |
+
else:
|
| 26 |
+
FAIL += 1
|
| 27 |
+
print(f" β {name} β {detail}")
|
| 28 |
+
|
| 29 |
+
def section(title):
|
| 30 |
+
print(f"\n{'='*70}")
|
| 31 |
+
print(f" {title}")
|
| 32 |
+
print(f"{'='*70}")
|
| 33 |
+
|
| 34 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 35 |
+
# GENERATE SYNTHETIC DATA (shared across all tests)
|
| 36 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 37 |
+
section("DATA GENERATION")
|
| 38 |
+
np.random.seed(42)
|
| 39 |
+
num_days = 1500
|
| 40 |
+
dt = 1/252
|
| 41 |
+
prices = [150.0]
|
| 42 |
+
vol = 0.20
|
| 43 |
+
|
| 44 |
+
for i in range(num_days - 1):
|
| 45 |
+
vol = vol + 0.1 * (0.20 - vol) * dt + 0.3 * np.sqrt(dt) * np.random.normal()
|
| 46 |
+
vol = max(vol, 0.05)
|
| 47 |
+
ret = (0.08 - 0.5 * vol**2) * dt + vol * np.sqrt(dt) * np.random.normal()
|
| 48 |
+
prices.append(prices[-1] * np.exp(ret))
|
| 49 |
+
|
| 50 |
+
df = pd.DataFrame({
|
| 51 |
+
'date': pd.date_range('2019-01-02', periods=num_days, freq='B')[:num_days],
|
| 52 |
+
'open': [p * (1 + np.random.normal(0, 0.002)) for p in prices],
|
| 53 |
+
'high': [p * (1 + abs(np.random.normal(0, 0.01))) for p in prices],
|
| 54 |
+
'low': [p * (1 - abs(np.random.normal(0, 0.01))) for p in prices],
|
| 55 |
+
'close': prices,
|
| 56 |
+
'volume': [int(1e6 * np.exp(np.random.normal(0, 0.3))) for _ in range(num_days)],
|
| 57 |
+
})
|
| 58 |
+
df['high'] = df[['open', 'high', 'close']].max(axis=1) * (1 + abs(np.random.normal(0, 0.002, num_days)))
|
| 59 |
+
df['low'] = df[['open', 'low', 'close']].min(axis=1) * (1 - abs(np.random.normal(0, 0.002, num_days)))
|
| 60 |
+
|
| 61 |
+
test("Data generated", len(df) == num_days, f"got {len(df)}")
|
| 62 |
+
test("OHLCV columns present", all(c in df.columns for c in ['open','high','low','close','volume']))
|
| 63 |
+
test("No NaN in raw data", df[['open','high','low','close','volume']].isna().sum().sum() == 0)
|
| 64 |
+
test("High >= Low", (df['high'] >= df['low']).all())
|
| 65 |
+
test("High >= Close", (df['high'] >= df['close']).all())
|
| 66 |
+
test("Low <= Close", (df['low'] <= df['close']).all())
|
| 67 |
+
print(f" Price range: ${min(prices):.2f} β ${max(prices):.2f}")
|
| 68 |
+
|
| 69 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 70 |
+
# TEST 1: FEATURE ENGINE
|
| 71 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 72 |
+
section("TEST 1: FEATURE ENGINE")
|
| 73 |
+
try:
|
| 74 |
+
from trading_intelligence.feature_engine import FeatureEngine, SentimentFeatureEngine
|
| 75 |
+
|
| 76 |
+
fe = FeatureEngine(lookback_window=30, prediction_horizons=[1, 5, 20])
|
| 77 |
+
features = fe.compute_all_features(df)
|
| 78 |
+
|
| 79 |
+
test("Feature engine initializes", True)
|
| 80 |
+
test("Features computed", len(features) > 0, f"got {len(features)} rows")
|
| 81 |
+
test("No NaN after dropna", features[fe.feature_names].isna().sum().sum() == 0)
|
| 82 |
+
test("Feature count >= 60", len(fe.feature_names) >= 60, f"got {len(fe.feature_names)}")
|
| 83 |
+
|
| 84 |
+
# Check specific feature groups
|
| 85 |
+
price_feats = [f for f in fe.feature_names if 'return' in f or 'momentum' in f or 'body' in f]
|
| 86 |
+
tech_feats = [f for f in fe.feature_names if 'rsi' in f or 'macd' in f or 'ema' in f or 'bb_' in f]
|
| 87 |
+
vol_feats = [f for f in fe.feature_names if 'vol' in f.lower() and 'obv' not in f]
|
| 88 |
+
regime_feats = [f for f in fe.feature_names if 'regime' in f or 'trend' in f or 'hurst' in f]
|
| 89 |
+
|
| 90 |
+
test("Price features exist", len(price_feats) > 5, f"got {len(price_feats)}")
|
| 91 |
+
test("Technical indicators exist", len(tech_feats) > 10, f"got {len(tech_feats)}")
|
| 92 |
+
test("Volatility features exist", len(vol_feats) > 5, f"got {len(vol_feats)}")
|
| 93 |
+
test("Regime features exist", len(regime_feats) > 2, f"got {len(regime_feats)}")
|
| 94 |
+
|
| 95 |
+
# Check targets
|
| 96 |
+
for h in [1, 5, 20]:
|
| 97 |
+
test(f"Target direction_{h} exists", f'target_direction_{h}' in features.columns)
|
| 98 |
+
test(f"Target return_{h} exists", f'target_return_{h}' in features.columns)
|
| 99 |
+
vals = features[f'target_direction_{h}'].dropna().unique()
|
| 100 |
+
test(f"Direction_{h} is binary", set(vals).issubset({0.0, 1.0}), f"got {vals[:5]}")
|
| 101 |
+
|
| 102 |
+
# Normalization
|
| 103 |
+
features_norm, norm_params = fe.normalize_features(features)
|
| 104 |
+
test("Normalization produces params", len(norm_params) > 0)
|
| 105 |
+
test("Normalized features have similar scale",
|
| 106 |
+
abs(features_norm[fe.feature_names].mean().mean()) < 1.0,
|
| 107 |
+
f"mean={features_norm[fe.feature_names].mean().mean():.4f}")
|
| 108 |
+
|
| 109 |
+
# Sequence creation
|
| 110 |
+
target_cols = []
|
| 111 |
+
for h in [1, 5, 20]:
|
| 112 |
+
target_cols.extend([f'target_direction_{h}', f'target_return_{h}'])
|
| 113 |
+
|
| 114 |
+
X, y = fe.create_sequences(features_norm, target_cols=target_cols)
|
| 115 |
+
valid = np.isfinite(X).all(axis=(1, 2)) & np.isfinite(y).all(axis=1)
|
| 116 |
+
X, y = X[valid], y[valid]
|
| 117 |
+
|
| 118 |
+
test("Sequences created", X.shape[0] > 0)
|
| 119 |
+
test("X shape correct (N, C, L)", len(X.shape) == 3)
|
| 120 |
+
test("X channels match features", X.shape[1] == len(fe.feature_names),
|
| 121 |
+
f"{X.shape[1]} vs {len(fe.feature_names)}")
|
| 122 |
+
test("X lookback correct", X.shape[2] == 30, f"got {X.shape[2]}")
|
| 123 |
+
test("Y targets = 6 (3 horizons Γ 2)", y.shape[1] == 6, f"got {y.shape[1]}")
|
| 124 |
+
test("No NaN/Inf in X", np.isfinite(X).all())
|
| 125 |
+
test("No NaN/Inf in y", np.isfinite(y).all())
|
| 126 |
+
|
| 127 |
+
# Sentiment engine
|
| 128 |
+
se = SentimentFeatureEngine()
|
| 129 |
+
score = se.compute_rule_based_sentiment("Stock upgraded, strong growth expected, bullish outlook")
|
| 130 |
+
test("Sentiment positive for bullish text", score > 0, f"score={score:.3f}")
|
| 131 |
+
score_neg = se.compute_rule_based_sentiment("Stock crashed, massive loss, bearish outlook")
|
| 132 |
+
test("Sentiment negative for bearish text", score_neg < 0, f"score={score_neg:.3f}")
|
| 133 |
+
|
| 134 |
+
print(f"\n π Feature Summary: {len(fe.feature_names)} features, {X.shape[0]} samples")
|
| 135 |
+
print(f" Feature names: {fe.feature_names[:10]}...")
|
| 136 |
+
|
| 137 |
+
except Exception as e:
|
| 138 |
+
test("Feature engine module", False, f"EXCEPTION: {e}")
|
| 139 |
+
traceback.print_exc()
|
| 140 |
+
|
| 141 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 142 |
+
# TEST 2: PREDICTION MODEL
|
| 143 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 144 |
+
section("TEST 2: PREDICTION MODEL")
|
| 145 |
+
try:
|
| 146 |
+
from trading_intelligence.prediction_model import (
|
| 147 |
+
PatchEmbedding, PositionalEncoding, MultiHeadAttention,
|
| 148 |
+
TransformerBlock, ChannelMixer, PredictionHead,
|
| 149 |
+
TradingTransformer, MultiTaskLoss
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
device = torch.device('cpu')
|
| 153 |
+
num_channels = X.shape[1]
|
| 154 |
+
batch_size = 16
|
| 155 |
+
|
| 156 |
+
# Test PatchEmbedding
|
| 157 |
+
pe = PatchEmbedding(patch_len=6, stride=3, d_model=64)
|
| 158 |
+
test_input = torch.randn(batch_size, num_channels, 30)
|
| 159 |
+
patches = pe(test_input)
|
| 160 |
+
test("PatchEmbedding forward", patches.shape[0] == batch_size)
|
| 161 |
+
test("PatchEmbedding output 4D", len(patches.shape) == 4)
|
| 162 |
+
test("PatchEmbedding d_model=64", patches.shape[-1] == 64)
|
| 163 |
+
|
| 164 |
+
# Test PositionalEncoding
|
| 165 |
+
pos = PositionalEncoding(d_model=64)
|
| 166 |
+
pos_out = pos(patches)
|
| 167 |
+
test("PositionalEncoding same shape", pos_out.shape == patches.shape)
|
| 168 |
+
|
| 169 |
+
# Test MultiHeadAttention
|
| 170 |
+
mha = MultiHeadAttention(d_model=64, n_heads=4)
|
| 171 |
+
attn_input = torch.randn(batch_size, 10, 64)
|
| 172 |
+
attn_out = mha(attn_input)
|
| 173 |
+
test("MHA forward", attn_out.shape == attn_input.shape)
|
| 174 |
+
|
| 175 |
+
# Test TransformerBlock
|
| 176 |
+
tb = TransformerBlock(d_model=64, n_heads=4, d_ff=128)
|
| 177 |
+
tb_out = tb(attn_input)
|
| 178 |
+
test("TransformerBlock forward", tb_out.shape == attn_input.shape)
|
| 179 |
+
|
| 180 |
+
# Test ChannelMixer
|
| 181 |
+
cm = ChannelMixer(num_channels=num_channels, d_model=64, n_heads=4)
|
| 182 |
+
cm_out = cm(patches)
|
| 183 |
+
test("ChannelMixer forward", cm_out.shape == patches.shape)
|
| 184 |
+
|
| 185 |
+
# Test PredictionHead
|
| 186 |
+
ph = PredictionHead(d_model=64, num_horizons=3)
|
| 187 |
+
ph_input = torch.randn(batch_size, 64)
|
| 188 |
+
ph_out = ph(ph_input)
|
| 189 |
+
test("PredictionHead returns dict", isinstance(ph_out, dict))
|
| 190 |
+
test("PredictionHead has direction_logits", 'direction_logits' in ph_out)
|
| 191 |
+
test("PredictionHead has expected_return", 'expected_return' in ph_out)
|
| 192 |
+
test("PredictionHead has log_variance", 'log_variance' in ph_out)
|
| 193 |
+
test("Direction shape (B, 3)", ph_out['direction_logits'].shape == (batch_size, 3))
|
| 194 |
+
|
| 195 |
+
# Test Full TradingTransformer
|
| 196 |
+
model = TradingTransformer(
|
| 197 |
+
num_channels=num_channels, seq_len=30, patch_len=6, stride=3,
|
| 198 |
+
d_model=64, n_heads=4, n_layers=2, d_ff=128,
|
| 199 |
+
num_horizons=3, dropout=0.1,
|
| 200 |
+
).to(device)
|
| 201 |
+
|
| 202 |
+
param_count = sum(p.numel() for p in model.parameters())
|
| 203 |
+
test("Model instantiates", True)
|
| 204 |
+
test("Model has parameters", param_count > 0, f"{param_count:,} params")
|
| 205 |
+
|
| 206 |
+
x_batch = torch.FloatTensor(X[:batch_size]).to(device)
|
| 207 |
+
output = model(x_batch)
|
| 208 |
+
test("Model forward pass", isinstance(output, dict))
|
| 209 |
+
test("Output direction_logits shape", output['direction_logits'].shape == (batch_size, 3))
|
| 210 |
+
test("Output expected_return shape", output['expected_return'].shape == (batch_size, 3))
|
| 211 |
+
test("Output log_variance shape", output['log_variance'].shape == (batch_size, 3))
|
| 212 |
+
test("No NaN in output", all(torch.isfinite(v).all().item() for v in output.values()))
|
| 213 |
+
|
| 214 |
+
# Test predict_with_confidence
|
| 215 |
+
preds = model.predict_with_confidence(x_batch)
|
| 216 |
+
test("predict_with_confidence returns dict", isinstance(preds, dict))
|
| 217 |
+
test("direction_probs in [0,1]", (preds['direction_probs'] >= 0).all() and (preds['direction_probs'] <= 1).all())
|
| 218 |
+
test("confidence in [0,1]", (preds['confidence'] >= 0).all() and (preds['confidence'] <= 1).all())
|
| 219 |
+
|
| 220 |
+
# Test MultiTaskLoss
|
| 221 |
+
loss_fn = MultiTaskLoss(num_horizons=3).to(device)
|
| 222 |
+
y_batch = torch.FloatTensor(y[:batch_size]).to(device)
|
| 223 |
+
directions = torch.stack([y_batch[:, i*2] for i in range(3)], dim=1)
|
| 224 |
+
returns = torch.stack([y_batch[:, i*2+1] for i in range(3)], dim=1)
|
| 225 |
+
targets = {'direction': directions, 'returns': returns}
|
| 226 |
+
|
| 227 |
+
losses = loss_fn(output, targets)
|
| 228 |
+
test("Loss computes", isinstance(losses, dict))
|
| 229 |
+
test("total_loss is scalar", losses['total_loss'].dim() == 0)
|
| 230 |
+
test("total_loss is finite", torch.isfinite(losses['total_loss']).item())
|
| 231 |
+
test("direction_loss exists", 'direction_loss' in losses)
|
| 232 |
+
test("return_loss exists", 'return_loss' in losses)
|
| 233 |
+
test("risk_loss exists", 'risk_loss' in losses)
|
| 234 |
+
|
| 235 |
+
# Test backward pass
|
| 236 |
+
losses['total_loss'].backward()
|
| 237 |
+
grads_ok = all(p.grad is not None and torch.isfinite(p.grad).all() for p in model.parameters() if p.requires_grad)
|
| 238 |
+
test("Backward pass - gradients computed", grads_ok)
|
| 239 |
+
|
| 240 |
+
print(f"\n π§ Model: {param_count:,} params, output verified across all heads")
|
| 241 |
+
|
| 242 |
+
except Exception as e:
|
| 243 |
+
test("Prediction model module", False, f"EXCEPTION: {e}")
|
| 244 |
+
traceback.print_exc()
|
| 245 |
+
|
| 246 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 247 |
+
# TEST 3: TRAINING LOOP (abbreviated)
|
| 248 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 249 |
+
section("TEST 3: TRAINING LOOP")
|
| 250 |
+
try:
|
| 251 |
+
from torch.utils.data import TensorDataset, DataLoader
|
| 252 |
+
|
| 253 |
+
# Split data
|
| 254 |
+
n = len(X)
|
| 255 |
+
train_end = int(n * 0.7)
|
| 256 |
+
val_end = int(n * 0.85)
|
| 257 |
+
X_train, y_train = X[:train_end], y[:train_end]
|
| 258 |
+
X_val, y_val = X[train_end:val_end], y[train_end:val_end]
|
| 259 |
+
X_test, y_test = X[val_end:], y[val_end:]
|
| 260 |
+
|
| 261 |
+
test("Train set size > 0", len(X_train) > 0, f"{len(X_train)}")
|
| 262 |
+
test("Val set size > 0", len(X_val) > 0, f"{len(X_val)}")
|
| 263 |
+
test("Test set size > 0", len(X_test) > 0, f"{len(X_test)}")
|
| 264 |
+
|
| 265 |
+
# Re-init model fresh
|
| 266 |
+
model = TradingTransformer(
|
| 267 |
+
num_channels=num_channels, seq_len=30, patch_len=6, stride=3,
|
| 268 |
+
d_model=64, n_heads=4, n_layers=2, d_ff=128,
|
| 269 |
+
num_horizons=3, dropout=0.1,
|
| 270 |
+
).to(device)
|
| 271 |
+
loss_fn = MultiTaskLoss(num_horizons=3).to(device)
|
| 272 |
+
optimizer = torch.optim.AdamW(
|
| 273 |
+
list(model.parameters()) + list(loss_fn.parameters()),
|
| 274 |
+
lr=1e-3, weight_decay=1e-4
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
train_loader = DataLoader(
|
| 278 |
+
TensorDataset(torch.FloatTensor(X_train), torch.FloatTensor(y_train)),
|
| 279 |
+
batch_size=128, shuffle=True
|
| 280 |
+
)
|
| 281 |
+
val_loader = DataLoader(
|
| 282 |
+
TensorDataset(torch.FloatTensor(X_val), torch.FloatTensor(y_val)),
|
| 283 |
+
batch_size=128, shuffle=False
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
# Train 5 epochs and check loss decreases
|
| 287 |
+
train_losses = []
|
| 288 |
+
val_losses = []
|
| 289 |
+
val_accs = []
|
| 290 |
+
|
| 291 |
+
for epoch in range(5):
|
| 292 |
+
# Train
|
| 293 |
+
model.train()
|
| 294 |
+
epoch_loss = 0
|
| 295 |
+
n_batch = 0
|
| 296 |
+
for xb, yb in train_loader:
|
| 297 |
+
xb, yb = xb.to(device), yb.to(device)
|
| 298 |
+
preds = model(xb)
|
| 299 |
+
dirs = torch.stack([yb[:, i*2] for i in range(3)], dim=1)
|
| 300 |
+
rets = torch.stack([yb[:, i*2+1] for i in range(3)], dim=1)
|
| 301 |
+
loss_dict = loss_fn(preds, {'direction': dirs, 'returns': rets})
|
| 302 |
+
optimizer.zero_grad()
|
| 303 |
+
loss_dict['total_loss'].backward()
|
| 304 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
| 305 |
+
optimizer.step()
|
| 306 |
+
epoch_loss += loss_dict['total_loss'].item()
|
| 307 |
+
n_batch += 1
|
| 308 |
+
train_losses.append(epoch_loss / n_batch)
|
| 309 |
+
|
| 310 |
+
# Validate
|
| 311 |
+
model.eval()
|
| 312 |
+
v_loss = 0
|
| 313 |
+
v_batches = 0
|
| 314 |
+
correct = 0
|
| 315 |
+
total = 0
|
| 316 |
+
with torch.no_grad():
|
| 317 |
+
for xb, yb in val_loader:
|
| 318 |
+
xb, yb = xb.to(device), yb.to(device)
|
| 319 |
+
preds = model(xb)
|
| 320 |
+
dirs = torch.stack([yb[:, i*2] for i in range(3)], dim=1)
|
| 321 |
+
rets = torch.stack([yb[:, i*2+1] for i in range(3)], dim=1)
|
| 322 |
+
loss_dict = loss_fn(preds, {'direction': dirs, 'returns': rets})
|
| 323 |
+
v_loss += loss_dict['total_loss'].item()
|
| 324 |
+
v_batches += 1
|
| 325 |
+
dir_preds = (torch.sigmoid(preds['direction_logits']) > 0.5).float()
|
| 326 |
+
correct += (dir_preds[:, 0] == dirs[:, 0]).sum().item()
|
| 327 |
+
total += len(xb)
|
| 328 |
+
val_losses.append(v_loss / v_batches)
|
| 329 |
+
val_accs.append(correct / total)
|
| 330 |
+
|
| 331 |
+
print(f" Epoch {epoch+1}: train_loss={train_losses[-1]:.4f} val_loss={val_losses[-1]:.4f} DA-1d={val_accs[-1]:.1%}")
|
| 332 |
+
|
| 333 |
+
test("Training runs without error", True)
|
| 334 |
+
test("Train loss decreases", train_losses[-1] < train_losses[0],
|
| 335 |
+
f"{train_losses[0]:.4f} β {train_losses[-1]:.4f}")
|
| 336 |
+
test("Val loss decreases", val_losses[-1] < val_losses[0],
|
| 337 |
+
f"{val_losses[0]:.4f} β {val_losses[-1]:.4f}")
|
| 338 |
+
test("Direction accuracy > random (40%)", val_accs[-1] > 0.40, f"{val_accs[-1]:.1%}")
|
| 339 |
+
test("No NaN in losses", all(np.isfinite(l) for l in train_losses + val_losses))
|
| 340 |
+
|
| 341 |
+
# Save and reload
|
| 342 |
+
os.makedirs('/app/models', exist_ok=True)
|
| 343 |
+
save_path = '/app/models/test_model.pt'
|
| 344 |
+
torch.save({
|
| 345 |
+
'model_state': model.state_dict(),
|
| 346 |
+
'config': {'num_channels': num_channels, 'd_model': 64, 'n_heads': 4,
|
| 347 |
+
'n_layers': 2, 'd_ff': 128, 'patch_len': 6, 'stride': 3}
|
| 348 |
+
}, save_path)
|
| 349 |
+
test("Model saves", os.path.exists(save_path))
|
| 350 |
+
|
| 351 |
+
checkpoint = torch.load(save_path, map_location='cpu', weights_only=False)
|
| 352 |
+
model2 = TradingTransformer(
|
| 353 |
+
num_channels=num_channels, seq_len=30, patch_len=6, stride=3,
|
| 354 |
+
d_model=64, n_heads=4, n_layers=2, d_ff=128, num_horizons=3
|
| 355 |
+
)
|
| 356 |
+
model2.load_state_dict(checkpoint['model_state'])
|
| 357 |
+
model2.eval()
|
| 358 |
+
with torch.no_grad():
|
| 359 |
+
out1 = model(torch.FloatTensor(X[:4]))
|
| 360 |
+
out2 = model2(torch.FloatTensor(X[:4]))
|
| 361 |
+
test("Saved/loaded model produces same output",
|
| 362 |
+
torch.allclose(out1['direction_logits'], out2['direction_logits'], atol=1e-5))
|
| 363 |
+
|
| 364 |
+
print(f"\n π Training verified: loss {train_losses[0]:.4f} β {train_losses[-1]:.4f}")
|
| 365 |
+
|
| 366 |
+
except Exception as e:
|
| 367 |
+
test("Training loop", False, f"EXCEPTION: {e}")
|
| 368 |
+
traceback.print_exc()
|
| 369 |
+
|
| 370 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 371 |
+
# TEST 4: RISK MODEL
|
| 372 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 373 |
+
section("TEST 4: RISK MODEL")
|
| 374 |
+
try:
|
| 375 |
+
from trading_intelligence.risk_model import (
|
| 376 |
+
PortfolioEncoder, TraderBehaviorAnalyzer, RiskModel, RiskLoss
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
B = 4
|
| 380 |
+
|
| 381 |
+
# Test PortfolioEncoder
|
| 382 |
+
pe = PortfolioEncoder(position_dim=8, max_positions=5, d_model=64)
|
| 383 |
+
positions = torch.randn(B, 5, 8)
|
| 384 |
+
account = torch.randn(B, 6)
|
| 385 |
+
mask = torch.ones(B, 5, dtype=torch.bool)
|
| 386 |
+
mask[:, 3:] = False
|
| 387 |
+
|
| 388 |
+
port_repr = pe(positions, account, mask)
|
| 389 |
+
test("PortfolioEncoder forward", port_repr.shape == (B, 64))
|
| 390 |
+
test("PortfolioEncoder no NaN", torch.isfinite(port_repr).all())
|
| 391 |
+
|
| 392 |
+
# Test TraderBehaviorAnalyzer
|
| 393 |
+
tba = TraderBehaviorAnalyzer(trade_dim=12, d_model=64, n_layers=2)
|
| 394 |
+
trade_hist = torch.randn(B, 20, 12)
|
| 395 |
+
behavior = tba(trade_hist)
|
| 396 |
+
test("BehaviorAnalyzer returns dict", isinstance(behavior, dict))
|
| 397 |
+
test("risk_appetite shape", behavior['risk_appetite'].shape == (B,))
|
| 398 |
+
test("risk_appetite in [0,1]", (behavior['risk_appetite'] >= 0).all() and (behavior['risk_appetite'] <= 1).all())
|
| 399 |
+
test("overtrading_prob in [0,1]", (behavior['overtrading_prob'] >= 0).all() and (behavior['overtrading_prob'] <= 1).all())
|
| 400 |
+
test("revenge_trading_prob in [0,1]", (behavior['revenge_trading_prob'] >= 0).all() and (behavior['revenge_trading_prob'] <= 1).all())
|
| 401 |
+
test("trader_type_logits shape", behavior['trader_type_logits'].shape == (B, 5))
|
| 402 |
+
test("behavior_embedding shape", behavior['behavior_embedding'].shape == (B, 64))
|
| 403 |
+
|
| 404 |
+
# Test Full RiskModel
|
| 405 |
+
rm = RiskModel(market_dim=64, portfolio_dim=64, behavior_dim=64)
|
| 406 |
+
rm.eval()
|
| 407 |
+
|
| 408 |
+
market_state = torch.randn(B, 64)
|
| 409 |
+
with torch.no_grad():
|
| 410 |
+
risk_out = rm(market_state, positions, account, trade_hist, mask)
|
| 411 |
+
|
| 412 |
+
test("RiskModel returns dict", isinstance(risk_out, dict))
|
| 413 |
+
test("risk_score shape", risk_out['risk_score'].shape == (B,))
|
| 414 |
+
test("risk_score in [0,1]", (risk_out['risk_score'] >= 0).all() and (risk_out['risk_score'] <= 1).all())
|
| 415 |
+
test("adjusted_position_size in [0,1]", (risk_out['adjusted_position_size'] >= 0).all() and (risk_out['adjusted_position_size'] <= 1).all())
|
| 416 |
+
test("stop_loss_atr_mult >= 0", (risk_out['stop_loss_atr_mult'] >= 0).all())
|
| 417 |
+
test("take_profit_atr_mult >= 0", (risk_out['take_profit_atr_mult'] >= 0).all())
|
| 418 |
+
test("drawdown_probs shape", risk_out['drawdown_probs'].shape == (B, 4))
|
| 419 |
+
test("drawdown_probs in [0,1]", (risk_out['drawdown_probs'] >= 0).all() and (risk_out['drawdown_probs'] <= 1).all())
|
| 420 |
+
test("var_estimates shape", risk_out['var_estimates'].shape == (B, 3))
|
| 421 |
+
test("behavior_profile in output", 'behavior_profile' in risk_out)
|
| 422 |
+
|
| 423 |
+
# Test RiskLoss
|
| 424 |
+
rl = RiskLoss()
|
| 425 |
+
targets = {
|
| 426 |
+
'actual_risk': torch.rand(B),
|
| 427 |
+
'optimal_position_size': torch.rand(B),
|
| 428 |
+
'drawdown_occurred': torch.rand(B, 4),
|
| 429 |
+
}
|
| 430 |
+
risk_losses = rl(risk_out, targets)
|
| 431 |
+
test("RiskLoss computes", 'total_loss' in risk_losses)
|
| 432 |
+
test("RiskLoss finite", torch.isfinite(risk_losses['total_loss']))
|
| 433 |
+
|
| 434 |
+
print(f"\n π‘οΈ Risk model: all outputs verified, shapes correct")
|
| 435 |
+
|
| 436 |
+
except Exception as e:
|
| 437 |
+
test("Risk model module", False, f"EXCEPTION: {e}")
|
| 438 |
+
traceback.print_exc()
|
| 439 |
+
|
| 440 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 441 |
+
# TEST 5: PERSONALIZATION
|
| 442 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 443 |
+
section("TEST 5: PERSONALIZATION")
|
| 444 |
+
try:
|
| 445 |
+
from trading_intelligence.personalization import (
|
| 446 |
+
TraderProfiler, BehaviorAlertSystem, PersonalizationEngine, TRADER_TYPES
|
| 447 |
+
)
|
| 448 |
+
|
| 449 |
+
test("5 trader types defined", len(TRADER_TYPES) == 5)
|
| 450 |
+
|
| 451 |
+
profiler = TraderProfiler()
|
| 452 |
+
|
| 453 |
+
# Test with conservative trader
|
| 454 |
+
conservative_trades = [
|
| 455 |
+
{'entry_price': 100, 'exit_price': 101, 'size': 0.01, 'pnl': 10, 'holding_time': 2880, 'direction': 1}
|
| 456 |
+
] * 20 + [
|
| 457 |
+
{'entry_price': 100, 'exit_price': 99.5, 'size': 0.01, 'pnl': -5, 'holding_time': 1440, 'direction': 1}
|
| 458 |
+
] * 8
|
| 459 |
+
|
| 460 |
+
feats = profiler.extract_behavior_features(conservative_trades)
|
| 461 |
+
test("Feature extraction returns array", isinstance(feats, np.ndarray))
|
| 462 |
+
test("15 behavior features", len(feats) == 15, f"got {len(feats)}")
|
| 463 |
+
test("Win rate correct", abs(feats[0] - 20/28) < 0.01, f"got {feats[0]:.3f}")
|
| 464 |
+
|
| 465 |
+
profile = profiler.predict_type(feats)
|
| 466 |
+
test("Profile returns dict", isinstance(profile, dict))
|
| 467 |
+
test("Profile has cluster", 'cluster' in profile)
|
| 468 |
+
test("Profile has type_name", 'type_name' in profile)
|
| 469 |
+
test("Conservative classified as Swing/Conservative", profile['type_name'] in ['Swing Trader', 'Conservative'])
|
| 470 |
+
print(f" Conservative Carol β {profile['type_name']}")
|
| 471 |
+
|
| 472 |
+
# Test aggressive trader
|
| 473 |
+
aggressive_trades = [
|
| 474 |
+
{'entry_price': 100, 'exit_price': 105, 'size': 0.15, 'pnl': 750, 'holding_time': 60, 'direction': 1}
|
| 475 |
+
] * 12 + [
|
| 476 |
+
{'entry_price': 100, 'exit_price': 93, 'size': 0.20, 'pnl': -1400, 'holding_time': 30, 'direction': 1}
|
| 477 |
+
] * 10
|
| 478 |
+
|
| 479 |
+
agg_feats = profiler.extract_behavior_features(aggressive_trades)
|
| 480 |
+
agg_profile = profiler.predict_type(agg_feats)
|
| 481 |
+
test("Aggressive classified correctly", agg_profile['type_name'] in ['Aggressive', 'Moderate'])
|
| 482 |
+
print(f" Aggressive Alex β {agg_profile['type_name']}")
|
| 483 |
+
|
| 484 |
+
# Test scalper
|
| 485 |
+
scalper_trades = [
|
| 486 |
+
{'entry_price': 100, 'exit_price': 100.1, 'size': 0.03, 'pnl': 3, 'holding_time': 2, 'direction': 1}
|
| 487 |
+
] * 80 + [
|
| 488 |
+
{'entry_price': 100, 'exit_price': 99.95, 'size': 0.03, 'pnl': -1.5, 'holding_time': 1, 'direction': -1}
|
| 489 |
+
] * 50
|
| 490 |
+
|
| 491 |
+
scalp_feats = profiler.extract_behavior_features(scalper_trades)
|
| 492 |
+
scalp_profile = profiler.predict_type(scalp_feats)
|
| 493 |
+
test("Scalper classified correctly", scalp_profile['type_name'] == 'Scalper')
|
| 494 |
+
print(f" Scalper Sam β {scalp_profile['type_name']}")
|
| 495 |
+
|
| 496 |
+
# Test empty trades
|
| 497 |
+
empty_feats = profiler.extract_behavior_features([])
|
| 498 |
+
test("Empty trades returns zeros", np.all(empty_feats == 0))
|
| 499 |
+
|
| 500 |
+
# Test BehaviorAlertSystem
|
| 501 |
+
alert_system = BehaviorAlertSystem()
|
| 502 |
+
|
| 503 |
+
# Normal situation
|
| 504 |
+
normal_alerts = alert_system.analyze(conservative_trades[-5:], 100000, 2.0)
|
| 505 |
+
test("Normal status", normal_alerts['status'] in ['normal', 'warning'])
|
| 506 |
+
|
| 507 |
+
# Overtrading scenario (10+ trades in 1 hour)
|
| 508 |
+
many_trades = [{'entry_price': 100, 'exit_price': 100.1, 'size': 0.01, 'pnl': 1, 'holding_time': 1, 'direction': 1}] * 15
|
| 509 |
+
over_alerts = alert_system.analyze(many_trades, 100000, 1.0)
|
| 510 |
+
test("Overtrading detected", any(a['type'] == 'OVERTRADING' for a in over_alerts['alerts']),
|
| 511 |
+
f"alerts: {[a['type'] for a in over_alerts['alerts']]}")
|
| 512 |
+
|
| 513 |
+
# Loss streak scenario
|
| 514 |
+
loss_trades = [{'entry_price': 100, 'exit_price': 99, 'size': 0.05, 'pnl': -50, 'holding_time': 60, 'direction': 1}] * 5
|
| 515 |
+
loss_alerts = alert_system.analyze(loss_trades, 100000, 1.0)
|
| 516 |
+
test("Loss streak detected", any(a['type'] == 'LOSS_STREAK' for a in loss_alerts['alerts']))
|
| 517 |
+
|
| 518 |
+
# Excessive drawdown
|
| 519 |
+
big_loss = [{'entry_price': 100, 'exit_price': 80, 'size': 0.2, 'pnl': -20000, 'holding_time': 30, 'direction': 1}] * 3
|
| 520 |
+
dd_alerts = alert_system.analyze(big_loss, 100000, 1.0)
|
| 521 |
+
test("Excessive drawdown detected", any(a['type'] == 'EXCESSIVE_DRAWDOWN' for a in dd_alerts['alerts']))
|
| 522 |
+
test("Critical status on drawdown", dd_alerts['status'] == 'critical')
|
| 523 |
+
|
| 524 |
+
# Test PersonalizationEngine
|
| 525 |
+
engine = PersonalizationEngine()
|
| 526 |
+
params = engine.get_personalized_params(
|
| 527 |
+
{'cluster': 0, 'type_name': 'Conservative'},
|
| 528 |
+
{'alerts': [], 'risk_multiplier': 1.0, 'status': 'normal'}
|
| 529 |
+
)
|
| 530 |
+
test("Personalization returns params", isinstance(params, dict))
|
| 531 |
+
test("Conservative max_position <= 2%", params['max_position_pct'] <= 0.02)
|
| 532 |
+
test("Conservative min_confidence >= 70%", params['min_confidence'] >= 0.7)
|
| 533 |
+
|
| 534 |
+
# With revenge trading alert
|
| 535 |
+
revenge_params = engine.get_personalized_params(
|
| 536 |
+
{'cluster': 2, 'type_name': 'Aggressive'},
|
| 537 |
+
{'alerts': [{'type': 'REVENGE_TRADING', 'severity': 'CRITICAL', 'message': 'test'}],
|
| 538 |
+
'risk_multiplier': 0.3, 'status': 'critical'}
|
| 539 |
+
)
|
| 540 |
+
test("Revenge trading increases min_confidence", revenge_params['min_confidence'] > 0.55)
|
| 541 |
+
test("Risk multiplier reduces position size", revenge_params['max_position_pct'] < 0.10)
|
| 542 |
+
|
| 543 |
+
print(f"\n π€ Personalization: all trader types, alerts, and adaptations verified")
|
| 544 |
+
|
| 545 |
+
except Exception as e:
|
| 546 |
+
test("Personalization module", False, f"EXCEPTION: {e}")
|
| 547 |
+
traceback.print_exc()
|
| 548 |
+
|
| 549 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 550 |
+
# TEST 6: DECISION ENGINE
|
| 551 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 552 |
+
section("TEST 6: DECISION ENGINE")
|
| 553 |
+
try:
|
| 554 |
+
from trading_intelligence.decision_engine import (
|
| 555 |
+
DecisionEngine, Signal, TradingDecision, format_decision
|
| 556 |
+
)
|
| 557 |
+
|
| 558 |
+
engine = DecisionEngine(
|
| 559 |
+
prediction_model=model,
|
| 560 |
+
personalization_engine=PersonalizationEngine(),
|
| 561 |
+
)
|
| 562 |
+
|
| 563 |
+
test_features = np.random.randn(1, num_channels, 30).astype(np.float32)
|
| 564 |
+
|
| 565 |
+
# Basic decision
|
| 566 |
+
decision = engine.make_decision(
|
| 567 |
+
market_features=test_features,
|
| 568 |
+
trader_profile={'cluster': 1, 'type_name': 'Moderate'},
|
| 569 |
+
behavior_alerts={'alerts': [], 'risk_multiplier': 1.0, 'status': 'normal'},
|
| 570 |
+
current_atr=0.015,
|
| 571 |
+
horizon_idx=0,
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
test("Decision is TradingDecision", isinstance(decision, TradingDecision))
|
| 575 |
+
test("Signal is valid enum", isinstance(decision.signal, Signal))
|
| 576 |
+
test("Confidence in [0,1]", 0 <= decision.confidence <= 1, f"{decision.confidence:.3f}")
|
| 577 |
+
test("Direction prob in [0,1]", 0 <= decision.direction_prob <= 1)
|
| 578 |
+
test("Risk score in [0,1]", 0 <= decision.risk_score <= 1)
|
| 579 |
+
test("Position size > 0", decision.position_size_pct > 0)
|
| 580 |
+
test("Stop loss > 0", decision.stop_loss_pct > 0)
|
| 581 |
+
test("Take profit > 0", decision.take_profit_pct > 0)
|
| 582 |
+
test("Has reasoning", len(decision.reasoning) > 0)
|
| 583 |
+
test("Has horizon label", decision.horizon in ['short_term', 'mid_term', 'long_term'])
|
| 584 |
+
|
| 585 |
+
print(f" Decision: {decision.signal.value} (conf={decision.confidence:.1%})")
|
| 586 |
+
|
| 587 |
+
# Multi-horizon decisions
|
| 588 |
+
decisions = engine.make_multi_horizon_decisions(
|
| 589 |
+
market_features=test_features,
|
| 590 |
+
trader_profile={'cluster': 1, 'type_name': 'Moderate'},
|
| 591 |
+
behavior_alerts={'alerts': [], 'risk_multiplier': 1.0, 'status': 'normal'},
|
| 592 |
+
current_atr=0.015,
|
| 593 |
+
)
|
| 594 |
+
test("3 horizon decisions", len(decisions) == 3)
|
| 595 |
+
test("Horizons are different",
|
| 596 |
+
len(set(d.horizon for d in decisions)) == 3,
|
| 597 |
+
f"{[d.horizon for d in decisions]}")
|
| 598 |
+
|
| 599 |
+
for d in decisions:
|
| 600 |
+
print(f" {d.horizon}: {d.signal.value} (conf={d.confidence:.1%}, dir={d.direction_prob:.1%})")
|
| 601 |
+
|
| 602 |
+
# Critical alert override
|
| 603 |
+
critical_decision = engine.make_decision(
|
| 604 |
+
market_features=test_features,
|
| 605 |
+
trader_profile={'cluster': 2, 'type_name': 'Aggressive'},
|
| 606 |
+
behavior_alerts={
|
| 607 |
+
'alerts': [{'type': 'REVENGE_TRADING', 'severity': 'CRITICAL', 'message': 'test'}],
|
| 608 |
+
'risk_multiplier': 0.3, 'status': 'critical'
|
| 609 |
+
},
|
| 610 |
+
current_atr=0.015,
|
| 611 |
+
horizon_idx=0,
|
| 612 |
+
)
|
| 613 |
+
test("Critical alert forces HOLD", critical_decision.signal == Signal.HOLD)
|
| 614 |
+
test("Alert in reasoning", any('CRITICAL' in r for r in critical_decision.reasoning))
|
| 615 |
+
|
| 616 |
+
# Test format_decision
|
| 617 |
+
formatted = format_decision(decision)
|
| 618 |
+
test("format_decision returns string", isinstance(formatted, str))
|
| 619 |
+
test("format_decision has signal", decision.signal.value in formatted)
|
| 620 |
+
test("format_decision has confidence", 'Confidence' in formatted)
|
| 621 |
+
|
| 622 |
+
# Test without model (defaults)
|
| 623 |
+
engine_no_model = DecisionEngine()
|
| 624 |
+
default_decision = engine_no_model.make_decision(
|
| 625 |
+
market_features=test_features,
|
| 626 |
+
current_atr=0.015,
|
| 627 |
+
)
|
| 628 |
+
test("Works without model (defaults)", isinstance(default_decision, TradingDecision))
|
| 629 |
+
|
| 630 |
+
print(f"\n π― Decision engine: all signal types, alerts, formatting verified")
|
| 631 |
+
|
| 632 |
+
except Exception as e:
|
| 633 |
+
test("Decision engine module", False, f"EXCEPTION: {e}")
|
| 634 |
+
traceback.print_exc()
|
| 635 |
+
|
| 636 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 637 |
+
# TEST 7: EVALUATION & BACKTESTING
|
| 638 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββοΏ½οΏ½βββββ
|
| 639 |
+
section("TEST 7: EVALUATION & BACKTESTING")
|
| 640 |
+
try:
|
| 641 |
+
from trading_intelligence.evaluation import Evaluator, format_evaluation
|
| 642 |
+
|
| 643 |
+
evaluator = Evaluator(prediction_horizons=[1, 5, 20], trading_costs=0.001)
|
| 644 |
+
test_loader = DataLoader(
|
| 645 |
+
TensorDataset(torch.FloatTensor(X_test), torch.FloatTensor(y_test)),
|
| 646 |
+
batch_size=128, shuffle=False
|
| 647 |
+
)
|
| 648 |
+
|
| 649 |
+
eval_results = evaluator.evaluate_predictions(model, test_loader, device)
|
| 650 |
+
|
| 651 |
+
test("Evaluation returns dict", isinstance(eval_results, dict))
|
| 652 |
+
test("Has summary", 'summary' in eval_results)
|
| 653 |
+
test("Has horizon_1", 'horizon_1' in eval_results)
|
| 654 |
+
test("Has horizon_5", 'horizon_5' in eval_results)
|
| 655 |
+
test("Has horizon_20", 'horizon_20' in eval_results)
|
| 656 |
+
|
| 657 |
+
summary = eval_results['summary']
|
| 658 |
+
test("num_test_samples > 0", summary['num_test_samples'] > 0)
|
| 659 |
+
test("avg_direction_accuracy in [0,1]", 0 <= summary['avg_direction_accuracy'] <= 1)
|
| 660 |
+
test("avg_ic is finite", np.isfinite(summary['avg_ic']))
|
| 661 |
+
|
| 662 |
+
for h in [1, 5, 20]:
|
| 663 |
+
hr = eval_results[f'horizon_{h}']
|
| 664 |
+
test(f"H{h} direction_accuracy in [0,1]", 0 <= hr['direction_accuracy'] <= 1)
|
| 665 |
+
test(f"H{h} sharpe_ratio is finite", np.isfinite(hr['sharpe_ratio']))
|
| 666 |
+
test(f"H{h} max_drawdown in [0,1]", 0 <= hr['max_drawdown'] <= 1)
|
| 667 |
+
test(f"H{h} profit_factor >= 0", hr['profit_factor'] >= 0)
|
| 668 |
+
test(f"H{h} win_rate in [0,1]", 0 <= hr['win_rate'] <= 1)
|
| 669 |
+
test(f"H{h} num_trades > 0", hr['num_trades'] > 0)
|
| 670 |
+
print(f" H{h}: DA={hr['direction_accuracy']:.1%} IC={hr['information_coefficient']:.4f} "
|
| 671 |
+
f"Sharpe={hr['sharpe_ratio']:.2f} DD={hr['max_drawdown']:.1%} PF={hr['profit_factor']:.2f}")
|
| 672 |
+
|
| 673 |
+
# Test format_evaluation
|
| 674 |
+
formatted = format_evaluation(eval_results)
|
| 675 |
+
test("format_evaluation returns string", isinstance(formatted, str))
|
| 676 |
+
test("format_evaluation has content", len(formatted) > 100)
|
| 677 |
+
|
| 678 |
+
print(f"\n π Evaluation: all metrics computed and validated")
|
| 679 |
+
|
| 680 |
+
except Exception as e:
|
| 681 |
+
test("Evaluation module", False, f"EXCEPTION: {e}")
|
| 682 |
+
traceback.print_exc()
|
| 683 |
+
|
| 684 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 685 |
+
# TEST 8: TRAINING PIPELINE (high-level API)
|
| 686 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 687 |
+
section("TEST 8: TRAINING PIPELINE (high-level)")
|
| 688 |
+
try:
|
| 689 |
+
from trading_intelligence.training import TrainingPipeline, FinancialTimeSeriesDataset
|
| 690 |
+
|
| 691 |
+
pipeline = TrainingPipeline(
|
| 692 |
+
lookback_window=30,
|
| 693 |
+
prediction_horizons=[1, 5, 20],
|
| 694 |
+
d_model=64, n_heads=4, n_layers=2, d_ff=128,
|
| 695 |
+
patch_len=6, stride=3, dropout=0.1,
|
| 696 |
+
learning_rate=1e-3, batch_size=128,
|
| 697 |
+
max_epochs=3, patience=2,
|
| 698 |
+
)
|
| 699 |
+
|
| 700 |
+
train_loader, val_loader, test_loader = pipeline.prepare_data(df)
|
| 701 |
+
test("Pipeline prepare_data", True)
|
| 702 |
+
test("Pipeline model initialized", pipeline.model is not None)
|
| 703 |
+
test("Pipeline loss_fn initialized", pipeline.loss_fn is not None)
|
| 704 |
+
|
| 705 |
+
results = pipeline.train(train_loader, val_loader)
|
| 706 |
+
test("Pipeline train completes", 'best_val_loss' in results)
|
| 707 |
+
test("Pipeline training history", len(results['history']) > 0)
|
| 708 |
+
|
| 709 |
+
# Save/load cycle
|
| 710 |
+
pipeline.save_model('/app/models/pipeline_model.pt')
|
| 711 |
+
test("Pipeline model saved", os.path.exists('/app/models/pipeline_model.pt'))
|
| 712 |
+
|
| 713 |
+
pipeline2 = TrainingPipeline(lookback_window=30, prediction_horizons=[1,5,20])
|
| 714 |
+
pipeline2.load_model('/app/models/pipeline_model.pt')
|
| 715 |
+
test("Pipeline model loaded", pipeline2.model is not None)
|
| 716 |
+
|
| 717 |
+
print(f"\n π§ Training pipeline: prepare, train, save, load all verified")
|
| 718 |
+
|
| 719 |
+
except Exception as e:
|
| 720 |
+
test("Training pipeline", False, f"EXCEPTION: {e}")
|
| 721 |
+
traceback.print_exc()
|
| 722 |
+
|
| 723 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 724 |
+
# FULL INTEGRATION TEST
|
| 725 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 726 |
+
section("INTEGRATION TEST: FULL PIPELINE")
|
| 727 |
+
try:
|
| 728 |
+
print(" Running complete end-to-end flow...")
|
| 729 |
+
|
| 730 |
+
# 1. Raw data β Features
|
| 731 |
+
fe = FeatureEngine(lookback_window=30, prediction_horizons=[1, 5, 20])
|
| 732 |
+
features = fe.compute_all_features(df)
|
| 733 |
+
features_norm, _ = fe.normalize_features(features)
|
| 734 |
+
|
| 735 |
+
# 2. Features β Sequences
|
| 736 |
+
target_cols = []
|
| 737 |
+
for h in [1, 5, 20]:
|
| 738 |
+
target_cols.extend([f'target_direction_{h}', f'target_return_{h}'])
|
| 739 |
+
X_all, y_all = fe.create_sequences(features_norm, target_cols=target_cols)
|
| 740 |
+
valid = np.isfinite(X_all).all(axis=(1, 2)) & np.isfinite(y_all).all(axis=1)
|
| 741 |
+
X_all, y_all = X_all[valid], y_all[valid]
|
| 742 |
+
|
| 743 |
+
# 3. Train model
|
| 744 |
+
model_final = TradingTransformer(
|
| 745 |
+
num_channels=X_all.shape[1], seq_len=30, patch_len=6, stride=3,
|
| 746 |
+
d_model=64, n_heads=4, n_layers=2, d_ff=128, num_horizons=3
|
| 747 |
+
)
|
| 748 |
+
|
| 749 |
+
# 4. Get prediction
|
| 750 |
+
model_final.eval()
|
| 751 |
+
with torch.no_grad():
|
| 752 |
+
sample = torch.FloatTensor(X_all[-1:])
|
| 753 |
+
pred = model_final.predict_with_confidence(sample)
|
| 754 |
+
|
| 755 |
+
# 5. Risk assessment
|
| 756 |
+
rm = RiskModel(market_dim=64, portfolio_dim=64, behavior_dim=64)
|
| 757 |
+
rm.eval()
|
| 758 |
+
|
| 759 |
+
# 6. Personalization
|
| 760 |
+
profiler = TraderProfiler()
|
| 761 |
+
alert_system = BehaviorAlertSystem()
|
| 762 |
+
pers = PersonalizationEngine()
|
| 763 |
+
|
| 764 |
+
sample_trades = [
|
| 765 |
+
{'entry_price': 100, 'exit_price': 101.5, 'size': 0.05, 'pnl': 75, 'holding_time': 120, 'direction': 1}
|
| 766 |
+
] * 15 + [
|
| 767 |
+
{'entry_price': 100, 'exit_price': 99, 'size': 0.05, 'pnl': -50, 'holding_time': 60, 'direction': -1}
|
| 768 |
+
] * 8
|
| 769 |
+
|
| 770 |
+
trader_feats = profiler.extract_behavior_features(sample_trades)
|
| 771 |
+
trader_profile = profiler.predict_type(trader_feats)
|
| 772 |
+
alerts = alert_system.analyze(sample_trades[-5:], 100000, 1.0)
|
| 773 |
+
|
| 774 |
+
# 7. Decision
|
| 775 |
+
decision_engine = DecisionEngine(
|
| 776 |
+
prediction_model=model_final,
|
| 777 |
+
personalization_engine=pers,
|
| 778 |
+
)
|
| 779 |
+
|
| 780 |
+
final_decision = decision_engine.make_decision(
|
| 781 |
+
market_features=X_all[-1:],
|
| 782 |
+
trader_profile=trader_profile,
|
| 783 |
+
behavior_alerts=alerts,
|
| 784 |
+
current_atr=0.015,
|
| 785 |
+
horizon_idx=1,
|
| 786 |
+
)
|
| 787 |
+
|
| 788 |
+
test("Integration: features computed", len(features) > 0)
|
| 789 |
+
test("Integration: sequences created", X_all.shape[0] > 0)
|
| 790 |
+
test("Integration: prediction made", pred['direction_probs'].shape == (1, 3))
|
| 791 |
+
test("Integration: trader profiled", trader_profile['type_name'] in TRADER_TYPES.values())
|
| 792 |
+
test("Integration: decision generated", isinstance(final_decision.signal, Signal))
|
| 793 |
+
|
| 794 |
+
print(f"\n Full pipeline output:")
|
| 795 |
+
print(format_decision(final_decision))
|
| 796 |
+
|
| 797 |
+
test("Integration: complete pipeline works", True)
|
| 798 |
+
|
| 799 |
+
except Exception as e:
|
| 800 |
+
test("Integration test", False, f"EXCEPTION: {e}")
|
| 801 |
+
traceback.print_exc()
|
| 802 |
+
|
| 803 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 804 |
+
# FINAL SUMMARY
|
| 805 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 806 |
+
section("TEST SUMMARY")
|
| 807 |
+
total = PASS + FAIL
|
| 808 |
+
print(f"\n β
Passed: {PASS}/{total}")
|
| 809 |
+
print(f" β Failed: {FAIL}/{total}")
|
| 810 |
+
print(f" Pass Rate: {PASS/total*100:.1f}%")
|
| 811 |
+
|
| 812 |
+
if FAIL == 0:
|
| 813 |
+
print(f"\n π ALL TESTS PASSED!")
|
| 814 |
+
else:
|
| 815 |
+
print(f"\n β οΈ {FAIL} test(s) need attention")
|
| 816 |
+
|
| 817 |
+
print(f"\n{'='*70}")
|