avinashhm commited on
Commit
0e2300a
Β·
verified Β·
1 Parent(s): 244371a

Fix: test_all.py - all 174 tests passing

Browse files
Files changed (1) hide show
  1. 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}")