quantmllibrary 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. quantml/__init__.py +74 -0
  2. quantml/autograd.py +154 -0
  3. quantml/cli/__init__.py +10 -0
  4. quantml/cli/run_experiment.py +385 -0
  5. quantml/config/__init__.py +28 -0
  6. quantml/config/config.py +259 -0
  7. quantml/data/__init__.py +33 -0
  8. quantml/data/cache.py +149 -0
  9. quantml/data/feature_store.py +234 -0
  10. quantml/data/futures.py +254 -0
  11. quantml/data/loaders.py +236 -0
  12. quantml/data/memory_optimizer.py +234 -0
  13. quantml/data/validators.py +390 -0
  14. quantml/experiments/__init__.py +23 -0
  15. quantml/experiments/logger.py +208 -0
  16. quantml/experiments/results.py +158 -0
  17. quantml/experiments/tracker.py +223 -0
  18. quantml/features/__init__.py +25 -0
  19. quantml/features/base.py +104 -0
  20. quantml/features/gap_features.py +124 -0
  21. quantml/features/registry.py +138 -0
  22. quantml/features/volatility_features.py +140 -0
  23. quantml/features/volume_features.py +142 -0
  24. quantml/functional.py +37 -0
  25. quantml/models/__init__.py +27 -0
  26. quantml/models/attention.py +258 -0
  27. quantml/models/dropout.py +130 -0
  28. quantml/models/gru.py +319 -0
  29. quantml/models/linear.py +112 -0
  30. quantml/models/lstm.py +353 -0
  31. quantml/models/mlp.py +286 -0
  32. quantml/models/normalization.py +289 -0
  33. quantml/models/rnn.py +154 -0
  34. quantml/models/tcn.py +238 -0
  35. quantml/online.py +209 -0
  36. quantml/ops.py +1707 -0
  37. quantml/optim/__init__.py +42 -0
  38. quantml/optim/adafactor.py +206 -0
  39. quantml/optim/adagrad.py +157 -0
  40. quantml/optim/adam.py +267 -0
  41. quantml/optim/lookahead.py +97 -0
  42. quantml/optim/quant_optimizer.py +228 -0
  43. quantml/optim/radam.py +192 -0
  44. quantml/optim/rmsprop.py +203 -0
  45. quantml/optim/schedulers.py +286 -0
  46. quantml/optim/sgd.py +181 -0
  47. quantml/py.typed +0 -0
  48. quantml/streaming.py +175 -0
  49. quantml/tensor.py +462 -0
  50. quantml/time_series.py +447 -0
  51. quantml/training/__init__.py +135 -0
  52. quantml/training/alpha_eval.py +203 -0
  53. quantml/training/backtest.py +280 -0
  54. quantml/training/backtest_analysis.py +168 -0
  55. quantml/training/cv.py +106 -0
  56. quantml/training/data_loader.py +177 -0
  57. quantml/training/ensemble.py +84 -0
  58. quantml/training/feature_importance.py +135 -0
  59. quantml/training/features.py +364 -0
  60. quantml/training/futures_backtest.py +266 -0
  61. quantml/training/gradient_clipping.py +206 -0
  62. quantml/training/losses.py +248 -0
  63. quantml/training/lr_finder.py +127 -0
  64. quantml/training/metrics.py +376 -0
  65. quantml/training/regularization.py +89 -0
  66. quantml/training/trainer.py +239 -0
  67. quantml/training/walk_forward.py +190 -0
  68. quantml/utils/__init__.py +51 -0
  69. quantml/utils/gradient_check.py +274 -0
  70. quantml/utils/logging.py +181 -0
  71. quantml/utils/ops_cpu.py +231 -0
  72. quantml/utils/profiling.py +364 -0
  73. quantml/utils/reproducibility.py +220 -0
  74. quantml/utils/serialization.py +335 -0
  75. quantmllibrary-0.1.0.dist-info/METADATA +536 -0
  76. quantmllibrary-0.1.0.dist-info/RECORD +79 -0
  77. quantmllibrary-0.1.0.dist-info/WHEEL +5 -0
  78. quantmllibrary-0.1.0.dist-info/licenses/LICENSE +22 -0
  79. quantmllibrary-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,364 @@
