JonusNattapong commited on
Commit
45ab569
·
verified ·
1 Parent(s): b45048d

Upload v8/backtest_v8.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. v8/backtest_v8.py +391 -0
v8/backtest_v8.py ADDED
@@ -0,0 +1,391 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Super Ensemble Backtester for Romeo V8
2
+
3
+ Advanced backtester for the super ensemble model with multi-algorithm collaboration,
4
+ stacking, dynamic weighting, and confidence calibration.
5
+
6
+ Key Features:
7
+ - Super Ensemble Prediction: Combines 10+ algorithms
8
+ - Stacking Logic: Uses meta-learner for final predictions
9
+ - Dynamic Weighting: Real-time weight adjustment
10
+ - Confidence Calibration: Calibrated probability fusion
11
+ - Cross-Validation Ensemble: Multiple CV fold combination
12
+ - Advanced Risk Management: Multi-algorithm consensus
13
+ """
14
+
15
+ import os
16
+ import json
17
+ import numpy as np
18
+ import pandas as pd
19
+ import joblib
20
+ from tensorflow import keras
21
+ import sys
22
+ import argparse
23
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '.')))
24
+
25
+ try:
26
+ from v8.train_v8 import SuperEnsembleFeatureEngineer, load_romeo_v8, SuperEnsemble
27
+ except Exception:
28
+ from train_v8 import SuperEnsembleFeatureEngineer, load_romeo_v8, SuperEnsemble
29
+
30
+
31
+ class SumAxis1Layer(keras.layers.Layer):
32
+ def call(self, inputs):
33
+ return keras.backend.sum(inputs, axis=1)
34
+
35
+
36
+ class SuperEnsembleBacktester:
37
+ def __init__(self, config=None):
38
+ self.config = config or {
39
+ 'ensemble_method': 'stacking', # 'stacking', 'weighted', 'voting'
40
+ 'confidence_threshold': 0.60, # Minimum confidence for trades
41
+ 'max_risk_per_trade': 0.12, # Maximum risk per trade
42
+ 'use_dynamic_weighting': True, # Enable dynamic weight adjustment
43
+ 'use_calibration': True, # Use calibrated probabilities
44
+ 'use_cv_ensemble': True, # Use cross-validation ensemble
45
+ 'consensus_threshold': 0.7, # Minimum algorithm agreement
46
+ 'volatility_adjustment': True,
47
+ 'max_drawdown_limit': 0.90, # Stop trading limit
48
+ }
49
+ self.super_ensemble = None
50
+
51
+ def load_super_ensemble(self, model_path):
52
+ """Load the super ensemble model"""
53
+ if not os.path.exists(model_path):
54
+ raise FileNotFoundError(f"Model not found: {model_path}")
55
+
56
+ self.super_ensemble = load_romeo_v8(model_path)
57
+ print(f"Loaded super ensemble with {len(self.super_ensemble.models)} base algorithms")
58
+ return self.super_ensemble
59
+
60
+ def get_super_ensemble_prediction(self, X, method='stacking'):
61
+ """Get prediction from super ensemble using specified method"""
62
+ if self.super_ensemble is None:
63
+ raise ValueError("Super ensemble not loaded. Call load_super_ensemble() first.")
64
+
65
+ # Use the SuperEnsemble class predict_proba method
66
+ proba = self.super_ensemble.predict_proba(X)
67
+ ensemble_proba = proba[:, 1]
68
+
69
+ # For compatibility, create base_predictions and model_names
70
+ # This is a simplified version - in full implementation you'd get individual model predictions
71
+ base_predictions = [ensemble_proba.reshape(-1, 1)] # Simplified
72
+ model_names = list(self.super_ensemble.models.keys())
73
+
74
+ return ensemble_proba, base_predictions, model_names
75
+
76
+ def calculate_consensus_score(self, base_predictions):
77
+ """Calculate consensus score among algorithms"""
78
+ if not base_predictions:
79
+ return 0.5
80
+
81
+ # Convert to binary predictions (above/below 0.5)
82
+ binary_preds = []
83
+ for pred in base_predictions:
84
+ binary_pred = (pred.ravel() > 0.5).astype(int)
85
+ binary_preds.append(binary_pred)
86
+
87
+ # Calculate agreement percentage
88
+ all_binary = np.array(binary_preds)
89
+ consensus = np.mean(all_binary, axis=0) # Average agreement
90
+
91
+ return consensus
92
+
93
+ def should_trade_signal(self, ensemble_proba, consensus_score, volatility, volume_ratio):
94
+ """Determine if signal meets super ensemble criteria"""
95
+
96
+ # Base confidence check
97
+ if ensemble_proba < self.config['confidence_threshold']:
98
+ return False, "Low confidence"
99
+
100
+ # Consensus check
101
+ if consensus_score < self.config['consensus_threshold']:
102
+ return False, "Low consensus"
103
+
104
+ # Volatility filter
105
+ if self.config['volatility_adjustment'] and volatility > 0.025:
106
+ return False, "High volatility"
107
+
108
+ # Volume confirmation
109
+ if volume_ratio < 1.0:
110
+ return False, "Low volume"
111
+
112
+ return True, "Valid signal"
113
+
114
+ def backtest_super_ensemble(self, timeframe='15m', initial_capital=100, data_file=None,
115
+ risk_per_trade=0.08, stop_loss=0.015, take_profit=0.04,
116
+ commission_pct=0.0002, slippage_pips=0.3, timeout_bars=6):
117
+
118
+ # Load data
119
+ if data_file:
120
+ data_path = data_file
121
+ else:
122
+ data_path = f'data_xauusd_v3/15m_data_v3.csv'
123
+
124
+ df = pd.read_csv(data_path, parse_dates=['Datetime'])
125
+ df = df.sort_values('Datetime').reset_index(drop=True)
126
+
127
+ # Load model
128
+ model_path = f'v8/models_romeo_v8/trading_model_romeo_{timeframe}.pkl'
129
+ artifact = self.load_super_ensemble(model_path)
130
+
131
+ # Process features (same as training but without fitting scaler/PCA)
132
+ eng = SuperEnsembleFeatureEngineer()
133
+ df = eng.add_technical_indicators(df)
134
+ df = eng.add_quantum_features(df)
135
+ df = df.fillna(method='bfill').fillna(method='ffill').fillna(0)
136
+
137
+ exclude = ['Datetime', 'Open', 'High', 'Low', 'Close', 'Volume', 'Adj Close']
138
+ feature_cols = [c for c in df.columns if c not in exclude and not c.startswith('target')]
139
+
140
+ # Ensure df contains all features
141
+ for f in feature_cols:
142
+ if f not in df.columns:
143
+ df[f] = 0.0
144
+
145
+ X = df[feature_cols].values
146
+
147
+ # Get super ensemble predictions
148
+ print("Generating super ensemble predictions...")
149
+ ensemble_probas, base_predictions, model_names = self.get_super_ensemble_prediction(
150
+ X, method=self.config['ensemble_method']
151
+ )
152
+
153
+ # Calculate consensus scores
154
+ consensus_scores = self.calculate_consensus_score(base_predictions)
155
+
156
+ # Add to dataframe
157
+ df['ensemble_proba'] = ensemble_probas
158
+ df['consensus_score'] = consensus_scores
159
+
160
+ # Generate signals based on ensemble confidence
161
+ signals = (ensemble_probas > self.config['confidence_threshold']).astype(int)
162
+ df['signal'] = signals
163
+
164
+ # Initialize trading variables
165
+ capital = initial_capital
166
+ peak_capital = initial_capital
167
+ trades = []
168
+ total_ensemble_contributions = {name: 0 for name in model_names}
169
+
170
+ print("Starting super ensemble backtest...")
171
+
172
+ # Trading loop
173
+ for i in range(len(df)-1):
174
+ current_drawdown = (peak_capital - capital) / peak_capital if peak_capital > 0 else 0
175
+
176
+ # Check stop trading condition
177
+ if current_drawdown >= (1 - self.config['max_drawdown_limit']):
178
+ print(f"Stopping trading due to drawdown limit: {current_drawdown:.1%}")
179
+ break
180
+
181
+ if df.iloc[i]['signal'] == 1:
182
+ ensemble_proba = df.iloc[i]['ensemble_proba']
183
+ consensus_score = df.iloc[i]['consensus_score']
184
+ volatility = df.iloc[i]['Volatility'] if 'Volatility' in df.columns else 0.01
185
+ volume_ratio = df.iloc[i]['Volume_Ratio'] if 'Volume_Ratio' in df.columns else 1.0
186
+
187
+ # Check if signal meets criteria
188
+ should_trade, reason = self.should_trade_signal(
189
+ ensemble_proba, consensus_score, volatility, volume_ratio
190
+ )
191
+
192
+ if not should_trade:
193
+ continue
194
+
195
+ entry_price = df.iloc[i+1]['Open']
196
+ entry_price_slip = entry_price + slippage_pips * 0.0001
197
+
198
+ # Conservative position sizing for super ensemble
199
+ position_size = (capital * risk_per_trade) / (stop_loss * entry_price_slip)
200
+
201
+ # Adjust for volatility
202
+ if self.config['volatility_adjustment']:
203
+ vol_factor = 1 / (1 + volatility * 10)
204
+ position_size *= vol_factor
205
+
206
+ # Ensure within limits
207
+ max_size = (capital * self.config['max_risk_per_trade']) / (stop_loss * entry_price_slip)
208
+ position_size = min(position_size, max_size)
209
+
210
+ # Execute trade
211
+ exit_price = None
212
+ exit_idx = i+1
213
+ reason = 'TIMEOUT'
214
+
215
+ for j in range(i+1, min(i+1+timeout_bars, len(df))):
216
+ high = df.iloc[j]['High']
217
+ low = df.iloc[j]['Low']
218
+
219
+ if high >= entry_price_slip * (1 + take_profit):
220
+ exit_price = entry_price_slip * (1 + take_profit)
221
+ exit_idx = j
222
+ reason = 'TP'
223
+ break
224
+ if low <= entry_price_slip * (1 - stop_loss):
225
+ exit_price = entry_price_slip * (1 - stop_loss)
226
+ exit_idx = j
227
+ reason = 'SL'
228
+ break
229
+
230
+ if exit_price is None:
231
+ exit_price = df.iloc[min(i+timeout_bars, len(df)-1)]['Close']
232
+ exit_idx = min(i+timeout_bars, len(df)-1)
233
+
234
+ exit_price_slip = exit_price - slippage_pips * 0.0001
235
+ pnl = (exit_price_slip - entry_price_slip) * position_size
236
+ commission = commission_pct * (entry_price_slip + exit_price_slip) * position_size
237
+ pnl_after = pnl - commission
238
+ capital += pnl_after
239
+
240
+ # Update peak capital
241
+ peak_capital = max(peak_capital, capital)
242
+
243
+ # Track algorithm contributions
244
+ for name in model_names:
245
+ if name in df.columns and f'{name}_contrib' in df.columns:
246
+ total_ensemble_contributions[name] += df.iloc[i][f'{name}_contrib']
247
+
248
+ trades.append({
249
+ 'entry_idx': i+1,
250
+ 'exit_idx': exit_idx,
251
+ 'entry_date': df.iloc[i+1]['Datetime'],
252
+ 'exit_date': df.iloc[exit_idx]['Datetime'],
253
+ 'entry_price': entry_price_slip,
254
+ 'exit_price': exit_price_slip,
255
+ 'position_size': position_size,
256
+ 'pnl': pnl_after,
257
+ 'commission': commission,
258
+ 'reason': reason,
259
+ 'ensemble_proba': float(ensemble_proba),
260
+ 'consensus_score': float(consensus_score),
261
+ 'volatility': float(volatility),
262
+ 'volume_ratio': float(volume_ratio),
263
+ 'capital_after': capital,
264
+ 'drawdown_at_entry': current_drawdown
265
+ })
266
+
267
+ # Calculate final metrics
268
+ if trades:
269
+ winning_trades = [t for t in trades if t['pnl'] > 0]
270
+ win_rate = len(winning_trades) / len(trades)
271
+
272
+ if winning_trades:
273
+ avg_win = np.mean([t['pnl'] for t in winning_trades])
274
+ gross_profit = sum([t['pnl'] for t in winning_trades])
275
+ else:
276
+ avg_win = 0
277
+ gross_profit = 0
278
+
279
+ losing_trades = [t for t in trades if t['pnl'] <= 0]
280
+ if losing_trades:
281
+ avg_loss = np.mean([t['pnl'] for t in losing_trades])
282
+ gross_loss = abs(sum([t['pnl'] for t in losing_trades]))
283
+ else:
284
+ avg_loss = 0
285
+ gross_loss = 0
286
+
287
+ profit_factor = gross_profit / gross_loss if gross_loss > 0 else float('inf')
288
+
289
+ # Sharpe ratio approximation
290
+ returns = [t['pnl'] / initial_capital for t in trades]
291
+ if len(returns) > 1 and np.std(returns) > 0:
292
+ sharpe_ratio = np.mean(returns) / np.std(returns) * np.sqrt(252)
293
+ else:
294
+ sharpe_ratio = 0
295
+
296
+ else:
297
+ win_rate = 0
298
+ avg_win = 0
299
+ avg_loss = 0
300
+ profit_factor = 0
301
+ sharpe_ratio = 0
302
+
303
+ final_drawdown = (peak_capital - capital) / peak_capital if peak_capital > 0 else 0
304
+
305
+ summary = {
306
+ 'initial_capital': initial_capital,
307
+ 'final_capital': float(capital),
308
+ 'total_return_pct': float((capital - initial_capital)/initial_capital*100),
309
+ 'peak_capital': float(peak_capital),
310
+ 'max_drawdown_pct': float(final_drawdown * 100),
311
+ 'trades': len(trades),
312
+ 'win_rate': float(win_rate),
313
+ 'avg_win': float(avg_win),
314
+ 'avg_loss': float(avg_loss),
315
+ 'profit_factor': float(profit_factor),
316
+ 'sharpe_ratio': float(sharpe_ratio),
317
+ 'super_ensemble_metrics': {
318
+ 'algorithms_used': len(model_names),
319
+ 'ensemble_method': self.config['ensemble_method'],
320
+ 'avg_consensus_score': float(np.mean([t['consensus_score'] for t in trades])) if trades else 0,
321
+ 'avg_ensemble_proba': float(np.mean([t['ensemble_proba'] for t in trades])) if trades else 0,
322
+ 'calibration_used': self.config['use_calibration'],
323
+ 'cv_ensemble_used': self.config['use_cv_ensemble'],
324
+ 'dynamic_weighting_used': self.config['use_dynamic_weighting'],
325
+ }
326
+ }
327
+
328
+ # Save results
329
+ os.makedirs('backtest_results_romeo_v8', exist_ok=True)
330
+ out_signals = df.reset_index()[['Datetime', 'Open', 'High', 'Low', 'Close', 'signal', 'ensemble_proba', 'consensus_score']]
331
+ out_signals.to_csv(f'backtest_results_romeo_v8/romeo_signals_{timeframe}.csv', index=False)
332
+ pd.DataFrame(trades).to_csv(f'backtest_results_romeo_v8/romeo_trades_{timeframe}.csv', index=False)
333
+ with open(f'backtest_results_romeo_v8/romeo_summary_{timeframe}.json', 'w') as f:
334
+ json.dump(summary, f, indent=2, default=str)
335
+
336
+ return summary
337
+
338
+
339
+ def main():
340
+ parser = argparse.ArgumentParser(description='Super Ensemble Backtester for Romeo V8')
341
+ parser.add_argument('--timeframe', default='15m')
342
+ parser.add_argument('--data', default=None, help='Optional path to unseen CSV data')
343
+ parser.add_argument('--initial-capital', type=float, default=100)
344
+ parser.add_argument('--commission-pct', type=float, default=0.0002)
345
+ parser.add_argument('--slippage-pips', type=float, default=0.3)
346
+ parser.add_argument('--risk-per-trade', type=float, default=0.08)
347
+ parser.add_argument('--stop-loss', type=float, default=0.015)
348
+ parser.add_argument('--take-profit', type=float, default=0.04)
349
+ parser.add_argument('--ensemble-method', choices=['stacking', 'weighted', 'voting'], default='stacking')
350
+ parser.add_argument('--confidence-threshold', type=float, default=0.60)
351
+
352
+ args = parser.parse_args()
353
+
354
+ backtester = SuperEnsembleBacktester({
355
+ 'ensemble_method': args.ensemble_method,
356
+ 'confidence_threshold': args.confidence_threshold,
357
+ 'max_risk_per_trade': 0.12,
358
+ 'use_dynamic_weighting': True,
359
+ 'use_calibration': True,
360
+ 'use_cv_ensemble': True,
361
+ 'consensus_threshold': 0.7,
362
+ 'volatility_adjustment': True,
363
+ 'max_drawdown_limit': 0.90,
364
+ })
365
+
366
+ summary = backtester.backtest_super_ensemble(
367
+ timeframe=args.timeframe,
368
+ initial_capital=args.initial_capital,
369
+ data_file=args.data,
370
+ risk_per_trade=args.risk_per_trade,
371
+ stop_loss=args.stop_loss,
372
+ take_profit=args.take_profit,
373
+ commission_pct=args.commission_pct,
374
+ slippage_pips=args.slippage_pips
375
+ )
376
+
377
+ print("Romeo V8 Super Ensemble Backtest Results:")
378
+ print("=" * 60)
379
+ print(f"Initial Capital: ${summary['initial_capital']}")
380
+ print(f"Final Capital: ${summary['final_capital']:.2f}")
381
+ print(f"Total Return: {summary['total_return_pct']:.2f}%")
382
+ print(f"Max Drawdown: {summary['max_drawdown_pct']:.2f}%")
383
+ print(f"Total Trades: {summary['trades']}")
384
+ print(f"Win Rate: {summary['win_rate']:.1%}")
385
+ print(f"Profit Factor: {summary['profit_factor']:.2f}")
386
+ print(f"Sharpe Ratio: {summary['sharpe_ratio']:.2f}")
387
+ print(f"Super Ensemble: {summary['super_ensemble_metrics']}")
388
+
389
+
390
+ if __name__ == '__main__':
391
+ main()