ivolatility-backtesting 1.1.0__py3-none-any.whl → 1.2.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 +10 -4
- ivolatility_backtesting/ivolatility_backtesting.py +297 -129
- {ivolatility_backtesting-1.1.0.dist-info → ivolatility_backtesting-1.2.0.dist-info}/METADATA +72 -70
- ivolatility_backtesting-1.2.0.dist-info/RECORD +7 -0
- {ivolatility_backtesting-1.1.0.dist-info → ivolatility_backtesting-1.2.0.dist-info}/WHEEL +1 -1
- ivolatility_backtesting-1.1.0.dist-info/RECORD +0 -7
- {ivolatility_backtesting-1.1.0.dist-info → ivolatility_backtesting-1.2.0.dist-info/licenses}/LICENSE +0 -0
- {ivolatility_backtesting-1.1.0.dist-info → ivolatility_backtesting-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
from .ivolatility_backtesting import (
|
|
2
2
|
BacktestResults, BacktestAnalyzer, ResultsReporter,
|
|
3
3
|
ChartGenerator, ResultsExporter, run_backtest,
|
|
4
|
-
init_api,
|
|
4
|
+
init_api, api_call, APIHelper, APIManager,
|
|
5
|
+
ResourceMonitor, create_progress_bar, update_progress, format_time
|
|
5
6
|
)
|
|
6
7
|
|
|
7
8
|
__all__ = [
|
|
8
9
|
'BacktestResults',
|
|
9
|
-
'BacktestAnalyzer',
|
|
10
|
+
'BacktestAnalyzer',
|
|
10
11
|
'ResultsReporter',
|
|
11
12
|
'ChartGenerator',
|
|
12
13
|
'ResultsExporter',
|
|
13
14
|
'run_backtest',
|
|
14
15
|
'init_api',
|
|
15
|
-
'
|
|
16
|
-
'
|
|
16
|
+
'api_call',
|
|
17
|
+
'APIHelper',
|
|
18
|
+
'APIManager',
|
|
19
|
+
'ResourceMonitor',
|
|
20
|
+
'create_progress_bar',
|
|
21
|
+
'update_progress',
|
|
22
|
+
'format_time'
|
|
17
23
|
]
|
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
"""
|
|
2
|
-
ivolatility_backtesting.py -
|
|
3
|
-
|
|
2
|
+
ivolatility_backtesting.py - UNIVERSAL BACKTEST FRAMEWORK
|
|
3
|
+
Version 2.0 - With DataFrame/Dict Auto-Normalization
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
5
|
+
Key Features:
|
|
6
|
+
- Handles both dict and DataFrame API responses automatically
|
|
7
|
+
- Universal BacktestResults interface
|
|
8
|
+
- 30+ calculated metrics
|
|
9
|
+
- Automatic reporting, charts, and exports
|
|
10
|
+
- One-command runner: run_backtest()
|
|
10
11
|
|
|
11
12
|
Usage:
|
|
12
13
|
from ivolatility_backtesting import *
|
|
13
14
|
|
|
14
|
-
# Initialize API
|
|
15
|
+
# Initialize API
|
|
15
16
|
init_api(os.getenv("API_KEY"))
|
|
16
17
|
|
|
17
|
-
# Use
|
|
18
|
+
# Use api_call for normalized responses
|
|
18
19
|
data = api_call('/equities/eod/stock-prices', symbol='AAPL', from_='2024-01-01')
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
|
|
21
|
+
# Run backtest
|
|
22
|
+
analyzer = run_backtest(my_strategy, CONFIG)
|
|
21
23
|
"""
|
|
22
24
|
|
|
23
25
|
import pandas as pd
|
|
@@ -27,6 +29,8 @@ import seaborn as sns
|
|
|
27
29
|
from datetime import datetime, timedelta
|
|
28
30
|
import ivolatility as ivol
|
|
29
31
|
import os
|
|
32
|
+
import time
|
|
33
|
+
import psutil
|
|
30
34
|
|
|
31
35
|
# Set style
|
|
32
36
|
sns.set_style('darkgrid')
|
|
@@ -34,40 +38,217 @@ plt.rcParams['figure.figsize'] = (15, 8)
|
|
|
34
38
|
|
|
35
39
|
|
|
36
40
|
# ============================================================
|
|
37
|
-
#
|
|
41
|
+
# RESOURCE MONITOR - NEW!
|
|
42
|
+
# ============================================================
|
|
43
|
+
class ResourceMonitor:
|
|
44
|
+
"""Monitor CPU and RAM using cgroups v2 and psutil fallback"""
|
|
45
|
+
|
|
46
|
+
def __init__(self):
|
|
47
|
+
self.process = psutil.Process()
|
|
48
|
+
self.cpu_count = psutil.cpu_count()
|
|
49
|
+
self.last_cpu_time = None
|
|
50
|
+
self.last_check_time = None
|
|
51
|
+
self.use_cgroups = self._check_cgroups_v2()
|
|
52
|
+
|
|
53
|
+
def _check_cgroups_v2(self):
|
|
54
|
+
"""Check if cgroups v2 is available"""
|
|
55
|
+
try:
|
|
56
|
+
return os.path.exists('/sys/fs/cgroup/cpu.stat') and \
|
|
57
|
+
os.path.exists('/sys/fs/cgroup/memory.current')
|
|
58
|
+
except:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
def _read_cgroup_cpu(self):
|
|
62
|
+
"""Read CPU usage from cgroups v2"""
|
|
63
|
+
try:
|
|
64
|
+
with open('/sys/fs/cgroup/cpu.stat', 'r') as f:
|
|
65
|
+
for line in f:
|
|
66
|
+
if line.startswith('usage_usec'):
|
|
67
|
+
return int(line.split()[1])
|
|
68
|
+
except:
|
|
69
|
+
pass
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
def _read_cgroup_memory(self):
|
|
73
|
+
"""Read memory usage from cgroups v2"""
|
|
74
|
+
try:
|
|
75
|
+
with open('/sys/fs/cgroup/memory.current', 'r') as f:
|
|
76
|
+
current = int(f.read().strip())
|
|
77
|
+
with open('/sys/fs/cgroup/memory.max', 'r') as f:
|
|
78
|
+
max_mem = f.read().strip()
|
|
79
|
+
if max_mem == 'max':
|
|
80
|
+
max_mem = psutil.virtual_memory().total
|
|
81
|
+
else:
|
|
82
|
+
max_mem = int(max_mem)
|
|
83
|
+
return current, max_mem
|
|
84
|
+
except:
|
|
85
|
+
pass
|
|
86
|
+
return None, None
|
|
87
|
+
|
|
88
|
+
def get_cpu_percent(self):
|
|
89
|
+
"""Get CPU usage percentage with cgroups v2 fallback to psutil"""
|
|
90
|
+
if self.use_cgroups:
|
|
91
|
+
current_time = time.time()
|
|
92
|
+
current_cpu = self._read_cgroup_cpu()
|
|
93
|
+
|
|
94
|
+
if current_cpu and self.last_cpu_time and self.last_check_time:
|
|
95
|
+
time_delta = current_time - self.last_check_time
|
|
96
|
+
cpu_delta = current_cpu - self.last_cpu_time
|
|
97
|
+
|
|
98
|
+
if time_delta > 0:
|
|
99
|
+
# Convert microseconds to percentage
|
|
100
|
+
cpu_percent = (cpu_delta / (time_delta * 1_000_000)) * 100
|
|
101
|
+
cpu_percent = min(cpu_percent, 100 * self.cpu_count)
|
|
102
|
+
|
|
103
|
+
self.last_cpu_time = current_cpu
|
|
104
|
+
self.last_check_time = current_time
|
|
105
|
+
|
|
106
|
+
return round(cpu_percent, 1)
|
|
107
|
+
|
|
108
|
+
self.last_cpu_time = current_cpu
|
|
109
|
+
self.last_check_time = current_time
|
|
110
|
+
|
|
111
|
+
# Fallback to psutil
|
|
112
|
+
try:
|
|
113
|
+
cpu = self.process.cpu_percent(interval=0.1)
|
|
114
|
+
return round(cpu, 1) if cpu > 0 else round(psutil.cpu_percent(interval=0.1), 1)
|
|
115
|
+
except:
|
|
116
|
+
return 0.0
|
|
117
|
+
|
|
118
|
+
def get_memory_info(self):
|
|
119
|
+
"""Get memory usage (MB and %) with cgroups v2 fallback to psutil"""
|
|
120
|
+
if self.use_cgroups:
|
|
121
|
+
current, max_mem = self._read_cgroup_memory()
|
|
122
|
+
if current and max_mem:
|
|
123
|
+
mb = current / (1024 * 1024)
|
|
124
|
+
percent = (current / max_mem) * 100
|
|
125
|
+
return round(mb, 1), round(percent, 1)
|
|
126
|
+
|
|
127
|
+
# Fallback to psutil
|
|
128
|
+
try:
|
|
129
|
+
mem = self.process.memory_info()
|
|
130
|
+
mb = mem.rss / (1024 * 1024)
|
|
131
|
+
total = psutil.virtual_memory().total
|
|
132
|
+
percent = (mem.rss / total) * 100
|
|
133
|
+
return round(mb, 1), round(percent, 1)
|
|
134
|
+
except:
|
|
135
|
+
return 0.0, 0.0
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def create_progress_bar():
|
|
139
|
+
"""
|
|
140
|
+
Create enhanced progress bar with ETA, CPU%, RAM
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
tuple: (progress_bar, status_label, resource_monitor, start_time)
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
progress_bar, status_label, monitor, start_time = create_progress_bar()
|
|
147
|
+
|
|
148
|
+
for i in range(total):
|
|
149
|
+
update_progress(progress_bar, status_label, monitor, i, total, start_time)
|
|
150
|
+
"""
|
|
151
|
+
from IPython.display import display
|
|
152
|
+
import ipywidgets as widgets
|
|
153
|
+
|
|
154
|
+
progress_bar = widgets.FloatProgress(
|
|
155
|
+
value=0, min=0, max=100,
|
|
156
|
+
description='Progress:',
|
|
157
|
+
bar_style='info',
|
|
158
|
+
style={'bar_color': '#00ff00'},
|
|
159
|
+
layout=widgets.Layout(width='100%', height='30px')
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
status_label = widgets.HTML(
|
|
163
|
+
value="<b style='color:#0066cc'>Starting...</b>"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
display(widgets.VBox([progress_bar, status_label]))
|
|
167
|
+
|
|
168
|
+
monitor = ResourceMonitor()
|
|
169
|
+
start_time = time.time()
|
|
170
|
+
|
|
171
|
+
return progress_bar, status_label, monitor, start_time
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def update_progress(progress_bar, status_label, monitor, current, total, start_time, message="Processing"):
|
|
175
|
+
"""
|
|
176
|
+
Update progress bar with ETA, CPU%, RAM
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
progress_bar: Progress widget
|
|
180
|
+
status_label: Status HTML widget
|
|
181
|
+
monitor: ResourceMonitor instance
|
|
182
|
+
current: Current iteration (0-based)
|
|
183
|
+
total: Total iterations
|
|
184
|
+
start_time: Start timestamp
|
|
185
|
+
message: Status message
|
|
186
|
+
"""
|
|
187
|
+
progress = (current / total) * 100
|
|
188
|
+
progress_bar.value = progress
|
|
189
|
+
|
|
190
|
+
# Calculate ETA
|
|
191
|
+
elapsed = time.time() - start_time
|
|
192
|
+
if current > 0:
|
|
193
|
+
eta_seconds = (elapsed / current) * (total - current)
|
|
194
|
+
eta_str = format_time(eta_seconds)
|
|
195
|
+
else:
|
|
196
|
+
eta_str = "calculating..."
|
|
197
|
+
|
|
198
|
+
# Get resources
|
|
199
|
+
cpu = monitor.get_cpu_percent()
|
|
200
|
+
ram_mb, ram_pct = monitor.get_memory_info()
|
|
201
|
+
|
|
202
|
+
# Update status
|
|
203
|
+
status_label.value = (
|
|
204
|
+
f"<b style='color:#0066cc'>{message} ({current}/{total})</b><br>"
|
|
205
|
+
f"<span style='color:#666'>ETA: {eta_str} | CPU: {cpu}% | RAM: {ram_mb}MB ({ram_pct}%)</span>"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def format_time(seconds):
|
|
210
|
+
"""Format seconds to human readable time"""
|
|
211
|
+
if seconds < 60:
|
|
212
|
+
return f"{int(seconds)}s"
|
|
213
|
+
elif seconds < 3600:
|
|
214
|
+
return f"{int(seconds // 60)}m {int(seconds % 60)}s"
|
|
215
|
+
else:
|
|
216
|
+
hours = int(seconds // 3600)
|
|
217
|
+
minutes = int((seconds % 3600) // 60)
|
|
218
|
+
return f"{hours}h {minutes}m"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# ============================================================
|
|
222
|
+
# API HELPER - AUTOMATIC NORMALIZATION
|
|
38
223
|
# ============================================================
|
|
39
224
|
class APIHelper:
|
|
40
225
|
"""
|
|
41
|
-
|
|
42
|
-
|
|
226
|
+
Normalizes API responses to consistent format
|
|
227
|
+
Handles: dict, DataFrame, or None
|
|
43
228
|
"""
|
|
44
229
|
|
|
45
230
|
@staticmethod
|
|
46
231
|
def normalize_response(response, debug=False):
|
|
47
232
|
"""
|
|
48
|
-
Convert API response to
|
|
49
|
-
|
|
50
|
-
Args:
|
|
51
|
-
response: API response (dict, DataFrame, or other)
|
|
52
|
-
debug: Print debug information
|
|
233
|
+
Convert API response to dict format
|
|
53
234
|
|
|
54
235
|
Returns:
|
|
55
|
-
dict with 'data' key
|
|
236
|
+
dict with 'data' key or None
|
|
56
237
|
"""
|
|
57
238
|
if response is None:
|
|
58
239
|
if debug:
|
|
59
240
|
print("[APIHelper] Response is None")
|
|
60
241
|
return None
|
|
61
242
|
|
|
62
|
-
# Case 1:
|
|
243
|
+
# Case 1: Dict with 'data' key
|
|
63
244
|
if isinstance(response, dict):
|
|
64
245
|
if 'data' in response:
|
|
65
246
|
if debug:
|
|
66
|
-
print(f"[APIHelper] Dict response
|
|
247
|
+
print(f"[APIHelper] Dict response: {len(response['data'])} records")
|
|
67
248
|
return response
|
|
68
249
|
else:
|
|
69
250
|
if debug:
|
|
70
|
-
print("[APIHelper] Dict
|
|
251
|
+
print("[APIHelper] Dict without 'data' key")
|
|
71
252
|
return None
|
|
72
253
|
|
|
73
254
|
# Case 2: DataFrame - convert to dict
|
|
@@ -79,73 +260,39 @@ class APIHelper:
|
|
|
79
260
|
|
|
80
261
|
records = response.to_dict('records')
|
|
81
262
|
if debug:
|
|
82
|
-
print(f"[APIHelper]
|
|
263
|
+
print(f"[APIHelper] DataFrame converted: {len(records)} records")
|
|
83
264
|
return {'data': records, 'status': 'success'}
|
|
84
265
|
|
|
85
266
|
# Case 3: Unknown type
|
|
86
267
|
if debug:
|
|
87
|
-
print(f"[APIHelper] Unexpected
|
|
268
|
+
print(f"[APIHelper] Unexpected type: {type(response)}")
|
|
88
269
|
return None
|
|
89
|
-
|
|
90
|
-
@staticmethod
|
|
91
|
-
def safe_dataframe(response, debug=False):
|
|
92
|
-
"""
|
|
93
|
-
Safely convert API response to DataFrame
|
|
94
|
-
|
|
95
|
-
Args:
|
|
96
|
-
response: API response (any type)
|
|
97
|
-
debug: Print debug information
|
|
98
|
-
|
|
99
|
-
Returns:
|
|
100
|
-
pandas DataFrame or empty DataFrame if invalid
|
|
101
|
-
"""
|
|
102
|
-
normalized = APIHelper.normalize_response(response, debug=debug)
|
|
103
|
-
|
|
104
|
-
if normalized is None or 'data' not in normalized:
|
|
105
|
-
if debug:
|
|
106
|
-
print("[APIHelper] Cannot create DataFrame - no valid data")
|
|
107
|
-
return pd.DataFrame()
|
|
108
|
-
|
|
109
|
-
try:
|
|
110
|
-
df = pd.DataFrame(normalized['data'])
|
|
111
|
-
if debug:
|
|
112
|
-
print(f"[APIHelper] Created DataFrame with shape {df.shape}")
|
|
113
|
-
return df
|
|
114
|
-
except Exception as e:
|
|
115
|
-
if debug:
|
|
116
|
-
print(f"[APIHelper] DataFrame creation failed: {e}")
|
|
117
|
-
return pd.DataFrame()
|
|
118
270
|
|
|
119
271
|
|
|
120
272
|
# ============================================================
|
|
121
|
-
#
|
|
273
|
+
# API MANAGER
|
|
122
274
|
# ============================================================
|
|
123
275
|
class APIManager:
|
|
124
|
-
"""
|
|
125
|
-
Centralized API key management for IVolatility API
|
|
126
|
-
Now includes response normalization
|
|
127
|
-
"""
|
|
276
|
+
"""Centralized API key management"""
|
|
128
277
|
_api_key = None
|
|
129
278
|
_methods = {}
|
|
130
279
|
|
|
131
280
|
@classmethod
|
|
132
281
|
def initialize(cls, api_key):
|
|
133
|
-
"""
|
|
282
|
+
"""Initialize API with key"""
|
|
134
283
|
if not api_key:
|
|
135
284
|
raise ValueError("API key cannot be empty")
|
|
136
285
|
cls._api_key = api_key
|
|
137
286
|
ivol.setLoginParams(apiKey=api_key)
|
|
138
|
-
print(f"[API] Initialized
|
|
287
|
+
print(f"[API] Initialized: {api_key[:10]}...{api_key[-5:]}")
|
|
139
288
|
|
|
140
289
|
@classmethod
|
|
141
290
|
def get_method(cls, endpoint):
|
|
142
|
-
"""Get API method with
|
|
291
|
+
"""Get API method with cached instances"""
|
|
143
292
|
if cls._api_key is None:
|
|
144
293
|
api_key = os.getenv("API_KEY")
|
|
145
294
|
if not api_key:
|
|
146
|
-
raise ValueError(
|
|
147
|
-
"API key not initialized. Call init_api(key) first or set API_KEY environment variable"
|
|
148
|
-
)
|
|
295
|
+
raise ValueError("API key not set. Call init_api(key) first")
|
|
149
296
|
cls.initialize(api_key)
|
|
150
297
|
|
|
151
298
|
if endpoint not in cls._methods:
|
|
@@ -153,77 +300,91 @@ class APIManager:
|
|
|
153
300
|
cls._methods[endpoint] = ivol.setMethod(endpoint)
|
|
154
301
|
|
|
155
302
|
return cls._methods[endpoint]
|
|
156
|
-
|
|
157
|
-
@classmethod
|
|
158
|
-
def is_initialized(cls):
|
|
159
|
-
"""Check if API is initialized"""
|
|
160
|
-
return cls._api_key is not None
|
|
161
303
|
|
|
162
304
|
|
|
163
|
-
#
|
|
305
|
+
# ============================================================
|
|
306
|
+
# PUBLIC API FUNCTIONS
|
|
307
|
+
# ============================================================
|
|
164
308
|
def init_api(api_key=None):
|
|
165
|
-
"""
|
|
309
|
+
"""
|
|
310
|
+
Initialize IVolatility API
|
|
311
|
+
|
|
312
|
+
Example:
|
|
313
|
+
init_api("your-api-key")
|
|
314
|
+
# or
|
|
315
|
+
init_api() # Auto-load from API_KEY env variable
|
|
316
|
+
"""
|
|
166
317
|
if api_key is None:
|
|
167
318
|
api_key = os.getenv("API_KEY")
|
|
168
319
|
APIManager.initialize(api_key)
|
|
169
320
|
|
|
170
321
|
|
|
171
|
-
def get_api_method(endpoint):
|
|
172
|
-
"""Get API method for specified endpoint"""
|
|
173
|
-
return APIManager.get_method(endpoint)
|
|
174
|
-
|
|
175
|
-
|
|
176
322
|
def api_call(endpoint, debug=False, **kwargs):
|
|
177
323
|
"""
|
|
178
324
|
Make API call with automatic response normalization
|
|
179
325
|
|
|
180
326
|
Args:
|
|
181
327
|
endpoint: API endpoint path
|
|
182
|
-
debug: Enable debug output
|
|
328
|
+
debug: Enable debug output (prints full URL with API key)
|
|
183
329
|
**kwargs: API parameters
|
|
184
330
|
|
|
185
331
|
Returns:
|
|
186
|
-
dict with 'data' key
|
|
332
|
+
dict with 'data' key or None
|
|
187
333
|
|
|
188
334
|
Example:
|
|
189
|
-
#
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
elif isinstance(response, dict):
|
|
195
|
-
df = pd.DataFrame(response['data'])
|
|
335
|
+
# Automatic handling of dict or DataFrame
|
|
336
|
+
data = api_call('/equities/eod/stock-prices',
|
|
337
|
+
symbol='AAPL',
|
|
338
|
+
from_='2024-01-01',
|
|
339
|
+
debug=True)
|
|
196
340
|
|
|
197
|
-
# New way (automatic):
|
|
198
|
-
data = api_call('/equities/eod/stock-prices', symbol='AAPL', from_='2024-01-01')
|
|
199
341
|
if data:
|
|
200
342
|
df = pd.DataFrame(data['data'])
|
|
201
343
|
"""
|
|
202
344
|
try:
|
|
203
|
-
|
|
345
|
+
# Build full URL for debugging
|
|
346
|
+
if debug and APIManager._api_key:
|
|
347
|
+
base_url = "https://restapi.ivolatility.com"
|
|
348
|
+
|
|
349
|
+
# Convert Python parameter names to API parameter names
|
|
350
|
+
# from_ -> from, to_ -> to
|
|
351
|
+
url_params = {}
|
|
352
|
+
for key, value in kwargs.items():
|
|
353
|
+
# Remove trailing underscore for reserved Python keywords
|
|
354
|
+
clean_key = key.rstrip('_') if key.endswith('_') else key
|
|
355
|
+
url_params[clean_key] = value
|
|
356
|
+
|
|
357
|
+
params_str = "&".join([f"{k}={v}" for k, v in url_params.items()])
|
|
358
|
+
full_url = f"{base_url}{endpoint}?apiKey={APIManager._api_key}&{params_str}"
|
|
359
|
+
print(f"\n[API] Full URL:")
|
|
360
|
+
print(f"[API] {full_url}\n")
|
|
361
|
+
|
|
362
|
+
method = APIManager.get_method(endpoint)
|
|
204
363
|
response = method(**kwargs)
|
|
205
364
|
|
|
206
365
|
normalized = APIHelper.normalize_response(response, debug=debug)
|
|
207
366
|
|
|
208
367
|
if normalized is None and debug:
|
|
209
|
-
print(f"[api_call] Failed to get
|
|
210
|
-
print(f"[api_call]
|
|
368
|
+
print(f"[api_call] ❌ Failed to get data")
|
|
369
|
+
print(f"[api_call] Endpoint: {endpoint}")
|
|
370
|
+
print(f"[api_call] Params: {kwargs}")
|
|
211
371
|
|
|
212
372
|
return normalized
|
|
213
373
|
|
|
214
374
|
except Exception as e:
|
|
215
375
|
if debug:
|
|
216
|
-
print(f"[api_call] Exception: {e}")
|
|
376
|
+
print(f"[api_call] ❌ Exception: {e}")
|
|
217
377
|
print(f"[api_call] Endpoint: {endpoint}")
|
|
218
|
-
print(f"[api_call]
|
|
378
|
+
print(f"[api_call] Params: {kwargs}")
|
|
219
379
|
return None
|
|
220
380
|
|
|
221
381
|
|
|
222
382
|
# ============================================================
|
|
223
|
-
# BACKTEST RESULTS
|
|
383
|
+
# BACKTEST RESULTS
|
|
224
384
|
# ============================================================
|
|
225
385
|
class BacktestResults:
|
|
226
386
|
"""Universal container for backtest results"""
|
|
387
|
+
|
|
227
388
|
def __init__(self,
|
|
228
389
|
equity_curve,
|
|
229
390
|
equity_dates,
|
|
@@ -264,10 +425,11 @@ class BacktestResults:
|
|
|
264
425
|
|
|
265
426
|
|
|
266
427
|
# ============================================================
|
|
267
|
-
# BACKTEST ANALYZER
|
|
428
|
+
# BACKTEST ANALYZER
|
|
268
429
|
# ============================================================
|
|
269
430
|
class BacktestAnalyzer:
|
|
270
|
-
"""
|
|
431
|
+
"""Calculate 30+ metrics from BacktestResults"""
|
|
432
|
+
|
|
271
433
|
def __init__(self, results):
|
|
272
434
|
self.results = results
|
|
273
435
|
self.metrics = {}
|
|
@@ -276,11 +438,11 @@ class BacktestAnalyzer:
|
|
|
276
438
|
"""Calculate all available metrics"""
|
|
277
439
|
r = self.results
|
|
278
440
|
|
|
279
|
-
#
|
|
441
|
+
# Profitability
|
|
280
442
|
self.metrics['total_pnl'] = r.final_capital - r.initial_capital
|
|
281
443
|
self.metrics['total_return'] = (self.metrics['total_pnl'] / r.initial_capital) * 100
|
|
282
444
|
|
|
283
|
-
# CAGR with protection
|
|
445
|
+
# CAGR with zero-division protection
|
|
284
446
|
if len(r.equity_dates) > 0:
|
|
285
447
|
start_date = min(r.equity_dates)
|
|
286
448
|
end_date = max(r.equity_dates)
|
|
@@ -291,7 +453,6 @@ class BacktestAnalyzer:
|
|
|
291
453
|
self.metrics['show_cagr'] = False
|
|
292
454
|
else:
|
|
293
455
|
years = days_diff / 365.25
|
|
294
|
-
|
|
295
456
|
if years >= 1.0:
|
|
296
457
|
self.metrics['cagr'] = ((r.final_capital / r.initial_capital) ** (1/years) - 1) * 100
|
|
297
458
|
self.metrics['show_cagr'] = True
|
|
@@ -532,11 +693,10 @@ class BacktestAnalyzer:
|
|
|
532
693
|
|
|
533
694
|
|
|
534
695
|
# ============================================================
|
|
535
|
-
# RESULTS REPORTER
|
|
536
|
-
# (All unchanged - same as before)
|
|
696
|
+
# RESULTS REPORTER
|
|
537
697
|
# ============================================================
|
|
538
698
|
class ResultsReporter:
|
|
539
|
-
"""
|
|
699
|
+
"""Print comprehensive metrics report"""
|
|
540
700
|
|
|
541
701
|
@staticmethod
|
|
542
702
|
def print_full_report(analyzer):
|
|
@@ -554,7 +714,7 @@ class ResultsReporter:
|
|
|
554
714
|
for debug_msg in r.debug_info[:10]:
|
|
555
715
|
print(debug_msg)
|
|
556
716
|
if len(r.debug_info) > 10:
|
|
557
|
-
print(f"... and {len(r.debug_info) - 10} more
|
|
717
|
+
print(f"... and {len(r.debug_info) - 10} more messages")
|
|
558
718
|
print()
|
|
559
719
|
|
|
560
720
|
print("PROFITABILITY METRICS")
|
|
@@ -600,14 +760,15 @@ class ResultsReporter:
|
|
|
600
760
|
print(f"Beta (vs {r.benchmark_symbol}): {m['beta']:>15.2f} (<1 defensive, >1 aggressive)")
|
|
601
761
|
print(f"R^2 (vs {r.benchmark_symbol}): {m['r_squared']:>15.2f} (market correlation 0-1)")
|
|
602
762
|
|
|
763
|
+
# Warning for unrealistic results
|
|
603
764
|
if abs(m['total_return']) > 200 or m['volatility'] > 150:
|
|
604
765
|
print()
|
|
605
|
-
print("UNREALISTIC RESULTS DETECTED:")
|
|
766
|
+
print("⚠️ UNREALISTIC RESULTS DETECTED:")
|
|
606
767
|
if abs(m['total_return']) > 200:
|
|
607
|
-
print(f" Total return {m['total_return']:.1f}% is extremely high")
|
|
768
|
+
print(f" • Total return {m['total_return']:.1f}% is extremely high")
|
|
608
769
|
if m['volatility'] > 150:
|
|
609
|
-
print(f" Volatility {m['volatility']:.1f}% is higher than leveraged ETFs")
|
|
610
|
-
print(" Review configuration before trusting results")
|
|
770
|
+
print(f" • Volatility {m['volatility']:.1f}% is higher than leveraged ETFs")
|
|
771
|
+
print(" → Review configuration before trusting results")
|
|
611
772
|
|
|
612
773
|
print()
|
|
613
774
|
|
|
@@ -639,8 +800,11 @@ class ResultsReporter:
|
|
|
639
800
|
print("="*80)
|
|
640
801
|
|
|
641
802
|
|
|
803
|
+
# ============================================================
|
|
804
|
+
# CHART GENERATOR
|
|
805
|
+
# ============================================================
|
|
642
806
|
class ChartGenerator:
|
|
643
|
-
"""
|
|
807
|
+
"""Generate 6 professional charts"""
|
|
644
808
|
|
|
645
809
|
@staticmethod
|
|
646
810
|
def create_all_charts(analyzer, filename='backtest_results.png'):
|
|
@@ -750,8 +914,11 @@ class ChartGenerator:
|
|
|
750
914
|
print(f"Chart saved: {filename}")
|
|
751
915
|
|
|
752
916
|
|
|
917
|
+
# ============================================================
|
|
918
|
+
# RESULTS EXPORTER
|
|
919
|
+
# ============================================================
|
|
753
920
|
class ResultsExporter:
|
|
754
|
-
"""
|
|
921
|
+
"""Export results to CSV files"""
|
|
755
922
|
|
|
756
923
|
@staticmethod
|
|
757
924
|
def export_all(analyzer, prefix='backtest'):
|
|
@@ -762,25 +929,27 @@ class ResultsExporter:
|
|
|
762
929
|
print("No trades to export")
|
|
763
930
|
return
|
|
764
931
|
|
|
932
|
+
# Trades
|
|
765
933
|
trades_df = pd.DataFrame(r.trades)
|
|
766
934
|
trades_df['entry_date'] = pd.to_datetime(trades_df['entry_date']).dt.strftime('%Y-%m-%d')
|
|
767
935
|
trades_df['exit_date'] = pd.to_datetime(trades_df['exit_date']).dt.strftime('%Y-%m-%d')
|
|
768
936
|
trades_df.to_csv(f'{prefix}_trades.csv', index=False)
|
|
769
|
-
print(f"
|
|
937
|
+
print(f"Exported: {prefix}_trades.csv")
|
|
770
938
|
|
|
939
|
+
# Equity
|
|
771
940
|
equity_df = pd.DataFrame({
|
|
772
941
|
'date': pd.to_datetime(r.equity_dates).strftime('%Y-%m-%d'),
|
|
773
942
|
'equity': r.equity_curve
|
|
774
943
|
})
|
|
775
944
|
equity_df.to_csv(f'{prefix}_equity.csv', index=False)
|
|
776
|
-
print(f"
|
|
945
|
+
print(f"Exported: {prefix}_equity.csv")
|
|
777
946
|
|
|
947
|
+
# Summary
|
|
778
948
|
with open(f'{prefix}_summary.txt', 'w') as f:
|
|
779
949
|
f.write("BACKTEST SUMMARY\n")
|
|
780
950
|
f.write("="*70 + "\n\n")
|
|
781
951
|
f.write(f"Strategy: {r.config.get('strategy_name', 'Unknown')}\n")
|
|
782
952
|
f.write(f"Period: {r.config.get('start_date', 'N/A')} to {r.config.get('end_date', 'N/A')}\n\n")
|
|
783
|
-
|
|
784
953
|
f.write("PERFORMANCE\n")
|
|
785
954
|
f.write("-"*70 + "\n")
|
|
786
955
|
f.write(f"Initial Capital: ${r.initial_capital:,.2f}\n")
|
|
@@ -791,11 +960,11 @@ class ResultsExporter:
|
|
|
791
960
|
f.write(f"Win Rate: {m['win_rate']:.2f}%\n")
|
|
792
961
|
f.write(f"Total Trades: {m['total_trades']}\n")
|
|
793
962
|
|
|
794
|
-
print(f"
|
|
963
|
+
print(f"Exported: {prefix}_summary.txt")
|
|
795
964
|
|
|
796
965
|
|
|
797
966
|
# ============================================================
|
|
798
|
-
# ONE-COMMAND RUNNER
|
|
967
|
+
# ONE-COMMAND RUNNER
|
|
799
968
|
# ============================================================
|
|
800
969
|
def run_backtest(strategy_function, config,
|
|
801
970
|
print_report=True,
|
|
@@ -803,7 +972,12 @@ def run_backtest(strategy_function, config,
|
|
|
803
972
|
export_results=True,
|
|
804
973
|
chart_filename='backtest_results.png',
|
|
805
974
|
export_prefix='backtest'):
|
|
806
|
-
"""
|
|
975
|
+
"""
|
|
976
|
+
Run complete backtest with one command
|
|
977
|
+
|
|
978
|
+
Example:
|
|
979
|
+
analyzer = run_backtest(my_strategy, CONFIG)
|
|
980
|
+
"""
|
|
807
981
|
|
|
808
982
|
print("="*80)
|
|
809
983
|
print(" "*25 + "STARTING BACKTEST")
|
|
@@ -827,24 +1001,15 @@ def run_backtest(strategy_function, config,
|
|
|
827
1001
|
print(f"\n[*] Creating charts: {chart_filename}")
|
|
828
1002
|
try:
|
|
829
1003
|
ChartGenerator.create_all_charts(analyzer, chart_filename)
|
|
830
|
-
print(f"[OK] Charts saved: {chart_filename}")
|
|
831
1004
|
except Exception as e:
|
|
832
1005
|
print(f"[ERROR] Chart creation failed: {e}")
|
|
833
|
-
elif create_charts and len(results.trades) == 0:
|
|
834
|
-
print("\n[!] No trades - skipping charts")
|
|
835
1006
|
|
|
836
1007
|
if export_results and len(results.trades) > 0:
|
|
837
|
-
print(f"\n[*] Exporting results: {export_prefix}_
|
|
1008
|
+
print(f"\n[*] Exporting results: {export_prefix}_*")
|
|
838
1009
|
try:
|
|
839
1010
|
ResultsExporter.export_all(analyzer, export_prefix)
|
|
840
|
-
print(f"[OK] Files exported:")
|
|
841
|
-
print(f" - {export_prefix}_trades.csv")
|
|
842
|
-
print(f" - {export_prefix}_equity.csv")
|
|
843
|
-
print(f" - {export_prefix}_summary.txt")
|
|
844
1011
|
except Exception as e:
|
|
845
1012
|
print(f"[ERROR] Export failed: {e}")
|
|
846
|
-
elif export_results and len(results.trades) == 0:
|
|
847
|
-
print("\n[!] No trades - skipping export")
|
|
848
1013
|
|
|
849
1014
|
return analyzer
|
|
850
1015
|
|
|
@@ -860,8 +1025,11 @@ __all__ = [
|
|
|
860
1025
|
'ResultsExporter',
|
|
861
1026
|
'run_backtest',
|
|
862
1027
|
'init_api',
|
|
863
|
-
'
|
|
864
|
-
'
|
|
865
|
-
'
|
|
866
|
-
'
|
|
1028
|
+
'api_call',
|
|
1029
|
+
'APIHelper',
|
|
1030
|
+
'APIManager',
|
|
1031
|
+
'ResourceMonitor',
|
|
1032
|
+
'create_progress_bar',
|
|
1033
|
+
'update_progress',
|
|
1034
|
+
'format_time'
|
|
867
1035
|
]
|
{ivolatility_backtesting-1.1.0.dist-info → ivolatility_backtesting-1.2.0.dist-info}/METADATA
RENAMED
|
@@ -1,70 +1,72 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
2
|
-
Name: ivolatility_backtesting
|
|
3
|
-
Version: 1.
|
|
4
|
-
Summary: A universal backtesting framework for financial strategies using the IVolatility API.
|
|
5
|
-
Author-email: IVolatility <support@ivolatility.com>
|
|
6
|
-
Project-URL: Homepage, https://ivolatility.com
|
|
7
|
-
Keywords: backtesting,finance,trading,ivolatility
|
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
-
Classifier: Operating System :: OS Independent
|
|
16
|
-
Requires-Python: >=3.8
|
|
17
|
-
Description-Content-Type: text/markdown
|
|
18
|
-
License-File: LICENSE
|
|
19
|
-
Requires-Dist: pandas>=1.5.0
|
|
20
|
-
Requires-Dist: numpy>=1.21.0
|
|
21
|
-
Requires-Dist: matplotlib>=3.5.0
|
|
22
|
-
Requires-Dist: seaborn>=0.11.0
|
|
23
|
-
Requires-Dist: ivolatility>=1.8.2
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ivolatility_backtesting
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: A universal backtesting framework for financial strategies using the IVolatility API.
|
|
5
|
+
Author-email: IVolatility <support@ivolatility.com>
|
|
6
|
+
Project-URL: Homepage, https://ivolatility.com
|
|
7
|
+
Keywords: backtesting,finance,trading,ivolatility
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: pandas>=1.5.0
|
|
20
|
+
Requires-Dist: numpy>=1.21.0
|
|
21
|
+
Requires-Dist: matplotlib>=3.5.0
|
|
22
|
+
Requires-Dist: seaborn>=0.11.0
|
|
23
|
+
Requires-Dist: ivolatility>=1.8.2
|
|
24
|
+
Requires-Dist: psutil>=7.1.0
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# IVolatility Backtesting
|
|
28
|
+
A universal backtesting framework for financial strategies using the IVolatility API.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
```bash
|
|
32
|
+
pip install ivolatility_backtesting
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
```python
|
|
37
|
+
from ivolatility_backtesting import run_backtest, init_api
|
|
38
|
+
|
|
39
|
+
# Initialize API
|
|
40
|
+
init_api("your-api-key")
|
|
41
|
+
|
|
42
|
+
# Define your strategy
|
|
43
|
+
def my_strategy(config):
|
|
44
|
+
# Strategy logic
|
|
45
|
+
return BacktestResults(
|
|
46
|
+
equity_curve=[100000, 110000],
|
|
47
|
+
equity_dates=["2023-01-01", "2023-01-02"],
|
|
48
|
+
trades=[{"pnl": 1000, "entry_date": "2023-01-01", "exit_date": "2023-01-02"}],
|
|
49
|
+
initial_capital=100000,
|
|
50
|
+
config=config
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Run backtest
|
|
54
|
+
CONFIG = {
|
|
55
|
+
"initial_capital": 100000,
|
|
56
|
+
"start_date": "2023-01-01",
|
|
57
|
+
"end_date": "2024-01-01",
|
|
58
|
+
"strategy_name": "My Strategy"
|
|
59
|
+
}
|
|
60
|
+
analyzer = run_backtest(my_strategy, CONFIG)
|
|
61
|
+
|
|
62
|
+
# Access metrics
|
|
63
|
+
print(f"Sharpe Ratio: {analyzer.metrics['sharpe']:.2f}")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Requirements
|
|
67
|
+
- Python >= 3.8
|
|
68
|
+
- pandas >= 1.5.0
|
|
69
|
+
- numpy >= 1.21.0
|
|
70
|
+
- matplotlib >= 3.5.0
|
|
71
|
+
- seaborn >= 0.11.0
|
|
72
|
+
- ivolatility >= 1.8.2
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ivolatility_backtesting/__init__.py,sha256=abZYqTZwvzgSdSs55g3_zU8mtbNKveUndoDgKU8tnIo,577
|
|
2
|
+
ivolatility_backtesting/ivolatility_backtesting.py,sha256=_lo2QrdWTf8IVpp4AIIGw7_t88GhbSeHRAT4KEcwmBw,40916
|
|
3
|
+
ivolatility_backtesting-1.2.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
ivolatility_backtesting-1.2.0.dist-info/METADATA,sha256=SRFqAyNI-qOs2CeX3DZF0kJwbVnsQQMbOCkPs2LNOKc,2052
|
|
5
|
+
ivolatility_backtesting-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
ivolatility_backtesting-1.2.0.dist-info/top_level.txt,sha256=Qv3irUBntr8b11WIKNN6zzCSguwaWC4nWR-ZKq8NsjY,24
|
|
7
|
+
ivolatility_backtesting-1.2.0.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
ivolatility_backtesting/__init__.py,sha256=-VS3l4sUlmlMjVDwPY2BoXOVoYa5oVGYH9cscK5NLzw,395
|
|
2
|
-
ivolatility_backtesting/ivolatility_backtesting.py,sha256=GLO_h72_mPmLDoknBIH6_rRWXxrZ0BSAr7Q85QTFBkk,35661
|
|
3
|
-
ivolatility_backtesting-1.1.0.dist-info/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
ivolatility_backtesting-1.1.0.dist-info/METADATA,sha256=d8vKlXZcuCGCiXzwbJ2DQncLGnFdP844f4NXi0aDG18,2071
|
|
5
|
-
ivolatility_backtesting-1.1.0.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
|
6
|
-
ivolatility_backtesting-1.1.0.dist-info/top_level.txt,sha256=Qv3irUBntr8b11WIKNN6zzCSguwaWC4nWR-ZKq8NsjY,24
|
|
7
|
-
ivolatility_backtesting-1.1.0.dist-info/RECORD,,
|
{ivolatility_backtesting-1.1.0.dist-info → ivolatility_backtesting-1.2.0.dist-info/licenses}/LICENSE
RENAMED
|
File without changes
|
{ivolatility_backtesting-1.1.0.dist-info → ivolatility_backtesting-1.2.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|