1
+ """
2
+ Feature engineering pipeline for quant models.
3
+
4
+ This module provides utilities for creating features from raw market data,
5
+ including lagged features, rolling windows, cross-sectional features, and normalization.
6
+ """
7
+
8
+ from typing import List, Optional, Union, Callable, Dict, Any
9
+ from quantml.tensor import Tensor
10
+ from quantml import time_series
11
+
12
+ # Try to import NumPy
13
+ try:
14
+ import numpy as np
15
+ HAS_NUMPY = True
16
+ except ImportError:
17
+ HAS_NUMPY = False
18
+ np = None
19
+
20
+
21
+ class FeaturePipeline:
22
+ """
23
+ Feature engineering pipeline for reproducible feature creation.
24
+
25
+ This class provides a framework for creating features from raw data,
26
+ with support for lagged features, rolling windows, normalization, and more.
27
+
28
+ Attributes:
29
+ features: List of feature definitions
30
+ normalizers: Dictionary of normalizers for each feature
31
+
32
+ Examples:
33
+ >>> pipeline = FeaturePipeline()
34
+ >>> pipeline.add_lagged_feature('price', lags=[1, 5, 10])
35
+ >>> pipeline.add_rolling_feature('price', window=20, func='mean')
36
+ >>> features = pipeline.transform(prices)
37
+ """
38
+
39
+ def __init__(self):
40
+ """Initialize feature pipeline."""
41
+ self.features = []
42
+ self.normalizers = {}
43
+
44
+ def add_lagged_feature(
45
+ self,
46
+ name: str,
47
+ lags: List[int],
48
+ fill_value: float = 0.0
49
+ ):
50
+ """
51
+ Add lagged features.
52
+
53
+ Args:
54
+ name: Feature name
55
+ lags: List of lag values (e.g., [1, 5, 10] for 1, 5, 10 period lags)
56
+ fill_value: Value to use for missing lags (at beginning)
57
+ """
58
+ self.features.append({
59
+ 'type': 'lagged',
60
+ 'name': name,
61
+ 'lags': lags,
62
+ 'fill_value': fill_value
63
+ })
64
+
65
+ def add_rolling_feature(
66
+ self,
67
+ name: str,
68
+ window: int,
69
+ func: str = 'mean',
70
+ min_periods: Optional[int] = None
71
+ ):
72
+ """
73
+ Add rolling window feature.
74
+
75
+ Args:
76
+ name: Feature name
77
+ window: Window size
78
+ func: Function to apply ('mean', 'std', 'min', 'max', 'sum')
79
+ min_periods: Minimum periods required (default: window)
80
+ """
81
+ if min_periods is None:
82
+ min_periods = window
83
+
84
+ self.features.append({
85
+ 'type': 'rolling',
86
+ 'name': name,
87
+ 'window': window,
88
+ 'func': func,
89
+ 'min_periods': min_periods
90
+ })
91
+
92
+ def add_time_series_feature(
93
+ self,
94
+ name: str,
95
+ func: str,
96
+ **kwargs
97
+ ):
98
+ """
99
+ Add time-series feature (EMA, returns, etc.).
100
+
101
+ Args:
102
+ name: Feature name
103
+ func: Function name ('ema', 'returns', 'volatility', etc.)
104
+ **kwargs: Arguments for the function
105
+ """
106
+ self.features.append({
107
+ 'type': 'time_series',
108
+ 'name': name,
109
+ 'func': func,
110
+ 'kwargs': kwargs
111
+ })
112
+
113
+ def add_normalization(
114
+ self,
115
+ feature_name: str,
116
+ method: str = 'zscore',
117
+ window: Optional[int] = None
118
+ ):
119
+ """
120
+ Add normalization to a feature.
121
+
122
+ Args:
123
+ feature_name: Name of feature to normalize
124
+ method: Normalization method ('zscore', 'minmax', 'robust')
125
+ window: Rolling window for normalization (None for global)
126
+ """
127
+ if feature_name not in self.normalizers:
128
+ self.normalizers[feature_name] = []
129
+ self.normalizers[feature_name].append({
130
+ 'method': method,
131
+ 'window': window
132
+ })
133
+
134
+ def transform(self, data: Dict[str, List[float]]) -> List[List[float]]:
135
+ """
136
+ Transform raw data into features.
137
+
138
+ Args:
139
+ data: Dictionary of feature name -> values
140
+
141
+ Returns:
142
+ List of feature vectors (one per time step)
143
+ """
144
+ n = len(list(data.values())[0]) if data else 0
145
+ if n == 0:
146
+ return []
147
+
148
+ feature_matrix = []
149
+
150
+ for i in range(n):
151
+ feature_vector = []
152
+
153
+ for feat_def in self.features:
154
+ feat_name = feat_def['name']
155
+ if feat_name not in data:
156
+ continue
157
+
158
+ values = data[feat_name]
159
+
160
+ if feat_def['type'] == 'lagged':
161
+ for lag in feat_def['lags']:
162
+ if i >= lag:
163
+ feature_vector.append(values[i - lag])
164
+ else:
165
+ feature_vector.append(feat_def['fill_value'])
166
+
167
+ elif feat_def['type'] == 'rolling':
168
+ window = feat_def['window']
169
+ func = feat_def['func']
170
+ min_periods = feat_def['min_periods']
171
+
172
+ start_idx = max(0, i - window + 1)
173
+ window_data = values[start_idx:i+1]
174
+
175
+ if len(window_data) >= min_periods:
176
+ if func == 'mean':
177
+ feat_val = sum(window_data) / len(window_data)
178
+ elif func == 'std':
179
+ mean_val = sum(window_data) / len(window_data)
180
+ variance = sum((x - mean_val) ** 2 for x in window_data) / len(window_data)
181
+ feat_val = variance ** 0.5
182
+ elif func == 'min':
183
+ feat_val = min(window_data)
184
+ elif func == 'max':
185
+ feat_val = max(window_data)
186
+ elif func == 'sum':
187
+ feat_val = sum(window_data)
188
+ else:
189
+ feat_val = 0.0
190
+ else:
191
+ feat_val = 0.0
192
+
193
+ feature_vector.append(feat_val)
194
+
195
+ elif feat_def['type'] == 'time_series':
196
+ func_name = feat_def['func']
197
+ kwargs = feat_def['kwargs']
198
+
199
+ # Convert to tensor for time_series operations
200
+ tensor_data = Tensor([values[:i+1]])
201
+
202
+ if func_name == 'ema':
203
+ n_periods = kwargs.get('n', 20)
204
+ ema_vals = time_series.ema(tensor_data, n=n_periods)
205
+ if isinstance(ema_vals.data[0], list) and len(ema_vals.data[0]) > 0:
206
+ feature_vector.append(ema_vals.data[0][-1])
207
+ else:
208
+ feature_vector.append(0.0)
209
+
210
+ elif func_name == 'returns':
211
+ rets = time_series.returns(tensor_data)
212
+ if isinstance(rets.data[0], list) and len(rets.data[0]) > 0:
213
+ feature_vector.append(rets.data[0][-1])
214
+ else:
215
+ feature_vector.append(0.0)
216
+
217
+ elif func_name == 'volatility':
218
+ n_periods = kwargs.get('n', 20)
219
+ vol = time_series.volatility(tensor_data, n=n_periods)
220
+ if isinstance(vol.data[0], list) and len(vol.data[0]) > 0:
221
+ feature_vector.append(vol.data[0][-1])
222
+ else:
223
+ feature_vector.append(0.0)
224
+
225
+ else:
226
+ feature_vector.append(0.0)
227
+
228
+ feature_matrix.append(feature_vector)
229
+
230
+ # Apply normalization
231
+ if self.normalizers:
232
+ feature_matrix = self._apply_normalization(feature_matrix, data)
233
+
234
+ return feature_matrix
235
+
236
+ def _apply_normalization(
237
+ self,
238
+ feature_matrix: List[List[float]],
239
+ original_data: Dict[str, List[float]]
240
+ ) -> List[List[float]]:
241
+ """Apply normalization to features."""
242
+ # Simplified normalization - in practice would track feature indices
243
+ # For now, apply z-score normalization globally
244
+ if HAS_NUMPY:
245
+ try:
246
+ matrix = np.array(feature_matrix, dtype=np.float64)
247
+ mean_vals = np.mean(matrix, axis=0)
248
+ std_vals = np.std(matrix, axis=0)
249
+ std_vals = np.where(std_vals == 0, 1.0, std_vals)
250
+ normalized = (matrix - mean_vals) / std_vals
251
+ return normalized.tolist()
252
+ except (ValueError, TypeError):
253
+ pass
254
+
255
+ # Pure Python fallback
256
+ if len(feature_matrix) == 0:
257
+ return feature_matrix
258
+
259
+ n_features = len(feature_matrix[0])
260
+ means = [sum(row[i] for row in feature_matrix) / len(feature_matrix)
261
+ for i in range(n_features)]
262
+ stds = []
263
+ for i in range(n_features):
264
+ variance = sum((row[i] - means[i]) ** 2 for row in feature_matrix) / len(feature_matrix)
265
+ stds.append(variance ** 0.5 if variance > 0 else 1.0)
266
+
267
+ normalized = []
268
+ for row in feature_matrix:
269
+ normalized.append([
270
+ (row[i] - means[i]) / stds[i] if stds[i] > 0 else 0.0
271
+ for i in range(n_features)
272
+ ])
273
+
274
+ return normalized
275
+
276
+
277
+ def create_lagged_features(data: List[float], lags: List[int]) -> List[List[float]]:
278
+ """
279
+ Create lagged features from a time series.
280
+
281
+ Args:
282
+ data: Time series data
283
+ lags: List of lag values
284
+
285
+ Returns:
286
+ List of feature vectors with lagged values
287
+ """
288
+ features = []
289
+ for i in range(len(data)):
290
+ feature_vec = []
291
+ for lag in lags:
292
+ if i >= lag:
293
+ feature_vec.append(data[i - lag])
294
+ else:
295
+ feature_vec.append(0.0)
296
+ features.append(feature_vec)
297
+ return features
298
+
299
+
300
+ def normalize_features(
301
+ features: List[List[float]],
302
+ method: str = 'zscore'
303
+ ) -> List[List[float]]:
304
+ """
305
+ Normalize feature matrix.
306
+
307
+ Args:
308
+ features: Feature matrix
309
+ method: Normalization method ('zscore', 'minmax')
310
+
311
+ Returns:
312
+ Normalized feature matrix
313
+ """
314
+ if len(features) == 0:
315
+ return features
316
+
317
+ if HAS_NUMPY:
318
+ try:
319
+ matrix = np.array(features, dtype=np.float64)
320
+ if method == 'zscore':
321
+ mean_vals = np.mean(matrix, axis=0)
322
+ std_vals = np.std(matrix, axis=0)
323
+ std_vals = np.where(std_vals == 0, 1.0, std_vals)
324
+ normalized = (matrix - mean_vals) / std_vals
325
+ elif method == 'minmax':
326
+ min_vals = np.min(matrix, axis=0)
327
+ max_vals = np.max(matrix, axis=0)
328
+ ranges = max_vals - min_vals
329
+ ranges = np.where(ranges == 0, 1.0, ranges)
330
+ normalized = (matrix - min_vals) / ranges
331
+ else:
332
+ normalized = matrix
333
+ return normalized.tolist()
334
+ except (ValueError, TypeError):
335
+ pass
336
+
337
+ # Pure Python fallback
338
+ n_features = len(features[0])
339
+
340
+ if method == 'zscore':
341
+ means = [sum(row[i] for row in features) / len(features) for i in range(n_features)]
342
+ stds = []
343
+ for i in range(n_features):
344
+ variance = sum((row[i] - means[i]) ** 2 for row in features) / len(features)
345
+ stds.append(variance ** 0.5 if variance > 0 else 1.0)
346
+
347
+ normalized = [
348
+ [(row[i] - means[i]) / stds[i] if stds[i] > 0 else 0.0 for i in range(n_features)]
349
+ for row in features
350
+ ]
351
+ elif method == 'minmax':
352
+ mins = [min(row[i] for row in features) for i in range(n_features)]
353
+ maxs = [max(row[i] for row in features) for i in range(n_features)]
354
+ ranges = [maxs[i] - mins[i] if maxs[i] > mins[i] else 1.0 for i in range(n_features)]
355
+
356
+ normalized = [
357
+ [(row[i] - mins[i]) / ranges[i] if ranges[i] > 0 else 0.0 for i in range(n_features)]
358
+ for row in features
359
+ ]
360
+ else:
361
+ normalized = features
362
+
363
+ return normalized
364
+
@@ -0,0 +1,266 @@
1
+ """
2
+ Futures-specific backtesting engine.
3
+
4
+ Handles contract rolls, margin requirements, overnight gaps, and session-based trading.
5
+ """
6
+
7
+ from typing import List, Optional, Dict, Any, Tuple
8
+ from quantml.training.backtest import BacktestEngine
9
+ from quantml.training.metrics import sharpe_ratio, max_drawdown
10
+
11
+
12
+ class FuturesBacktestEngine(BacktestEngine):
13
+ """
14
+ Backtesting engine for futures contracts.
15
+
16
+ Extends BacktestEngine with futures-specific features:
17
+ - Contract roll handling
18
+ - Margin requirements
19
+ - Overnight gap simulation
20
+ - Session-based trading (RTH vs ETH)
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ initial_capital: float = 100000.0,
26
+ commission: float = 0.001,
27
+ slippage: float = 0.0005,
28
+ margin_requirement: float = 0.05, # 5% margin
29
+ contract_size: float = 50.0, # ES contract multiplier
30
+ roll_dates: Optional[List[int]] = None,
31
+ session_type: str = "RTH" # RTH or ETH
32
+ ):
33
+ """
34
+ Initialize futures backtesting engine.
35
+
36
+ Args:
37
+ initial_capital: Starting capital
38
+ commission: Commission per trade
39
+ slippage: Slippage per trade
40
+ margin_requirement: Margin requirement as fraction (e.g., 0.05 = 5%)
41
+ contract_size: Contract multiplier (50 for ES, 20 for NQ)
42
+ roll_dates: List of indices where contract rolls occur
43
+ session_type: "RTH" (regular trading hours) or "ETH" (extended)
44
+ """
45
+ super().__init__(initial_capital, commission, slippage)
46
+ self.margin_requirement = margin_requirement
47
+ self.contract_size = contract_size
48
+ self.roll_dates = roll_dates or []
49
+ self.session_type = session_type
50
+
51
+ def _calculate_margin(self, position: float, price: float) -> float:
52
+ """
53
+ Calculate margin requirement for position.
54
+
55
+ Args:
56
+ position: Position size (number of contracts)
57
+ price: Current price
58
+
59
+ Returns:
60
+ Required margin
61
+ """
62
+ position_value = abs(position) * price * self.contract_size
63
+ return position_value * self.margin_requirement
64
+
65
+ def _apply_overnight_gap(
66
+ self,
67
+ position: float,
68
+ prev_close: float,
69
+ current_open: float
70
+ ) -> Tuple[float, float]:
71
+ """
72
+ Apply overnight gap to position.
73
+
74
+ Args:
75
+ position: Current position
76
+ prev_close: Previous day's close
77
+ current_open: Current day's open
78
+
79
+ Returns:
80
+ (updated_capital, gap_pnl)
81
+ """
82
+ if position == 0 or prev_close == 0:
83
+ return 0.0, 0.0
84
+
85
+ gap = (current_open - prev_close) / prev_close
86
+ position_value = abs(position) * prev_close * self.contract_size
87
+
88
+ if position > 0: # Long
89
+ gap_pnl = position_value * gap
90
+ else: # Short
91
+ gap_pnl = -position_value * gap
92
+
93
+ return gap_pnl, gap_pnl
94
+
95
+ def run_futures(
96
+ self,
97
+ signals: List[float],
98
+ prices: List[float],
99
+ opens: Optional[List[float]] = None,
100
+ closes: Optional[List[float]] = None,
101
+ volumes: Optional[List[float]] = None
102
+ ) -> Dict[str, Any]:
103
+ """
104
+ Run futures backtest with contract rolls and overnight gaps.
105
+
106
+ Args:
107
+ signals: Trading signals
108
+ prices: Price data (can be close prices)
109
+ opens: Opening prices (for gap calculation)
110
+ closes: Closing prices (for gap calculation)
111
+ volumes: Volume data
112
+
113
+ Returns:
114
+ Backtest results with futures-specific metrics
115
+ """
116
+ if len(signals) != len(prices):
117
+ raise ValueError("signals and prices must have same length")
118
+
119
+ # Use closes if provided, otherwise use prices
120
+ if closes is None:
121
+ closes = prices
122
+ if opens is None:
123
+ opens = prices
124
+
125
+ n = len(signals)
126
+ capital = self.initial_capital
127
+ position = 0.0 # Position in contracts
128
+ equity_curve = [capital]
129
+ trades = []
130
+ returns = []
131
+ overnight_gaps = []
132
+ margin_used = []
133
+
134
+ for i in range(n):
135
+ price = prices[i]
136
+ signal = signals[i]
137
+
138
+ # Check for contract roll
139
+ if i in self.roll_dates:
140
+ # Close position before roll
141
+ if position != 0:
142
+ trade_value = abs(position) * price * self.contract_size
143
+ commission_cost = trade_value * self.commission
144
+ capital -= commission_cost
145
+
146
+ trades.append({
147
+ 'index': i,
148
+ 'price': price,
149
+ 'execution_price': price,
150
+ 'size': -position, # Close position
151
+ 'cost': commission_cost,
152
+ 'type': 'roll'
153
+ })
154
+ position = 0.0
155
+
156
+ # Apply overnight gap (if not first bar)
157
+ if i > 0:
158
+ prev_close = closes[i-1] if i-1 < len(closes) else prices[i-1]
159
+ current_open = opens[i] if i < len(opens) else price
160
+
161
+ if position != 0:
162
+ gap_pnl, _ = self._apply_overnight_gap(position, prev_close, current_open)
163
+ capital += gap_pnl
164
+ overnight_gaps.append({
165
+ 'index': i,
166
+ 'gap': (current_open - prev_close) / prev_close if prev_close > 0 else 0.0,
167
+ 'pnl': gap_pnl
168
+ })
169
+
170
+ # Determine target position
171
+ target_position_value = self.position_sizing(signal, capital, price)
172
+ target_position = target_position_value / (price * self.contract_size) if price > 0 else 0.0
173
+
174
+ # Round to integer contracts
175
+ target_position = round(target_position)
176
+
177
+ # Calculate trade
178
+ trade_size = target_position - position
179
+
180
+ if abs(trade_size) > 0.5: # Only trade if at least 1 contract
181
+ # Check margin requirement
182
+ new_position_value = abs(target_position) * price * self.contract_size
183
+ required_margin = new_position_value * self.margin_requirement
184
+
185
+ if required_margin > capital:
186
+ # Insufficient margin, reduce position
187
+ max_position = int(capital / (price * self.contract_size * self.margin_requirement))
188
+ target_position = max_position if target_position > 0 else -max_position
189
+ trade_size = target_position - position
190
+
191
+ if abs(trade_size) > 0.5:
192
+ # Apply slippage
193
+ execution_price = price * (1 + self.slippage * (1 if trade_size > 0 else -1))
194
+
195
+ # Calculate costs
196
+ trade_value = abs(trade_size) * execution_price * self.contract_size
197
+ commission_cost = trade_value * self.commission
198
+ slippage_cost = abs(trade_size) * price * self.contract_size * self.slippage
199
+ total_cost = commission_cost + slippage_cost
200
+
201
+ # Update capital
202
+ capital -= trade_size * execution_price * self.contract_size + total_cost
203
+
204
+ # Update position
205
+ position = target_position
206
+
207
+ trades.append({
208
+ 'index': i,
209
+ 'price': price,
210
+ 'execution_price': execution_price,
211
+ 'size': trade_size,
212
+ 'cost': total_cost,
213
+ 'type': 'trade'
214
+ })
215
+
216
+ # Update equity (mark-to-market)
217
+ current_value = capital + position * price * self.contract_size
218
+ equity_curve.append(current_value)
219
+
220
+ # Track margin usage
221
+ margin = self._calculate_margin(position, price)
222
+ margin_used.append(margin)
223
+
224
+ # Calculate return
225
+ if i > 0:
226
+ prev_value = equity_curve[-2]
227
+ ret = (current_value - prev_value) / prev_value if prev_value > 0 else 0.0
228
+ returns.append(ret)
229
+
230
+ # Calculate metrics
231
+ final_value = equity_curve[-1]
232
+ total_return = (final_value - self.initial_capital) / self.initial_capital
233
+
234
+ sharpe = sharpe_ratio(returns) if returns else 0.0
235
+ max_dd = max_drawdown(returns) if returns else 0.0
236
+
237
+ # Trade statistics
238
+ n_trades = len([t for t in trades if t.get('type') == 'trade'])
239
+ avg_margin_usage = sum(margin_used) / len(margin_used) if margin_used else 0.0
240
+ max_margin_usage = max(margin_used) if margin_used else 0.0
241
+
242
+ # Overnight gap statistics
243
+ gap_pnls = [g['pnl'] for g in overnight_gaps]
244
+ total_gap_pnl = sum(gap_pnls)
245
+ avg_gap = sum(g['gap'] for g in overnight_gaps) / len(overnight_gaps) if overnight_gaps else 0.0
246
+
247
+ return {
248
+ 'initial_capital': self.initial_capital,
249
+ 'final_value': final_value,
250
+ 'total_return': total_return,
251
+ 'equity_curve': equity_curve,
252
+ 'returns': returns,
253
+ 'trades': trades,
254
+ 'n_trades': n_trades,
255
+ 'overnight_gaps': overnight_gaps,
256
+ 'total_gap_pnl': total_gap_pnl,
257
+ 'avg_gap': avg_gap,
258
+ 'margin_used': margin_used,
259
+ 'avg_margin_usage': avg_margin_usage,
260
+ 'max_margin_usage': max_margin_usage,
261
+ 'sharpe_ratio': sharpe,
262
+ 'max_drawdown': max_dd,
263
+ 'contract_size': self.contract_size,
264
+ 'margin_requirement': self.margin_requirement
265
+ }
266
+