ivolatility-backtesting 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.
Potentially problematic release.
This version of ivolatility-backtesting might be problematic. Click here for more details.
- ivolatility_backtesting/___init__.py +17 -0
- ivolatility_backtesting/ivolatility_backtesting.py +821 -0
- ivolatility_backtesting-0.1.0.dist-info/LICENSE +21 -0
- ivolatility_backtesting-0.1.0.dist-info/METADATA +93 -0
- ivolatility_backtesting-0.1.0.dist-info/RECORD +7 -0
- ivolatility_backtesting-0.1.0.dist-info/WHEEL +5 -0
- ivolatility_backtesting-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .ivolatility_backtesting import (
|
|
2
|
+
BacktestResults, BacktestAnalyzer, ResultsReporter,
|
|
3
|
+
ChartGenerator, ResultsExporter, run_backtest,
|
|
4
|
+
init_api, get_api_method, APIManager
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
'BacktestResults',
|
|
9
|
+
'BacktestAnalyzer',
|
|
10
|
+
'ResultsReporter',
|
|
11
|
+
'ChartGenerator',
|
|
12
|
+
'ResultsExporter',
|
|
13
|
+
'run_backtest',
|
|
14
|
+
'init_api',
|
|
15
|
+
'get_api_method',
|
|
16
|
+
'APIManager'
|
|
17
|
+
]
|
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ivolatility_backtesting.py
|
|
3
|
+
Universal Backtest Framework with One-Command Runner
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
from ivolatility_backtesting import *
|
|
7
|
+
|
|
8
|
+
# Initialize API once
|
|
9
|
+
init_api(os.getenv("API_KEY"))
|
|
10
|
+
|
|
11
|
+
CONFIG = {...}
|
|
12
|
+
analyzer = run_backtest(my_strategy, CONFIG)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import pandas as pd
|
|
16
|
+
import numpy as np
|
|
17
|
+
import matplotlib.pyplot as plt
|
|
18
|
+
import seaborn as sns
|
|
19
|
+
from datetime import datetime, timedelta
|
|
20
|
+
import ivolatility as ivol
|
|
21
|
+
import os
|
|
22
|
+
|
|
23
|
+
# Set style
|
|
24
|
+
sns.set_style('darkgrid')
|
|
25
|
+
plt.rcParams['figure.figsize'] = (15, 8)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ============================================================
|
|
29
|
+
# GLOBAL API MANAGER
|
|
30
|
+
# ============================================================
|
|
31
|
+
class APIManager:
|
|
32
|
+
"""
|
|
33
|
+
Centralized API key management for IVolatility API
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
from ivolatility_backtesting import init_api, get_api_method
|
|
37
|
+
|
|
38
|
+
# Initialize once at the start
|
|
39
|
+
init_api(os.getenv("API_KEY"))
|
|
40
|
+
|
|
41
|
+
# Use anywhere in your code
|
|
42
|
+
getOptionsData = get_api_method('/equities/eod/stock-opts-by-param')
|
|
43
|
+
data = getOptionsData(symbol='SPY', tradeDate='2024-01-01', ...)
|
|
44
|
+
"""
|
|
45
|
+
_api_key = None
|
|
46
|
+
_methods = {}
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def initialize(cls, api_key):
|
|
50
|
+
"""Set API key globally - call this once at startup"""
|
|
51
|
+
if not api_key:
|
|
52
|
+
raise ValueError("API key cannot be empty")
|
|
53
|
+
cls._api_key = api_key
|
|
54
|
+
ivol.setLoginParams(apiKey=api_key)
|
|
55
|
+
print(f"[API] Initialized with key: {api_key[:10]}...{api_key[-5:]}")
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def get_method(cls, endpoint):
|
|
59
|
+
"""
|
|
60
|
+
Get API method with automatic key injection
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
endpoint: API endpoint path (e.g. '/equities/eod/stock-opts-by-param')
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Callable API method
|
|
67
|
+
"""
|
|
68
|
+
if cls._api_key is None:
|
|
69
|
+
# Auto-initialize from environment if not set
|
|
70
|
+
api_key = os.getenv("API_KEY")
|
|
71
|
+
if not api_key:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
"API key not initialized. Call init_api(key) first or set API_KEY environment variable"
|
|
74
|
+
)
|
|
75
|
+
cls.initialize(api_key)
|
|
76
|
+
|
|
77
|
+
# Cache methods to avoid recreation
|
|
78
|
+
if endpoint not in cls._methods:
|
|
79
|
+
# Re-set login params before creating method
|
|
80
|
+
ivol.setLoginParams(apiKey=cls._api_key)
|
|
81
|
+
cls._methods[endpoint] = ivol.setMethod(endpoint)
|
|
82
|
+
|
|
83
|
+
return cls._methods[endpoint]
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def is_initialized(cls):
|
|
87
|
+
"""Check if API is initialized"""
|
|
88
|
+
return cls._api_key is not None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# Public API functions
|
|
92
|
+
def init_api(api_key=None):
|
|
93
|
+
"""
|
|
94
|
+
Initialize IVolatility API with key
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
api_key: API key string. If None, tries to load from API_KEY env variable
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
init_api("your-api-key")
|
|
101
|
+
# or
|
|
102
|
+
init_api() # Auto-loads from environment
|
|
103
|
+
"""
|
|
104
|
+
if api_key is None:
|
|
105
|
+
api_key = os.getenv("API_KEY")
|
|
106
|
+
APIManager.initialize(api_key)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_api_method(endpoint):
|
|
110
|
+
"""
|
|
111
|
+
Get API method for specified endpoint
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
endpoint: API endpoint path
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Callable API method with key already configured
|
|
118
|
+
|
|
119
|
+
Example:
|
|
120
|
+
getOptionsData = get_api_method('/equities/eod/stock-opts-by-param')
|
|
121
|
+
data = getOptionsData(symbol='SPY', tradeDate='2024-01-01', cp='C')
|
|
122
|
+
"""
|
|
123
|
+
return APIManager.get_method(endpoint)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class BacktestResults:
|
|
127
|
+
"""
|
|
128
|
+
Universal container for backtest results
|
|
129
|
+
ANY strategy must return this format
|
|
130
|
+
"""
|
|
131
|
+
def __init__(self,
|
|
132
|
+
equity_curve, # List of equity values
|
|
133
|
+
equity_dates, # List of dates (matching equity_curve)
|
|
134
|
+
trades, # List of dicts with trade info
|
|
135
|
+
initial_capital, # Starting capital
|
|
136
|
+
config, # Strategy config dict
|
|
137
|
+
benchmark_prices=None, # Optional: dict {date: price}
|
|
138
|
+
benchmark_symbol='SPY', # Optional: benchmark ticker
|
|
139
|
+
daily_returns=None, # Optional: if not provided, calculated
|
|
140
|
+
debug_info=None): # Optional: debug information
|
|
141
|
+
|
|
142
|
+
self.equity_curve = equity_curve
|
|
143
|
+
self.equity_dates = equity_dates
|
|
144
|
+
self.trades = trades
|
|
145
|
+
self.initial_capital = initial_capital
|
|
146
|
+
self.final_capital = equity_curve[-1] if len(equity_curve) > 0 else initial_capital
|
|
147
|
+
self.config = config
|
|
148
|
+
self.benchmark_prices = benchmark_prices
|
|
149
|
+
self.benchmark_symbol = benchmark_symbol
|
|
150
|
+
self.debug_info = debug_info if debug_info else []
|
|
151
|
+
|
|
152
|
+
# Calculate daily returns if not provided
|
|
153
|
+
if daily_returns is None and len(equity_curve) > 1:
|
|
154
|
+
self.daily_returns = [
|
|
155
|
+
(equity_curve[i] - equity_curve[i-1]) / equity_curve[i-1]
|
|
156
|
+
for i in range(1, len(equity_curve))
|
|
157
|
+
]
|
|
158
|
+
else:
|
|
159
|
+
self.daily_returns = daily_returns if daily_returns else []
|
|
160
|
+
|
|
161
|
+
# Calculate max drawdown
|
|
162
|
+
self.max_drawdown = self._calculate_max_drawdown()
|
|
163
|
+
|
|
164
|
+
def _calculate_max_drawdown(self):
|
|
165
|
+
if len(self.equity_curve) < 2:
|
|
166
|
+
return 0
|
|
167
|
+
running_max = np.maximum.accumulate(self.equity_curve)
|
|
168
|
+
drawdowns = (np.array(self.equity_curve) - running_max) / running_max * 100
|
|
169
|
+
return abs(np.min(drawdowns))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class BacktestAnalyzer:
|
|
173
|
+
"""
|
|
174
|
+
Universal metrics calculator
|
|
175
|
+
Works with any BacktestResults object
|
|
176
|
+
"""
|
|
177
|
+
def __init__(self, results):
|
|
178
|
+
self.results = results
|
|
179
|
+
self.metrics = {}
|
|
180
|
+
|
|
181
|
+
def calculate_all_metrics(self):
|
|
182
|
+
"""Calculate all available metrics"""
|
|
183
|
+
r = self.results
|
|
184
|
+
|
|
185
|
+
# Basic profitability
|
|
186
|
+
self.metrics['total_pnl'] = r.final_capital - r.initial_capital
|
|
187
|
+
self.metrics['total_return'] = (self.metrics['total_pnl'] / r.initial_capital) * 100
|
|
188
|
+
|
|
189
|
+
# CAGR - WITH PROTECTION AGAINST DIVISION BY ZERO
|
|
190
|
+
if len(r.equity_dates) > 0:
|
|
191
|
+
start_date = min(r.equity_dates)
|
|
192
|
+
end_date = max(r.equity_dates)
|
|
193
|
+
days_diff = (end_date - start_date).days
|
|
194
|
+
|
|
195
|
+
# PROTECTION: если даты одинаковые или разница < 1 день
|
|
196
|
+
if days_diff <= 0:
|
|
197
|
+
self.metrics['cagr'] = 0
|
|
198
|
+
self.metrics['show_cagr'] = False
|
|
199
|
+
else:
|
|
200
|
+
years = days_diff / 365.25
|
|
201
|
+
|
|
202
|
+
if years >= 1.0:
|
|
203
|
+
self.metrics['cagr'] = ((r.final_capital / r.initial_capital) ** (1/years) - 1) * 100
|
|
204
|
+
self.metrics['show_cagr'] = True
|
|
205
|
+
else:
|
|
206
|
+
self.metrics['cagr'] = self.metrics['total_return'] * (365.25 / days_diff)
|
|
207
|
+
self.metrics['show_cagr'] = False
|
|
208
|
+
else:
|
|
209
|
+
self.metrics['cagr'] = 0
|
|
210
|
+
self.metrics['show_cagr'] = False
|
|
211
|
+
|
|
212
|
+
# Risk metrics
|
|
213
|
+
self.metrics['sharpe'] = self._sharpe_ratio(r.daily_returns)
|
|
214
|
+
self.metrics['sortino'] = self._sortino_ratio(r.daily_returns)
|
|
215
|
+
self.metrics['max_drawdown'] = r.max_drawdown
|
|
216
|
+
|
|
217
|
+
if len(r.daily_returns) > 0:
|
|
218
|
+
self.metrics['volatility'] = np.std(r.daily_returns) * np.sqrt(252) * 100
|
|
219
|
+
else:
|
|
220
|
+
self.metrics['volatility'] = 0
|
|
221
|
+
|
|
222
|
+
self.metrics['calmar'] = abs(self.metrics['total_return'] / r.max_drawdown) if r.max_drawdown > 0 else 0
|
|
223
|
+
self.metrics['omega'] = self._omega_ratio(r.daily_returns)
|
|
224
|
+
self.metrics['ulcer'] = self._ulcer_index(r.equity_curve)
|
|
225
|
+
|
|
226
|
+
# VaR
|
|
227
|
+
self.metrics['var_95'], self.metrics['var_95_pct'] = self._calculate_var(r.daily_returns, 0.95)
|
|
228
|
+
self.metrics['var_99'], self.metrics['var_99_pct'] = self._calculate_var(r.daily_returns, 0.99)
|
|
229
|
+
self.metrics['cvar_95'], self.metrics['cvar_95_pct'] = self._calculate_cvar(r.daily_returns, 0.95)
|
|
230
|
+
|
|
231
|
+
avg_equity = np.mean(r.equity_curve) if len(r.equity_curve) > 0 else r.initial_capital
|
|
232
|
+
self.metrics['var_95_dollar'] = self.metrics['var_95'] * avg_equity
|
|
233
|
+
self.metrics['var_99_dollar'] = self.metrics['var_99'] * avg_equity
|
|
234
|
+
self.metrics['cvar_95_dollar'] = self.metrics['cvar_95'] * avg_equity
|
|
235
|
+
|
|
236
|
+
# Distribution
|
|
237
|
+
self.metrics['tail_ratio'] = self._tail_ratio(r.daily_returns)
|
|
238
|
+
self.metrics['skewness'], self.metrics['kurtosis'] = self._skewness_kurtosis(r.daily_returns)
|
|
239
|
+
|
|
240
|
+
# Alpha/Beta
|
|
241
|
+
self.metrics['alpha'], self.metrics['beta'], self.metrics['r_squared'] = self._alpha_beta(r)
|
|
242
|
+
|
|
243
|
+
# Trading stats
|
|
244
|
+
if len(r.trades) > 0:
|
|
245
|
+
trades_df = pd.DataFrame(r.trades)
|
|
246
|
+
winning = trades_df[trades_df['pnl'] > 0]
|
|
247
|
+
losing = trades_df[trades_df['pnl'] <= 0]
|
|
248
|
+
|
|
249
|
+
self.metrics['total_trades'] = len(trades_df)
|
|
250
|
+
self.metrics['winning_trades'] = len(winning)
|
|
251
|
+
self.metrics['losing_trades'] = len(losing)
|
|
252
|
+
self.metrics['win_rate'] = (len(winning) / len(trades_df)) * 100 if len(trades_df) > 0 else 0
|
|
253
|
+
|
|
254
|
+
wins_sum = winning['pnl'].sum() if len(winning) > 0 else 0
|
|
255
|
+
losses_sum = abs(losing['pnl'].sum()) if len(losing) > 0 else 0
|
|
256
|
+
self.metrics['profit_factor'] = wins_sum / losses_sum if losses_sum > 0 else float('inf')
|
|
257
|
+
|
|
258
|
+
self.metrics['avg_win'] = winning['pnl'].mean() if len(winning) > 0 else 0
|
|
259
|
+
self.metrics['avg_loss'] = losing['pnl'].mean() if len(losing) > 0 else 0
|
|
260
|
+
self.metrics['best_trade'] = trades_df['pnl'].max()
|
|
261
|
+
self.metrics['worst_trade'] = trades_df['pnl'].min()
|
|
262
|
+
|
|
263
|
+
if len(winning) > 0 and len(losing) > 0:
|
|
264
|
+
self.metrics['avg_win_loss_ratio'] = abs(self.metrics['avg_win'] / self.metrics['avg_loss'])
|
|
265
|
+
else:
|
|
266
|
+
self.metrics['avg_win_loss_ratio'] = 0
|
|
267
|
+
|
|
268
|
+
self.metrics['max_win_streak'], self.metrics['max_loss_streak'] = self._win_loss_streaks(r.trades)
|
|
269
|
+
else:
|
|
270
|
+
self.metrics.update({
|
|
271
|
+
'total_trades': 0, 'winning_trades': 0, 'losing_trades': 0,
|
|
272
|
+
'win_rate': 0, 'profit_factor': 0, 'avg_win': 0, 'avg_loss': 0,
|
|
273
|
+
'best_trade': 0, 'worst_trade': 0, 'avg_win_loss_ratio': 0,
|
|
274
|
+
'max_win_streak': 0, 'max_loss_streak': 0
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
# Efficiency
|
|
278
|
+
running_max = np.maximum.accumulate(r.equity_curve)
|
|
279
|
+
max_dd_dollars = np.min(np.array(r.equity_curve) - running_max)
|
|
280
|
+
self.metrics['recovery_factor'] = self.metrics['total_pnl'] / abs(max_dd_dollars) if max_dd_dollars != 0 else 0
|
|
281
|
+
|
|
282
|
+
# Exposure time
|
|
283
|
+
if len(r.trades) > 0 and 'start_date' in r.config and 'end_date' in r.config:
|
|
284
|
+
total_days = (pd.to_datetime(r.config['end_date']) - pd.to_datetime(r.config['start_date'])).days
|
|
285
|
+
self.metrics['exposure_time'] = self._exposure_time(r.trades, total_days)
|
|
286
|
+
else:
|
|
287
|
+
self.metrics['exposure_time'] = 0
|
|
288
|
+
|
|
289
|
+
return self.metrics
|
|
290
|
+
|
|
291
|
+
def _sharpe_ratio(self, returns):
|
|
292
|
+
if len(returns) < 2:
|
|
293
|
+
return 0
|
|
294
|
+
return np.sqrt(252) * np.mean(returns) / np.std(returns) if np.std(returns) > 0 else 0
|
|
295
|
+
|
|
296
|
+
def _sortino_ratio(self, returns):
|
|
297
|
+
if len(returns) < 2:
|
|
298
|
+
return 0
|
|
299
|
+
returns_array = np.array(returns)
|
|
300
|
+
downside = returns_array[returns_array < 0]
|
|
301
|
+
if len(downside) == 0 or np.std(downside) == 0:
|
|
302
|
+
return 0
|
|
303
|
+
return np.sqrt(252) * np.mean(returns_array) / np.std(downside)
|
|
304
|
+
|
|
305
|
+
def _omega_ratio(self, returns, threshold=0):
|
|
306
|
+
if len(returns) < 2:
|
|
307
|
+
return 0
|
|
308
|
+
returns_array = np.array(returns)
|
|
309
|
+
gains = np.sum(np.maximum(returns_array - threshold, 0))
|
|
310
|
+
losses = np.sum(np.maximum(threshold - returns_array, 0))
|
|
311
|
+
return gains / losses if losses > 0 else float('inf')
|
|
312
|
+
|
|
313
|
+
def _ulcer_index(self, equity_curve):
|
|
314
|
+
if len(equity_curve) < 2:
|
|
315
|
+
return 0
|
|
316
|
+
equity_array = np.array(equity_curve)
|
|
317
|
+
running_max = np.maximum.accumulate(equity_array)
|
|
318
|
+
drawdown = (equity_array - running_max) / running_max
|
|
319
|
+
return np.sqrt(np.mean(drawdown ** 2)) * 100
|
|
320
|
+
|
|
321
|
+
def _calculate_var(self, returns, confidence=0.95):
|
|
322
|
+
if len(returns) < 10:
|
|
323
|
+
return 0, 0
|
|
324
|
+
returns_array = np.array(returns)
|
|
325
|
+
returns_array = returns_array[~np.isnan(returns_array)]
|
|
326
|
+
if len(returns_array) < 10:
|
|
327
|
+
return 0, 0
|
|
328
|
+
var_percentile = (1 - confidence) * 100
|
|
329
|
+
var_return = np.percentile(returns_array, var_percentile)
|
|
330
|
+
return var_return, var_return * 100
|
|
331
|
+
|
|
332
|
+
def _calculate_cvar(self, returns, confidence=0.95):
|
|
333
|
+
if len(returns) < 10:
|
|
334
|
+
return 0, 0
|
|
335
|
+
returns_array = np.array(returns)
|
|
336
|
+
returns_array = returns_array[~np.isnan(returns_array)]
|
|
337
|
+
if len(returns_array) < 10:
|
|
338
|
+
return 0, 0
|
|
339
|
+
var_percentile = (1 - confidence) * 100
|
|
340
|
+
var_threshold = np.percentile(returns_array, var_percentile)
|
|
341
|
+
tail_losses = returns_array[returns_array <= var_threshold]
|
|
342
|
+
if len(tail_losses) == 0:
|
|
343
|
+
return 0, 0
|
|
344
|
+
cvar_return = np.mean(tail_losses)
|
|
345
|
+
return cvar_return, cvar_return * 100
|
|
346
|
+
|
|
347
|
+
def _tail_ratio(self, returns):
|
|
348
|
+
if len(returns) < 20:
|
|
349
|
+
return 0
|
|
350
|
+
returns_array = np.array(returns)
|
|
351
|
+
right = np.percentile(returns_array, 95)
|
|
352
|
+
left = abs(np.percentile(returns_array, 5))
|
|
353
|
+
return right / left if left > 0 else 0
|
|
354
|
+
|
|
355
|
+
def _skewness_kurtosis(self, returns):
|
|
356
|
+
if len(returns) < 10:
|
|
357
|
+
return 0, 0
|
|
358
|
+
returns_array = np.array(returns)
|
|
359
|
+
mean = np.mean(returns_array)
|
|
360
|
+
std = np.std(returns_array)
|
|
361
|
+
if std == 0:
|
|
362
|
+
return 0, 0
|
|
363
|
+
skew = np.mean(((returns_array - mean) / std) ** 3)
|
|
364
|
+
kurt = np.mean(((returns_array - mean) / std) ** 4) - 3
|
|
365
|
+
return skew, kurt
|
|
366
|
+
|
|
367
|
+
def _alpha_beta(self, results):
|
|
368
|
+
if not hasattr(results, 'benchmark_prices') or not results.benchmark_prices:
|
|
369
|
+
return 0, 0, 0
|
|
370
|
+
if len(results.equity_dates) < 10:
|
|
371
|
+
return 0, 0, 0
|
|
372
|
+
|
|
373
|
+
benchmark_returns = []
|
|
374
|
+
sorted_dates = sorted(results.equity_dates)
|
|
375
|
+
|
|
376
|
+
for i in range(1, len(sorted_dates)):
|
|
377
|
+
prev_date = sorted_dates[i-1]
|
|
378
|
+
curr_date = sorted_dates[i]
|
|
379
|
+
|
|
380
|
+
if prev_date in results.benchmark_prices and curr_date in results.benchmark_prices:
|
|
381
|
+
prev_price = results.benchmark_prices[prev_date]
|
|
382
|
+
curr_price = results.benchmark_prices[curr_date]
|
|
383
|
+
bench_return = (curr_price - prev_price) / prev_price
|
|
384
|
+
benchmark_returns.append(bench_return)
|
|
385
|
+
else:
|
|
386
|
+
benchmark_returns.append(0)
|
|
387
|
+
|
|
388
|
+
if len(benchmark_returns) != len(results.daily_returns):
|
|
389
|
+
return 0, 0, 0
|
|
390
|
+
|
|
391
|
+
port_ret = np.array(results.daily_returns)
|
|
392
|
+
bench_ret = np.array(benchmark_returns)
|
|
393
|
+
|
|
394
|
+
bench_mean = np.mean(bench_ret)
|
|
395
|
+
port_mean = np.mean(port_ret)
|
|
396
|
+
|
|
397
|
+
covariance = np.mean((bench_ret - bench_mean) * (port_ret - port_mean))
|
|
398
|
+
benchmark_variance = np.mean((bench_ret - bench_mean) ** 2)
|
|
399
|
+
|
|
400
|
+
if benchmark_variance == 0:
|
|
401
|
+
return 0, 0, 0
|
|
402
|
+
|
|
403
|
+
beta = covariance / benchmark_variance
|
|
404
|
+
alpha_daily = port_mean - beta * bench_mean
|
|
405
|
+
alpha_annualized = alpha_daily * 252 * 100
|
|
406
|
+
|
|
407
|
+
ss_res = np.sum((port_ret - (alpha_daily + beta * bench_ret)) ** 2)
|
|
408
|
+
ss_tot = np.sum((port_ret - port_mean) ** 2)
|
|
409
|
+
r_squared = 1 - (ss_res / ss_tot) if ss_tot > 0 else 0
|
|
410
|
+
|
|
411
|
+
return alpha_annualized, beta, r_squared
|
|
412
|
+
|
|
413
|
+
def _win_loss_streaks(self, trades):
|
|
414
|
+
if len(trades) == 0:
|
|
415
|
+
return 0, 0
|
|
416
|
+
max_win = max_loss = current_win = current_loss = 0
|
|
417
|
+
for trade in trades:
|
|
418
|
+
if trade['pnl'] > 0:
|
|
419
|
+
current_win += 1
|
|
420
|
+
current_loss = 0
|
|
421
|
+
max_win = max(max_win, current_win)
|
|
422
|
+
else:
|
|
423
|
+
current_loss += 1
|
|
424
|
+
current_win = 0
|
|
425
|
+
max_loss = max(max_loss, current_loss)
|
|
426
|
+
return max_win, max_loss
|
|
427
|
+
|
|
428
|
+
def _exposure_time(self, trades, total_days):
|
|
429
|
+
if total_days <= 0 or len(trades) == 0:
|
|
430
|
+
return 0
|
|
431
|
+
days_with_positions = set()
|
|
432
|
+
for trade in trades:
|
|
433
|
+
entry = pd.to_datetime(trade['entry_date'])
|
|
434
|
+
exit = pd.to_datetime(trade['exit_date'])
|
|
435
|
+
date_range = pd.date_range(start=entry, end=exit, freq='D')
|
|
436
|
+
days_with_positions.update(date_range.date)
|
|
437
|
+
exposure_pct = (len(days_with_positions) / total_days) * 100
|
|
438
|
+
return min(exposure_pct, 100.0)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
class ResultsReporter:
|
|
442
|
+
"""Universal results printer"""
|
|
443
|
+
|
|
444
|
+
@staticmethod
|
|
445
|
+
def print_full_report(analyzer):
|
|
446
|
+
m = analyzer.metrics
|
|
447
|
+
r = analyzer.results
|
|
448
|
+
|
|
449
|
+
print("="*80)
|
|
450
|
+
print(" "*25 + "BACKTEST RESULTS")
|
|
451
|
+
print("="*80)
|
|
452
|
+
print()
|
|
453
|
+
|
|
454
|
+
# PRINT DEBUG INFO IF AVAILABLE
|
|
455
|
+
if hasattr(r, 'debug_info') and len(r.debug_info) > 0:
|
|
456
|
+
print("DEBUG INFORMATION")
|
|
457
|
+
print("-"*80)
|
|
458
|
+
for debug_msg in r.debug_info[:10]: # First 10 messages
|
|
459
|
+
print(debug_msg)
|
|
460
|
+
if len(r.debug_info) > 10:
|
|
461
|
+
print(f"... and {len(r.debug_info) - 10} more debug messages")
|
|
462
|
+
print()
|
|
463
|
+
|
|
464
|
+
# Profitability
|
|
465
|
+
print("PROFITABILITY METRICS")
|
|
466
|
+
print("-"*80)
|
|
467
|
+
print(f"Initial Capital: ${r.initial_capital:>15,.2f}")
|
|
468
|
+
print(f"Final Equity: ${r.final_capital:>15,.2f}")
|
|
469
|
+
print(f"Total P&L: ${m['total_pnl']:>15,.2f} (absolute profit/loss)")
|
|
470
|
+
print(f"Total Return: {m['total_return']:>15.2f}% (% gain/loss)")
|
|
471
|
+
if m['cagr'] != 0:
|
|
472
|
+
if m['show_cagr']:
|
|
473
|
+
print(f"CAGR: {m['cagr']:>15.2f}% (annualized compound growth)")
|
|
474
|
+
else:
|
|
475
|
+
print(f"Annualized Return: {m['cagr']:>15.2f}% (extrapolated to 1 year)")
|
|
476
|
+
print()
|
|
477
|
+
|
|
478
|
+
# Risk
|
|
479
|
+
print("RISK METRICS")
|
|
480
|
+
print("-"*80)
|
|
481
|
+
print(f"Sharpe Ratio: {m['sharpe']:>15.2f} (>1 good, >2 excellent)")
|
|
482
|
+
print(f"Sortino Ratio: {m['sortino']:>15.2f} (downside risk, >2 good)")
|
|
483
|
+
print(f"Calmar Ratio: {m['calmar']:>15.2f} (return/drawdown, >3 good)")
|
|
484
|
+
if m['omega'] != 0:
|
|
485
|
+
omega_display = f"{m['omega']:.2f}" if m['omega'] < 999 else "inf"
|
|
486
|
+
print(f"Omega Ratio: {omega_display:>15s} (gains/losses, >1 good)")
|
|
487
|
+
print(f"Maximum Drawdown: {m['max_drawdown']:>15.2f}% (peak to trough)")
|
|
488
|
+
if m['ulcer'] != 0:
|
|
489
|
+
print(f"Ulcer Index: {m['ulcer']:>15.2f}% (pain of drawdowns, lower better)")
|
|
490
|
+
print(f"Volatility (ann.): {m['volatility']:>15.2f}% (annualized std dev)")
|
|
491
|
+
|
|
492
|
+
if len(r.daily_returns) >= 10:
|
|
493
|
+
print(f"VaR (95%, 1-day): {m['var_95_pct']:>15.2f}% (${m['var_95_dollar']:>,.0f}) (max loss 95% confidence)")
|
|
494
|
+
print(f"VaR (99%, 1-day): {m['var_99_pct']:>15.2f}% (${m['var_99_dollar']:>,.0f}) (max loss 99% confidence)")
|
|
495
|
+
print(f"CVaR (95%, 1-day): {m['cvar_95_pct']:>15.2f}% (${m['cvar_95_dollar']:>,.0f}) (avg loss in worst 5%)")
|
|
496
|
+
|
|
497
|
+
if m['tail_ratio'] != 0:
|
|
498
|
+
print(f"Tail Ratio (95/5): {m['tail_ratio']:>15.2f} (big wins/losses, >1 good)")
|
|
499
|
+
|
|
500
|
+
if m['skewness'] != 0 or m['kurtosis'] != 0:
|
|
501
|
+
print(f"Skewness: {m['skewness']:>15.2f} (>0 positive tail)")
|
|
502
|
+
print(f"Kurtosis (excess): {m['kurtosis']:>15.2f} (>0 fat tails)")
|
|
503
|
+
|
|
504
|
+
if m['beta'] != 0 or m['alpha'] != 0:
|
|
505
|
+
print(f"Alpha (vs {r.benchmark_symbol}): {m['alpha']:>15.2f}% (excess return)")
|
|
506
|
+
print(f"Beta (vs {r.benchmark_symbol}): {m['beta']:>15.2f} (<1 defensive, >1 aggressive)")
|
|
507
|
+
print(f"R^2 (vs {r.benchmark_symbol}): {m['r_squared']:>15.2f} (market correlation 0-1)")
|
|
508
|
+
|
|
509
|
+
# Warning for unrealistic results
|
|
510
|
+
if abs(m['total_return']) > 200 or m['volatility'] > 150:
|
|
511
|
+
print()
|
|
512
|
+
print("UNREALISTIC RESULTS DETECTED:")
|
|
513
|
+
if abs(m['total_return']) > 200:
|
|
514
|
+
print(f" Total return {m['total_return']:.1f}% is extremely high")
|
|
515
|
+
if m['volatility'] > 150:
|
|
516
|
+
print(f" Volatility {m['volatility']:.1f}% is higher than leveraged ETFs")
|
|
517
|
+
print(" Review configuration before trusting results")
|
|
518
|
+
|
|
519
|
+
print()
|
|
520
|
+
|
|
521
|
+
# Efficiency
|
|
522
|
+
print("EFFICIENCY METRICS")
|
|
523
|
+
print("-"*80)
|
|
524
|
+
if m['recovery_factor'] != 0:
|
|
525
|
+
print(f"Recovery Factor: {m['recovery_factor']:>15.2f} (profit/max DD, >3 good)")
|
|
526
|
+
if m['exposure_time'] != 0:
|
|
527
|
+
print(f"Exposure Time: {m['exposure_time']:>15.1f}% (time in market)")
|
|
528
|
+
print()
|
|
529
|
+
|
|
530
|
+
# Trading stats
|
|
531
|
+
print("TRADING STATISTICS")
|
|
532
|
+
print("-"*80)
|
|
533
|
+
print(f"Total Trades: {m['total_trades']:>15}")
|
|
534
|
+
print(f"Winning Trades: {m['winning_trades']:>15}")
|
|
535
|
+
print(f"Losing Trades: {m['losing_trades']:>15}")
|
|
536
|
+
print(f"Win Rate: {m['win_rate']:>15.2f}% (% profitable trades)")
|
|
537
|
+
print(f"Profit Factor: {m['profit_factor']:>15.2f} (gross profit/loss, >1.5 good)")
|
|
538
|
+
if m['max_win_streak'] > 0 or m['max_loss_streak'] > 0:
|
|
539
|
+
print(f"Max Win Streak: {m['max_win_streak']:>15} (consecutive wins)")
|
|
540
|
+
print(f"Max Loss Streak: {m['max_loss_streak']:>15} (consecutive losses)")
|
|
541
|
+
print(f"Average Win: ${m['avg_win']:>15,.2f}")
|
|
542
|
+
print(f"Average Loss: ${m['avg_loss']:>15,.2f}")
|
|
543
|
+
print(f"Best Trade: ${m['best_trade']:>15,.2f}")
|
|
544
|
+
print(f"Worst Trade: ${m['worst_trade']:>15,.2f}")
|
|
545
|
+
if m['avg_win_loss_ratio'] != 0:
|
|
546
|
+
print(f"Avg Win/Loss Ratio: {m['avg_win_loss_ratio']:>15.2f} (avg win / avg loss)")
|
|
547
|
+
print()
|
|
548
|
+
print("="*80)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
class ChartGenerator:
|
|
552
|
+
"""Universal chart creator"""
|
|
553
|
+
|
|
554
|
+
@staticmethod
|
|
555
|
+
def create_all_charts(analyzer, filename='backtest_results.png'):
|
|
556
|
+
r = analyzer.results
|
|
557
|
+
m = analyzer.metrics
|
|
558
|
+
|
|
559
|
+
if len(r.trades) == 0:
|
|
560
|
+
print("No trades to visualize")
|
|
561
|
+
return
|
|
562
|
+
|
|
563
|
+
trades_df = pd.DataFrame(r.trades)
|
|
564
|
+
fig, axes = plt.subplots(3, 2, figsize=(18, 14))
|
|
565
|
+
fig.suptitle('Backtest Results - Comprehensive Analysis',
|
|
566
|
+
fontsize=16, fontweight='bold', y=0.995)
|
|
567
|
+
|
|
568
|
+
dates = pd.to_datetime(r.equity_dates)
|
|
569
|
+
equity_array = np.array(r.equity_curve)
|
|
570
|
+
|
|
571
|
+
# Equity Curve
|
|
572
|
+
ax1 = axes[0, 0]
|
|
573
|
+
ax1.plot(dates, equity_array, linewidth=2.5, color='#2196F3')
|
|
574
|
+
ax1.axhline(y=r.initial_capital, color='gray', linestyle='--', alpha=0.7)
|
|
575
|
+
ax1.fill_between(dates, r.initial_capital, equity_array,
|
|
576
|
+
where=(equity_array >= r.initial_capital),
|
|
577
|
+
alpha=0.3, color='green', interpolate=True)
|
|
578
|
+
ax1.fill_between(dates, r.initial_capital, equity_array,
|
|
579
|
+
where=(equity_array < r.initial_capital),
|
|
580
|
+
alpha=0.3, color='red', interpolate=True)
|
|
581
|
+
ax1.set_title('Portfolio Equity Curve', fontsize=12, fontweight='bold')
|
|
582
|
+
ax1.set_ylabel('Equity ($)')
|
|
583
|
+
ax1.grid(True, alpha=0.3)
|
|
584
|
+
ax1.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'${x/1000:.0f}K'))
|
|
585
|
+
|
|
586
|
+
# Drawdown
|
|
587
|
+
ax2 = axes[0, 1]
|
|
588
|
+
running_max = np.maximum.accumulate(equity_array)
|
|
589
|
+
drawdown = (equity_array - running_max) / running_max * 100
|
|
590
|
+
ax2.fill_between(dates, 0, drawdown, alpha=0.6, color='#f44336')
|
|
591
|
+
ax2.plot(dates, drawdown, color='#d32f2f', linewidth=2)
|
|
592
|
+
max_dd_idx = np.argmin(drawdown)
|
|
593
|
+
ax2.scatter(dates[max_dd_idx], drawdown[max_dd_idx], color='darkred', s=100, zorder=5, marker='v')
|
|
594
|
+
ax2.set_title('Drawdown Over Time', fontsize=12, fontweight='bold')
|
|
595
|
+
ax2.set_ylabel('Drawdown (%)')
|
|
596
|
+
ax2.grid(True, alpha=0.3)
|
|
597
|
+
|
|
598
|
+
# P&L Distribution
|
|
599
|
+
ax3 = axes[1, 0]
|
|
600
|
+
pnl_values = trades_df['pnl'].values
|
|
601
|
+
ax3.hist(pnl_values, bins=40, color='#4CAF50', alpha=0.7, edgecolor='black')
|
|
602
|
+
ax3.axvline(x=0, color='red', linestyle='--', linewidth=2)
|
|
603
|
+
ax3.axvline(x=np.median(pnl_values), color='blue', linestyle='--', linewidth=2)
|
|
604
|
+
ax3.set_title('Trade P&L Distribution', fontsize=12, fontweight='bold')
|
|
605
|
+
ax3.set_xlabel('P&L ($)')
|
|
606
|
+
ax3.set_ylabel('Frequency')
|
|
607
|
+
ax3.grid(True, alpha=0.3, axis='y')
|
|
608
|
+
|
|
609
|
+
# Signal Performance
|
|
610
|
+
ax4 = axes[1, 1]
|
|
611
|
+
if 'signal' in trades_df.columns:
|
|
612
|
+
signal_pnl = trades_df.groupby('signal')['pnl'].sum()
|
|
613
|
+
colors = ['#4CAF50' if x > 0 else '#f44336' for x in signal_pnl.values]
|
|
614
|
+
bars = ax4.bar(signal_pnl.index, signal_pnl.values, color=colors, alpha=0.7, edgecolor='black')
|
|
615
|
+
for bar in bars:
|
|
616
|
+
height = bar.get_height()
|
|
617
|
+
ax4.text(bar.get_x() + bar.get_width()/2., height,
|
|
618
|
+
f'${height:,.0f}', ha='center', va='bottom' if height > 0 else 'top', fontweight='bold')
|
|
619
|
+
ax4.set_title('P&L by Signal Type', fontsize=12, fontweight='bold')
|
|
620
|
+
else:
|
|
621
|
+
ax4.text(0.5, 0.5, 'No signal data', ha='center', va='center', transform=ax4.transAxes)
|
|
622
|
+
ax4.set_ylabel('Total P&L ($)')
|
|
623
|
+
ax4.axhline(y=0, color='black', linestyle='-', linewidth=1)
|
|
624
|
+
ax4.grid(True, alpha=0.3, axis='y')
|
|
625
|
+
|
|
626
|
+
# Monthly Returns
|
|
627
|
+
ax5 = axes[2, 0]
|
|
628
|
+
trades_df['exit_date'] = pd.to_datetime(trades_df['exit_date'])
|
|
629
|
+
trades_df['month'] = trades_df['exit_date'].dt.to_period('M')
|
|
630
|
+
monthly_pnl = trades_df.groupby('month')['pnl'].sum()
|
|
631
|
+
colors_monthly = ['#4CAF50' if x > 0 else '#f44336' for x in monthly_pnl.values]
|
|
632
|
+
ax5.bar(range(len(monthly_pnl)), monthly_pnl.values, color=colors_monthly, alpha=0.7, edgecolor='black')
|
|
633
|
+
ax5.set_title('Monthly P&L', fontsize=12, fontweight='bold')
|
|
634
|
+
ax5.set_ylabel('P&L ($)')
|
|
635
|
+
ax5.set_xticks(range(len(monthly_pnl)))
|
|
636
|
+
ax5.set_xticklabels([str(m) for m in monthly_pnl.index], rotation=45, ha='right')
|
|
637
|
+
ax5.axhline(y=0, color='black', linestyle='-', linewidth=1)
|
|
638
|
+
ax5.grid(True, alpha=0.3, axis='y')
|
|
639
|
+
|
|
640
|
+
# Top Symbols
|
|
641
|
+
ax6 = axes[2, 1]
|
|
642
|
+
if 'symbol' in trades_df.columns:
|
|
643
|
+
symbol_pnl = trades_df.groupby('symbol')['pnl'].sum().sort_values(ascending=True).tail(10)
|
|
644
|
+
colors_symbols = ['#4CAF50' if x > 0 else '#f44336' for x in symbol_pnl.values]
|
|
645
|
+
ax6.barh(range(len(symbol_pnl)), symbol_pnl.values, color=colors_symbols, alpha=0.7, edgecolor='black')
|
|
646
|
+
ax6.set_yticks(range(len(symbol_pnl)))
|
|
647
|
+
ax6.set_yticklabels(symbol_pnl.index, fontsize=9)
|
|
648
|
+
ax6.set_title('Top 10 Symbols by P&L', fontsize=12, fontweight='bold')
|
|
649
|
+
else:
|
|
650
|
+
ax6.text(0.5, 0.5, 'No symbol data', ha='center', va='center', transform=ax6.transAxes)
|
|
651
|
+
ax6.set_xlabel('Total P&L ($)')
|
|
652
|
+
ax6.axvline(x=0, color='black', linestyle='-', linewidth=1)
|
|
653
|
+
ax6.grid(True, alpha=0.3, axis='x')
|
|
654
|
+
|
|
655
|
+
plt.tight_layout()
|
|
656
|
+
plt.savefig(filename, dpi=300, bbox_inches='tight')
|
|
657
|
+
plt.show()
|
|
658
|
+
|
|
659
|
+
print(f"Chart saved: {filename}")
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
class ResultsExporter:
|
|
663
|
+
"""Universal results exporter"""
|
|
664
|
+
|
|
665
|
+
@staticmethod
|
|
666
|
+
def export_all(analyzer, prefix='backtest'):
|
|
667
|
+
r = analyzer.results
|
|
668
|
+
m = analyzer.metrics
|
|
669
|
+
|
|
670
|
+
if len(r.trades) == 0:
|
|
671
|
+
print("No trades to export")
|
|
672
|
+
return
|
|
673
|
+
|
|
674
|
+
# Export trades
|
|
675
|
+
trades_df = pd.DataFrame(r.trades)
|
|
676
|
+
trades_df['entry_date'] = pd.to_datetime(trades_df['entry_date']).dt.strftime('%Y-%m-%d')
|
|
677
|
+
trades_df['exit_date'] = pd.to_datetime(trades_df['exit_date']).dt.strftime('%Y-%m-%d')
|
|
678
|
+
trades_df.to_csv(f'{prefix}_trades.csv', index=False)
|
|
679
|
+
print(f"Trades exported: {prefix}_trades.csv")
|
|
680
|
+
|
|
681
|
+
# Export equity curve
|
|
682
|
+
equity_df = pd.DataFrame({
|
|
683
|
+
'date': pd.to_datetime(r.equity_dates).strftime('%Y-%m-%d'),
|
|
684
|
+
'equity': r.equity_curve
|
|
685
|
+
})
|
|
686
|
+
equity_df.to_csv(f'{prefix}_equity.csv', index=False)
|
|
687
|
+
print(f"Equity exported: {prefix}_equity.csv")
|
|
688
|
+
|
|
689
|
+
# Export summary
|
|
690
|
+
with open(f'{prefix}_summary.txt', 'w') as f:
|
|
691
|
+
f.write("BACKTEST SUMMARY\n")
|
|
692
|
+
f.write("="*70 + "\n\n")
|
|
693
|
+
f.write(f"Strategy: {r.config.get('strategy_name', 'Unknown')}\n")
|
|
694
|
+
f.write(f"Period: {r.config.get('start_date', 'N/A')} to {r.config.get('end_date', 'N/A')}\n\n")
|
|
695
|
+
|
|
696
|
+
f.write("PERFORMANCE\n")
|
|
697
|
+
f.write("-"*70 + "\n")
|
|
698
|
+
f.write(f"Initial Capital: ${r.initial_capital:,.2f}\n")
|
|
699
|
+
f.write(f"Final Equity: ${r.final_capital:,.2f}\n")
|
|
700
|
+
f.write(f"Total Return: {m['total_return']:.2f}%\n")
|
|
701
|
+
f.write(f"Sharpe Ratio: {m['sharpe']:.2f}\n")
|
|
702
|
+
f.write(f"Max Drawdown: {m['max_drawdown']:.2f}%\n")
|
|
703
|
+
f.write(f"Win Rate: {m['win_rate']:.2f}%\n")
|
|
704
|
+
f.write(f"Total Trades: {m['total_trades']}\n")
|
|
705
|
+
|
|
706
|
+
print(f"Summary exported: {prefix}_summary.txt")
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
# ============================================================
|
|
710
|
+
# ONE-COMMAND RUNNER
|
|
711
|
+
# ============================================================
|
|
712
|
+
def run_backtest(strategy_function, config,
|
|
713
|
+
print_report=True,
|
|
714
|
+
create_charts=True,
|
|
715
|
+
export_results=True,
|
|
716
|
+
chart_filename='backtest_results.png',
|
|
717
|
+
export_prefix='backtest'):
|
|
718
|
+
"""
|
|
719
|
+
Run complete backtest with one command
|
|
720
|
+
|
|
721
|
+
Args:
|
|
722
|
+
strategy_function: Your strategy function that returns BacktestResults
|
|
723
|
+
config: Configuration dictionary for the strategy
|
|
724
|
+
print_report: Print full metrics report (default: True)
|
|
725
|
+
create_charts: Generate 6 charts (default: True)
|
|
726
|
+
export_results: Export to CSV files (default: True)
|
|
727
|
+
chart_filename: Name for chart file (default: 'backtest_results.png')
|
|
728
|
+
export_prefix: Prefix for exported files (default: 'backtest')
|
|
729
|
+
|
|
730
|
+
Returns:
|
|
731
|
+
BacktestAnalyzer object with all metrics
|
|
732
|
+
|
|
733
|
+
Example:
|
|
734
|
+
from ivolatility_backtesting import run_backtest
|
|
735
|
+
|
|
736
|
+
def my_strategy(config):
|
|
737
|
+
# ... your strategy logic
|
|
738
|
+
return BacktestResults(...)
|
|
739
|
+
|
|
740
|
+
CONFIG = {'initial_capital': 100000, ...}
|
|
741
|
+
|
|
742
|
+
# Run everything with one command!
|
|
743
|
+
analyzer = run_backtest(my_strategy, CONFIG)
|
|
744
|
+
|
|
745
|
+
# Access metrics
|
|
746
|
+
print(f"Sharpe: {analyzer.metrics['sharpe']:.2f}")
|
|
747
|
+
"""
|
|
748
|
+
|
|
749
|
+
print("="*80)
|
|
750
|
+
print(" "*25 + "STARTING BACKTEST")
|
|
751
|
+
print("="*80)
|
|
752
|
+
print(f"Strategy: {config.get('strategy_name', 'Unknown')}")
|
|
753
|
+
print(f"Period: {config.get('start_date', 'N/A')} to {config.get('end_date', 'N/A')}")
|
|
754
|
+
print(f"Capital: ${config.get('initial_capital', 0):,.0f}")
|
|
755
|
+
print("="*80 + "\n")
|
|
756
|
+
|
|
757
|
+
# Run strategy
|
|
758
|
+
results = strategy_function(config)
|
|
759
|
+
|
|
760
|
+
# Calculate metrics
|
|
761
|
+
print("\n[*] Calculating metrics...")
|
|
762
|
+
analyzer = BacktestAnalyzer(results)
|
|
763
|
+
analyzer.calculate_all_metrics()
|
|
764
|
+
|
|
765
|
+
# Print report
|
|
766
|
+
if print_report:
|
|
767
|
+
print("\n" + "="*80)
|
|
768
|
+
ResultsReporter.print_full_report(analyzer)
|
|
769
|
+
|
|
770
|
+
# Create charts
|
|
771
|
+
if create_charts and len(results.trades) > 0:
|
|
772
|
+
print(f"\n[*] Creating charts: {chart_filename}")
|
|
773
|
+
try:
|
|
774
|
+
ChartGenerator.create_all_charts(analyzer, chart_filename)
|
|
775
|
+
print(f"[OK] Charts saved: {chart_filename}")
|
|
776
|
+
except Exception as e:
|
|
777
|
+
print(f"[ERROR] Chart creation failed: {e}")
|
|
778
|
+
elif create_charts and len(results.trades) == 0:
|
|
779
|
+
print("\n[!] No trades - skipping charts")
|
|
780
|
+
|
|
781
|
+
# Export results
|
|
782
|
+
if export_results and len(results.trades) > 0:
|
|
783
|
+
print(f"\n[*] Exporting results: {export_prefix}_*.csv")
|
|
784
|
+
try:
|
|
785
|
+
ResultsExporter.export_all(analyzer, export_prefix)
|
|
786
|
+
print(f"[OK] Files exported:")
|
|
787
|
+
print(f" - {export_prefix}_trades.csv")
|
|
788
|
+
print(f" - {export_prefix}_equity.csv")
|
|
789
|
+
print(f" - {export_prefix}_summary.txt")
|
|
790
|
+
except Exception as e:
|
|
791
|
+
print(f"[ERROR] Export failed: {e}")
|
|
792
|
+
elif export_results and len(results.trades) == 0:
|
|
793
|
+
print("\n[!] No trades - skipping export")
|
|
794
|
+
|
|
795
|
+
# Final summary
|
|
796
|
+
#print("\n" + "="*80)
|
|
797
|
+
#print(" "*30 + "SUMMARY")
|
|
798
|
+
#print("="*80)
|
|
799
|
+
#print(f"Total Return: {analyzer.metrics['total_return']:>10.2f}%")
|
|
800
|
+
#print(f"Sharpe Ratio: {analyzer.metrics['sharpe']:>10.2f}")
|
|
801
|
+
#print(f"Max Drawdown: {analyzer.metrics['max_drawdown']:>10.2f}%")
|
|
802
|
+
#print(f"Win Rate: {analyzer.metrics['win_rate']:>10.1f}%")
|
|
803
|
+
#print(f"Total Trades: {analyzer.metrics['total_trades']:>10}")
|
|
804
|
+
#print(f"Profit Factor: {analyzer.metrics['profit_factor']:>10.2f}")
|
|
805
|
+
#print("="*80)
|
|
806
|
+
|
|
807
|
+
return analyzer
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
# Export all classes and functions
|
|
811
|
+
__all__ = [
|
|
812
|
+
'BacktestResults',
|
|
813
|
+
'BacktestAnalyzer',
|
|
814
|
+
'ResultsReporter',
|
|
815
|
+
'ChartGenerator',
|
|
816
|
+
'ResultsExporter',
|
|
817
|
+
'run_backtest',
|
|
818
|
+
'init_api',
|
|
819
|
+
'get_api_method',
|
|
820
|
+
'APIManager'
|
|
821
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Your Name
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ivolatility_backtesting
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Universal Backtest Framework with One-Command Runner for IVolatility API
|
|
5
|
+
Author-email: Your Name <your.email@example.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Your Name
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
Project-URL: Homepage, https://github.com/yourusername/ivolatility_backtesting
|
|
28
|
+
Project-URL: Bug Tracker, https://github.com/yourusername/ivolatility_backtesting/issues
|
|
29
|
+
Keywords: backtesting,finance,trading,ivolatility
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Operating System :: OS Independent
|
|
34
|
+
Requires-Python: >=3.8
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
License-File: LICENSE
|
|
37
|
+
Requires-Dist: pandas>=1.5.0
|
|
38
|
+
Requires-Dist: numpy>=1.21.0
|
|
39
|
+
Requires-Dist: matplotlib>=3.5.0
|
|
40
|
+
Requires-Dist: seaborn>=0.11.0
|
|
41
|
+
Requires-Dist: ivolatility
|
|
42
|
+
|
|
43
|
+
# IVolatility Backtesting
|
|
44
|
+
A universal backtesting framework for financial strategies using the IVolatility API.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
```bash
|
|
48
|
+
pip install ivolatility_backtesting
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Note**: The `ivolatility` package is required but may not be available on PyPI. Contact IVolatility to obtain their SDK and install it manually before using this package.
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
```python
|
|
55
|
+
from ivolatility_backtesting import run_backtest, init_api
|
|
56
|
+
|
|
57
|
+
# Initialize API
|
|
58
|
+
init_api("your-api-key")
|
|
59
|
+
|
|
60
|
+
# Define your strategy
|
|
61
|
+
def my_strategy(config):
|
|
62
|
+
# Strategy logic
|
|
63
|
+
return BacktestResults(
|
|
64
|
+
equity_curve=[100000, 110000],
|
|
65
|
+
equity_dates=["2023-01-01", "2023-01-02"],
|
|
66
|
+
trades=[{"pnl": 1000, "entry_date": "2023-01-01", "exit_date": "2023-01-02"}],
|
|
67
|
+
initial_capital=100000,
|
|
68
|
+
config=config
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Run backtest
|
|
72
|
+
CONFIG = {
|
|
73
|
+
"initial_capital": 100000,
|
|
74
|
+
"start_date": "2023-01-01",
|
|
75
|
+
"end_date": "2024-01-01",
|
|
76
|
+
"strategy_name": "My Strategy"
|
|
77
|
+
}
|
|
78
|
+
analyzer = run_backtest(my_strategy, CONFIG)
|
|
79
|
+
|
|
80
|
+
# Access metrics
|
|
81
|
+
print(f"Sharpe Ratio: {analyzer.metrics['sharpe']:.2f}")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Requirements
|
|
85
|
+
- Python >= 3.10
|
|
86
|
+
- pandas >= 1.5.0
|
|
87
|
+
- numpy >= 1.21.0
|
|
88
|
+
- matplotlib >= 3.5.0
|
|
89
|
+
- seaborn >= 0.11.0
|
|
90
|
+
- ivolatility (contact IVolatility for SDK)
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
MIT License
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ivolatility_backtesting/___init__.py,sha256=-VS3l4sUlmlMjVDwPY2BoXOVoYa5oVGYH9cscK5NLzw,395
|
|
2
|
+
ivolatility_backtesting/ivolatility_backtesting.py,sha256=-rm9jhQeHJ2i-EDHcw3FfZ9DdM6pyFPeAjk3Nsnxp34,34012
|
|
3
|
+
ivolatility_backtesting-0.1.0.dist-info/LICENSE,sha256=GTVQl3vH6ht70wJXKC0yMT8CmXKHxv_YyO_utAgm7EA,1065
|
|
4
|
+
ivolatility_backtesting-0.1.0.dist-info/METADATA,sha256=QMXGatRo6iknX4PeBx-ARrPVtyZ_pkkeBwIdClNSRmE,3444
|
|
5
|
+
ivolatility_backtesting-0.1.0.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
|
6
|
+
ivolatility_backtesting-0.1.0.dist-info/top_level.txt,sha256=Qv3irUBntr8b11WIKNN6zzCSguwaWC4nWR-ZKq8NsjY,24
|
|
7
|
+
ivolatility_backtesting-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ivolatility_backtesting
|