ivolatility-backtesting 1.1.0__py3-none-any.whl → 1.3.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 +424 -258
- {ivolatility_backtesting-1.1.0.dist-info → ivolatility_backtesting-1.3.0.dist-info}/METADATA +72 -70
- ivolatility_backtesting-1.3.0.dist-info/RECORD +7 -0
- {ivolatility_backtesting-1.1.0.dist-info → ivolatility_backtesting-1.3.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.3.0.dist-info/licenses}/LICENSE +0 -0
- {ivolatility_backtesting-1.1.0.dist-info → ivolatility_backtesting-1.3.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,38 @@
|
|
|
1
1
|
"""
|
|
2
|
-
ivolatility_backtesting.py -
|
|
3
|
-
|
|
2
|
+
ivolatility_backtesting.py - UNIVERSAL BACKTEST FRAMEWORK
|
|
3
|
+
Version 4.1 - Dual RAM Display (Process + Container)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
5
|
+
Key Features:
|
|
6
|
+
- ResourceMonitor: CPU/RAM tracking (cgroups v2 + psutil fallback)
|
|
7
|
+
- Enhanced progress bar with ETA, CPU%, RAM
|
|
8
|
+
- Shows BOTH Python process RAM AND container total RAM
|
|
9
|
+
- api_call(): Auto-normalization for dict/DataFrame responses
|
|
10
|
+
- 30+ metrics, charts, exports
|
|
11
|
+
- One-command: run_backtest()
|
|
10
12
|
|
|
11
13
|
Usage:
|
|
12
14
|
from ivolatility_backtesting import *
|
|
13
15
|
|
|
14
|
-
# Initialize API once
|
|
15
16
|
init_api(os.getenv("API_KEY"))
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
data = api_call('/equities/eod/stock-prices',
|
|
19
|
+
symbol='AAPL',
|
|
20
|
+
from_='2024-01-01',
|
|
21
|
+
to='2024-12-31',
|
|
22
|
+
debug=True)
|
|
23
|
+
|
|
24
|
+
analyzer = run_backtest(my_strategy, CONFIG)
|
|
25
|
+
|
|
26
|
+
Resource Monitoring:
|
|
27
|
+
- CPU: Process CPU % (smoothed over 5 readings)
|
|
28
|
+
- RAM: Shows BOTH metrics when in container:
|
|
29
|
+
* Green: Python process memory (your strategy)
|
|
30
|
+
* Blue: Total container memory (includes Jupyter, cache, etc.)
|
|
31
|
+
|
|
32
|
+
Progress Display Example:
|
|
33
|
+
Processing 2024-07-30 (144/252)
|
|
34
|
+
ETA: 5m 23s | CPU: 46.8% | RAM: 856MB (42%) Python | 1280MB (64%) Container
|
|
35
|
+
Container: 1.0 cores, 2.0GB limit
|
|
21
36
|
"""
|
|
22
37
|
|
|
23
38
|
import pandas as pd
|
|
@@ -27,50 +42,303 @@ import seaborn as sns
|
|
|
27
42
|
from datetime import datetime, timedelta
|
|
28
43
|
import ivolatility as ivol
|
|
29
44
|
import os
|
|
45
|
+
import time
|
|
46
|
+
import psutil
|
|
30
47
|
|
|
31
|
-
# Set style
|
|
32
48
|
sns.set_style('darkgrid')
|
|
33
49
|
plt.rcParams['figure.figsize'] = (15, 8)
|
|
34
50
|
|
|
35
51
|
|
|
36
52
|
# ============================================================
|
|
37
|
-
#
|
|
53
|
+
# RESOURCE MONITOR
|
|
38
54
|
# ============================================================
|
|
39
|
-
class
|
|
40
|
-
"""
|
|
41
|
-
Helper class for normalized API responses
|
|
42
|
-
Automatically handles both dict and DataFrame responses
|
|
43
|
-
"""
|
|
55
|
+
class ResourceMonitor:
|
|
56
|
+
"""Monitor CPU and RAM - shows PROCESS resources (Python), not full container"""
|
|
44
57
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
def __init__(self, show_container_total=False):
|
|
59
|
+
self.process = psutil.Process()
|
|
60
|
+
self.cpu_count = psutil.cpu_count()
|
|
61
|
+
self.last_cpu_time = None
|
|
62
|
+
self.last_check_time = None
|
|
63
|
+
self.use_cgroups = self._check_cgroups_v2()
|
|
64
|
+
self.show_container_total = show_container_total # False = process RAM, True = container RAM
|
|
65
|
+
|
|
66
|
+
# CPU smoothing for more stable readings
|
|
67
|
+
self.cpu_history = []
|
|
68
|
+
self.cpu_history_max = 5 # Average over last 5 readings
|
|
69
|
+
|
|
70
|
+
# Determine actual CPU quota for containers
|
|
71
|
+
if self.use_cgroups:
|
|
72
|
+
quota = self._read_cpu_quota()
|
|
73
|
+
if quota and quota > 0:
|
|
74
|
+
self.cpu_count = quota # Override with container quota
|
|
75
|
+
|
|
76
|
+
self.context = "Container" if self.use_cgroups else "Host"
|
|
77
|
+
|
|
78
|
+
def _read_cpu_quota(self):
|
|
79
|
+
"""Read CPU quota from cgroups v2 (returns cores, e.g., 1.5)"""
|
|
80
|
+
try:
|
|
81
|
+
with open('/sys/fs/cgroup/cpu.max', 'r') as f:
|
|
82
|
+
line = f.read().strip()
|
|
83
|
+
if line == 'max':
|
|
84
|
+
return None # No limit
|
|
85
|
+
parts = line.split()
|
|
86
|
+
if len(parts) == 2:
|
|
87
|
+
quota = int(parts[0]) # microseconds
|
|
88
|
+
period = int(parts[1]) # microseconds
|
|
89
|
+
return quota / period # cores (e.g., 100000/100000 = 1.0)
|
|
90
|
+
except:
|
|
91
|
+
pass
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
def get_context_info(self):
|
|
95
|
+
"""Returns monitoring context and resource limits"""
|
|
96
|
+
if self.use_cgroups:
|
|
97
|
+
current, max_mem = self._read_cgroup_memory()
|
|
98
|
+
ram_info = ""
|
|
99
|
+
if max_mem:
|
|
100
|
+
max_mem_gb = max_mem / (1024**3)
|
|
101
|
+
ram_info = f", {max_mem_gb:.1f}GB limit"
|
|
102
|
+
|
|
103
|
+
mem_type = "container total" if self.show_container_total else "process only"
|
|
104
|
+
return f"Container (CPU: {self.cpu_count:.1f} cores{ram_info}) - RAM: {mem_type}"
|
|
105
|
+
else:
|
|
106
|
+
total_ram_gb = psutil.virtual_memory().total / (1024**3)
|
|
107
|
+
return f"Host ({self.cpu_count} cores, {total_ram_gb:.0f}GB RAM) - RAM: process"
|
|
49
108
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
109
|
+
def _check_cgroups_v2(self):
|
|
110
|
+
try:
|
|
111
|
+
return os.path.exists('/sys/fs/cgroup/cpu.stat') and \
|
|
112
|
+
os.path.exists('/sys/fs/cgroup/memory.current')
|
|
113
|
+
except:
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
def _read_cgroup_cpu(self):
|
|
117
|
+
try:
|
|
118
|
+
with open('/sys/fs/cgroup/cpu.stat', 'r') as f:
|
|
119
|
+
for line in f:
|
|
120
|
+
if line.startswith('usage_usec'):
|
|
121
|
+
return int(line.split()[1])
|
|
122
|
+
except:
|
|
123
|
+
pass
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
def _read_cgroup_memory(self):
|
|
127
|
+
try:
|
|
128
|
+
with open('/sys/fs/cgroup/memory.current', 'r') as f:
|
|
129
|
+
current = int(f.read().strip())
|
|
130
|
+
with open('/sys/fs/cgroup/memory.max', 'r') as f:
|
|
131
|
+
max_mem = f.read().strip()
|
|
132
|
+
if max_mem == 'max':
|
|
133
|
+
max_mem = psutil.virtual_memory().total
|
|
134
|
+
else:
|
|
135
|
+
max_mem = int(max_mem)
|
|
136
|
+
return current, max_mem
|
|
137
|
+
except:
|
|
138
|
+
pass
|
|
139
|
+
return None, None
|
|
140
|
+
|
|
141
|
+
def get_cpu_percent(self):
|
|
142
|
+
"""Get CPU% with smoothing - shows container limits if in container, host if not"""
|
|
143
|
+
if self.use_cgroups:
|
|
144
|
+
current_time = time.time()
|
|
145
|
+
current_cpu = self._read_cgroup_cpu()
|
|
146
|
+
|
|
147
|
+
if current_cpu and self.last_cpu_time and self.last_check_time:
|
|
148
|
+
time_delta = current_time - self.last_check_time
|
|
149
|
+
cpu_delta = current_cpu - self.last_cpu_time
|
|
150
|
+
|
|
151
|
+
if time_delta > 0:
|
|
152
|
+
# Calculate based on container CPU quota
|
|
153
|
+
cpu_percent = (cpu_delta / (time_delta * 1_000_000)) * 100
|
|
154
|
+
|
|
155
|
+
# Clamp to container limits
|
|
156
|
+
cpu_percent = min(cpu_percent, 100 * self.cpu_count)
|
|
157
|
+
|
|
158
|
+
# Add to history for smoothing
|
|
159
|
+
self.cpu_history.append(cpu_percent)
|
|
160
|
+
if len(self.cpu_history) > self.cpu_history_max:
|
|
161
|
+
self.cpu_history.pop(0)
|
|
162
|
+
|
|
163
|
+
self.last_cpu_time = current_cpu
|
|
164
|
+
self.last_check_time = current_time
|
|
165
|
+
|
|
166
|
+
# Return smoothed average
|
|
167
|
+
return round(sum(self.cpu_history) / len(self.cpu_history), 1)
|
|
168
|
+
|
|
169
|
+
self.last_cpu_time = current_cpu
|
|
170
|
+
self.last_check_time = current_time
|
|
171
|
+
|
|
172
|
+
# Fallback: host resources with smoothing
|
|
173
|
+
try:
|
|
174
|
+
cpu = self.process.cpu_percent(interval=0.1)
|
|
175
|
+
if cpu == 0:
|
|
176
|
+
cpu = psutil.cpu_percent(interval=0.1)
|
|
177
|
+
|
|
178
|
+
self.cpu_history.append(cpu)
|
|
179
|
+
if len(self.cpu_history) > self.cpu_history_max:
|
|
180
|
+
self.cpu_history.pop(0)
|
|
181
|
+
|
|
182
|
+
return round(sum(self.cpu_history) / len(self.cpu_history), 1)
|
|
183
|
+
except:
|
|
184
|
+
return 0.0
|
|
185
|
+
|
|
186
|
+
def get_memory_info(self):
|
|
187
|
+
"""
|
|
188
|
+
Get memory usage - returns BOTH process and container/host
|
|
53
189
|
|
|
54
190
|
Returns:
|
|
55
|
-
|
|
191
|
+
tuple: (process_mb, process_pct, container_mb, container_pct)
|
|
192
|
+
If no container, container values = process values
|
|
56
193
|
"""
|
|
194
|
+
try:
|
|
195
|
+
# Get process memory (Python only)
|
|
196
|
+
mem = self.process.memory_info()
|
|
197
|
+
process_mb = mem.rss / (1024 * 1024)
|
|
198
|
+
|
|
199
|
+
if self.use_cgroups:
|
|
200
|
+
# Get container total and limit
|
|
201
|
+
current, max_mem = self._read_cgroup_memory()
|
|
202
|
+
if max_mem:
|
|
203
|
+
process_percent = (mem.rss / max_mem) * 100
|
|
204
|
+
|
|
205
|
+
if current:
|
|
206
|
+
container_mb = current / (1024 * 1024)
|
|
207
|
+
container_percent = (current / max_mem) * 100
|
|
208
|
+
return (
|
|
209
|
+
round(process_mb, 1),
|
|
210
|
+
round(process_percent, 1),
|
|
211
|
+
round(container_mb, 1),
|
|
212
|
+
round(container_percent, 1)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# No container data, return process only
|
|
216
|
+
return (
|
|
217
|
+
round(process_mb, 1),
|
|
218
|
+
round(process_percent, 1),
|
|
219
|
+
round(process_mb, 1),
|
|
220
|
+
round(process_percent, 1)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Host: calculate % of total RAM
|
|
224
|
+
total = psutil.virtual_memory().total
|
|
225
|
+
percent = (mem.rss / total) * 100
|
|
226
|
+
|
|
227
|
+
# On host, process = "container" (no container isolation)
|
|
228
|
+
return (
|
|
229
|
+
round(process_mb, 1),
|
|
230
|
+
round(percent, 1),
|
|
231
|
+
round(process_mb, 1),
|
|
232
|
+
round(percent, 1)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
except:
|
|
236
|
+
return 0.0, 0.0, 0.0, 0.0
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def create_progress_bar():
|
|
240
|
+
"""Create enhanced progress bar with ETA, CPU%, RAM"""
|
|
241
|
+
from IPython.display import display
|
|
242
|
+
import ipywidgets as widgets
|
|
243
|
+
|
|
244
|
+
progress_bar = widgets.FloatProgress(
|
|
245
|
+
value=0, min=0, max=100,
|
|
246
|
+
description='Progress:',
|
|
247
|
+
bar_style='info',
|
|
248
|
+
style={'bar_color': '#00ff00'},
|
|
249
|
+
layout=widgets.Layout(width='100%', height='30px')
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
status_label = widgets.HTML(
|
|
253
|
+
value="<b style='color:#0066cc'>Starting...</b>"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
display(widgets.VBox([progress_bar, status_label]))
|
|
257
|
+
|
|
258
|
+
monitor = ResourceMonitor()
|
|
259
|
+
start_time = time.time()
|
|
260
|
+
|
|
261
|
+
return progress_bar, status_label, monitor, start_time
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def update_progress(progress_bar, status_label, monitor, current, total, start_time, message="Processing"):
|
|
265
|
+
"""
|
|
266
|
+
Update progress bar with ETA, CPU%, RAM (shows BOTH process and container)
|
|
267
|
+
"""
|
|
268
|
+
progress = (current / total) * 100
|
|
269
|
+
progress_bar.value = progress
|
|
270
|
+
|
|
271
|
+
elapsed = time.time() - start_time
|
|
272
|
+
if current > 0:
|
|
273
|
+
eta_seconds = (elapsed / current) * (total - current)
|
|
274
|
+
eta_str = format_time(eta_seconds)
|
|
275
|
+
else:
|
|
276
|
+
eta_str = "calculating..."
|
|
277
|
+
|
|
278
|
+
cpu = monitor.get_cpu_percent()
|
|
279
|
+
process_mb, process_pct, container_mb, container_pct = monitor.get_memory_info()
|
|
280
|
+
|
|
281
|
+
# Build RAM display - show both if different, otherwise just one
|
|
282
|
+
if abs(container_mb - process_mb) > 10: # Significant difference (>10MB)
|
|
283
|
+
ram_display = (
|
|
284
|
+
f"RAM: <span style='color:#4CAF50'>{process_mb}MB ({process_pct}%)</span> Python | "
|
|
285
|
+
f"<span style='color:#2196F3'>{container_mb}MB ({container_pct}%)</span> Container"
|
|
286
|
+
)
|
|
287
|
+
else:
|
|
288
|
+
# Same values (on host or small difference)
|
|
289
|
+
ram_display = f"RAM: {process_mb}MB ({process_pct}%)"
|
|
290
|
+
|
|
291
|
+
# Context info
|
|
292
|
+
if monitor.use_cgroups:
|
|
293
|
+
context_info = f"Container: {monitor.cpu_count:.1f} cores"
|
|
294
|
+
current, max_mem = monitor._read_cgroup_memory()
|
|
295
|
+
if max_mem:
|
|
296
|
+
context_info += f", {max_mem / (1024**3):.1f}GB limit"
|
|
297
|
+
else:
|
|
298
|
+
context_info = f"Host: {monitor.cpu_count} cores"
|
|
299
|
+
|
|
300
|
+
status_label.value = (
|
|
301
|
+
f"<b style='color:#0066cc'>{message} ({current}/{total})</b><br>"
|
|
302
|
+
f"<span style='color:#666'>ETA: {eta_str} | CPU: {cpu}% | {ram_display}</span><br>"
|
|
303
|
+
f"<span style='color:#999;font-size:10px'>{context_info}</span>"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def format_time(seconds):
|
|
308
|
+
"""Format seconds to human readable time"""
|
|
309
|
+
if seconds < 60:
|
|
310
|
+
return f"{int(seconds)}s"
|
|
311
|
+
elif seconds < 3600:
|
|
312
|
+
return f"{int(seconds // 60)}m {int(seconds % 60)}s"
|
|
313
|
+
else:
|
|
314
|
+
hours = int(seconds // 3600)
|
|
315
|
+
minutes = int((seconds % 3600) // 60)
|
|
316
|
+
return f"{hours}h {minutes}m"
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# ============================================================
|
|
320
|
+
# API HELPER - AUTOMATIC NORMALIZATION
|
|
321
|
+
# ============================================================
|
|
322
|
+
class APIHelper:
|
|
323
|
+
"""Normalizes API responses to consistent format"""
|
|
324
|
+
|
|
325
|
+
@staticmethod
|
|
326
|
+
def normalize_response(response, debug=False):
|
|
57
327
|
if response is None:
|
|
58
328
|
if debug:
|
|
59
329
|
print("[APIHelper] Response is None")
|
|
60
330
|
return None
|
|
61
331
|
|
|
62
|
-
# Case 1: Already a dict with 'data' key
|
|
63
332
|
if isinstance(response, dict):
|
|
64
333
|
if 'data' in response:
|
|
65
334
|
if debug:
|
|
66
|
-
print(f"[APIHelper] Dict response
|
|
335
|
+
print(f"[APIHelper] Dict response: {len(response['data'])} records")
|
|
67
336
|
return response
|
|
68
337
|
else:
|
|
69
338
|
if debug:
|
|
70
|
-
print("[APIHelper] Dict
|
|
339
|
+
print("[APIHelper] Dict without 'data' key")
|
|
71
340
|
return None
|
|
72
341
|
|
|
73
|
-
# Case 2: DataFrame - convert to dict
|
|
74
342
|
if isinstance(response, pd.DataFrame):
|
|
75
343
|
if response.empty:
|
|
76
344
|
if debug:
|
|
@@ -79,73 +347,33 @@ class APIHelper:
|
|
|
79
347
|
|
|
80
348
|
records = response.to_dict('records')
|
|
81
349
|
if debug:
|
|
82
|
-
print(f"[APIHelper]
|
|
350
|
+
print(f"[APIHelper] DataFrame converted: {len(records)} records")
|
|
83
351
|
return {'data': records, 'status': 'success'}
|
|
84
352
|
|
|
85
|
-
# Case 3: Unknown type
|
|
86
353
|
if debug:
|
|
87
|
-
print(f"[APIHelper] Unexpected
|
|
354
|
+
print(f"[APIHelper] Unexpected type: {type(response)}")
|
|
88
355
|
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
356
|
|
|
119
357
|
|
|
120
|
-
# ============================================================
|
|
121
|
-
# GLOBAL API MANAGER (Updated)
|
|
122
|
-
# ============================================================
|
|
123
358
|
class APIManager:
|
|
124
|
-
"""
|
|
125
|
-
Centralized API key management for IVolatility API
|
|
126
|
-
Now includes response normalization
|
|
127
|
-
"""
|
|
359
|
+
"""Centralized API key management"""
|
|
128
360
|
_api_key = None
|
|
129
361
|
_methods = {}
|
|
130
362
|
|
|
131
363
|
@classmethod
|
|
132
364
|
def initialize(cls, api_key):
|
|
133
|
-
"""Set API key globally - call this once at startup"""
|
|
134
365
|
if not api_key:
|
|
135
366
|
raise ValueError("API key cannot be empty")
|
|
136
367
|
cls._api_key = api_key
|
|
137
368
|
ivol.setLoginParams(apiKey=api_key)
|
|
138
|
-
print(f"[API] Initialized
|
|
369
|
+
print(f"[API] Initialized: {api_key[:10]}...{api_key[-5:]}")
|
|
139
370
|
|
|
140
371
|
@classmethod
|
|
141
372
|
def get_method(cls, endpoint):
|
|
142
|
-
"""Get API method with automatic key injection"""
|
|
143
373
|
if cls._api_key is None:
|
|
144
374
|
api_key = os.getenv("API_KEY")
|
|
145
375
|
if not api_key:
|
|
146
|
-
raise ValueError(
|
|
147
|
-
"API key not initialized. Call init_api(key) first or set API_KEY environment variable"
|
|
148
|
-
)
|
|
376
|
+
raise ValueError("API key not set. Call init_api(key) first")
|
|
149
377
|
cls.initialize(api_key)
|
|
150
378
|
|
|
151
379
|
if endpoint not in cls._methods:
|
|
@@ -153,61 +381,39 @@ class APIManager:
|
|
|
153
381
|
cls._methods[endpoint] = ivol.setMethod(endpoint)
|
|
154
382
|
|
|
155
383
|
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
384
|
|
|
162
385
|
|
|
163
|
-
# Public API functions (Updated)
|
|
164
386
|
def init_api(api_key=None):
|
|
165
|
-
"""Initialize IVolatility API
|
|
387
|
+
"""Initialize IVolatility API"""
|
|
166
388
|
if api_key is None:
|
|
167
389
|
api_key = os.getenv("API_KEY")
|
|
168
390
|
APIManager.initialize(api_key)
|
|
169
391
|
|
|
170
392
|
|
|
171
|
-
def get_api_method(endpoint):
|
|
172
|
-
"""Get API method for specified endpoint"""
|
|
173
|
-
return APIManager.get_method(endpoint)
|
|
174
|
-
|
|
175
|
-
|
|
176
393
|
def api_call(endpoint, debug=False, **kwargs):
|
|
177
|
-
"""
|
|
178
|
-
Make API call with automatic response normalization
|
|
179
|
-
|
|
180
|
-
Args:
|
|
181
|
-
endpoint: API endpoint path
|
|
182
|
-
debug: Enable debug output
|
|
183
|
-
**kwargs: API parameters
|
|
184
|
-
|
|
185
|
-
Returns:
|
|
186
|
-
dict with 'data' key (normalized format) or None if error
|
|
187
|
-
|
|
188
|
-
Example:
|
|
189
|
-
# Old way (manual handling):
|
|
190
|
-
method = get_api_method('/equities/eod/stock-prices')
|
|
191
|
-
response = method(symbol='AAPL', from_='2024-01-01')
|
|
192
|
-
if isinstance(response, pd.DataFrame):
|
|
193
|
-
df = response
|
|
194
|
-
elif isinstance(response, dict):
|
|
195
|
-
df = pd.DataFrame(response['data'])
|
|
196
|
-
|
|
197
|
-
# New way (automatic):
|
|
198
|
-
data = api_call('/equities/eod/stock-prices', symbol='AAPL', from_='2024-01-01')
|
|
199
|
-
if data:
|
|
200
|
-
df = pd.DataFrame(data['data'])
|
|
201
|
-
"""
|
|
394
|
+
"""Make API call with automatic response normalization"""
|
|
202
395
|
try:
|
|
203
|
-
|
|
396
|
+
if debug and APIManager._api_key:
|
|
397
|
+
base_url = "https://restapi.ivolatility.com"
|
|
398
|
+
url_params = {}
|
|
399
|
+
for key, value in kwargs.items():
|
|
400
|
+
clean_key = key.rstrip('_') if key.endswith('_') else key
|
|
401
|
+
url_params[clean_key] = value
|
|
402
|
+
|
|
403
|
+
params_str = "&".join([f"{k}={v}" for k, v in url_params.items()])
|
|
404
|
+
full_url = f"{base_url}{endpoint}?apiKey={APIManager._api_key}&{params_str}"
|
|
405
|
+
print(f"\n[API] Full URL:")
|
|
406
|
+
print(f"[API] {full_url}\n")
|
|
407
|
+
|
|
408
|
+
method = APIManager.get_method(endpoint)
|
|
204
409
|
response = method(**kwargs)
|
|
205
410
|
|
|
206
411
|
normalized = APIHelper.normalize_response(response, debug=debug)
|
|
207
412
|
|
|
208
413
|
if normalized is None and debug:
|
|
209
|
-
print(f"[api_call] Failed to get
|
|
210
|
-
print(f"[api_call]
|
|
414
|
+
print(f"[api_call] Failed to get data")
|
|
415
|
+
print(f"[api_call] Endpoint: {endpoint}")
|
|
416
|
+
print(f"[api_call] Params: {kwargs}")
|
|
211
417
|
|
|
212
418
|
return normalized
|
|
213
419
|
|
|
@@ -215,25 +421,19 @@ def api_call(endpoint, debug=False, **kwargs):
|
|
|
215
421
|
if debug:
|
|
216
422
|
print(f"[api_call] Exception: {e}")
|
|
217
423
|
print(f"[api_call] Endpoint: {endpoint}")
|
|
218
|
-
print(f"[api_call]
|
|
424
|
+
print(f"[api_call] Params: {kwargs}")
|
|
219
425
|
return None
|
|
220
426
|
|
|
221
427
|
|
|
222
428
|
# ============================================================
|
|
223
|
-
# BACKTEST RESULTS
|
|
429
|
+
# BACKTEST RESULTS
|
|
224
430
|
# ============================================================
|
|
225
431
|
class BacktestResults:
|
|
226
432
|
"""Universal container for backtest results"""
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
initial_capital,
|
|
232
|
-
config,
|
|
233
|
-
benchmark_prices=None,
|
|
234
|
-
benchmark_symbol='SPY',
|
|
235
|
-
daily_returns=None,
|
|
236
|
-
debug_info=None):
|
|
433
|
+
|
|
434
|
+
def __init__(self, equity_curve, equity_dates, trades, initial_capital,
|
|
435
|
+
config, benchmark_prices=None, benchmark_symbol='SPY',
|
|
436
|
+
daily_returns=None, debug_info=None):
|
|
237
437
|
|
|
238
438
|
self.equity_curve = equity_curve
|
|
239
439
|
self.equity_dates = equity_dates
|
|
@@ -264,23 +464,23 @@ class BacktestResults:
|
|
|
264
464
|
|
|
265
465
|
|
|
266
466
|
# ============================================================
|
|
267
|
-
# BACKTEST ANALYZER (
|
|
467
|
+
# BACKTEST ANALYZER (30+ METRICS)
|
|
268
468
|
# ============================================================
|
|
269
469
|
class BacktestAnalyzer:
|
|
270
|
-
"""
|
|
470
|
+
"""Calculate all metrics from BacktestResults"""
|
|
471
|
+
|
|
271
472
|
def __init__(self, results):
|
|
272
473
|
self.results = results
|
|
273
474
|
self.metrics = {}
|
|
274
|
-
|
|
475
|
+
|
|
275
476
|
def calculate_all_metrics(self):
|
|
276
|
-
"""Calculate all available metrics"""
|
|
277
477
|
r = self.results
|
|
278
478
|
|
|
279
|
-
#
|
|
479
|
+
# Profitability
|
|
280
480
|
self.metrics['total_pnl'] = r.final_capital - r.initial_capital
|
|
281
481
|
self.metrics['total_return'] = (self.metrics['total_pnl'] / r.initial_capital) * 100
|
|
282
482
|
|
|
283
|
-
# CAGR
|
|
483
|
+
# CAGR
|
|
284
484
|
if len(r.equity_dates) > 0:
|
|
285
485
|
start_date = min(r.equity_dates)
|
|
286
486
|
end_date = max(r.equity_dates)
|
|
@@ -291,7 +491,6 @@ class BacktestAnalyzer:
|
|
|
291
491
|
self.metrics['show_cagr'] = False
|
|
292
492
|
else:
|
|
293
493
|
years = days_diff / 365.25
|
|
294
|
-
|
|
295
494
|
if years >= 1.0:
|
|
296
495
|
self.metrics['cagr'] = ((r.final_capital / r.initial_capital) ** (1/years) - 1) * 100
|
|
297
496
|
self.metrics['show_cagr'] = True
|
|
@@ -306,12 +505,7 @@ class BacktestAnalyzer:
|
|
|
306
505
|
self.metrics['sharpe'] = self._sharpe_ratio(r.daily_returns)
|
|
307
506
|
self.metrics['sortino'] = self._sortino_ratio(r.daily_returns)
|
|
308
507
|
self.metrics['max_drawdown'] = r.max_drawdown
|
|
309
|
-
|
|
310
|
-
if len(r.daily_returns) > 0:
|
|
311
|
-
self.metrics['volatility'] = np.std(r.daily_returns) * np.sqrt(252) * 100
|
|
312
|
-
else:
|
|
313
|
-
self.metrics['volatility'] = 0
|
|
314
|
-
|
|
508
|
+
self.metrics['volatility'] = np.std(r.daily_returns) * np.sqrt(252) * 100 if len(r.daily_returns) > 0 else 0
|
|
315
509
|
self.metrics['calmar'] = abs(self.metrics['total_return'] / r.max_drawdown) if r.max_drawdown > 0 else 0
|
|
316
510
|
self.metrics['omega'] = self._omega_ratio(r.daily_returns)
|
|
317
511
|
self.metrics['ulcer'] = self._ulcer_index(r.equity_curve)
|
|
@@ -335,37 +529,9 @@ class BacktestAnalyzer:
|
|
|
335
529
|
|
|
336
530
|
# Trading stats
|
|
337
531
|
if len(r.trades) > 0:
|
|
338
|
-
|
|
339
|
-
winning = trades_df[trades_df['pnl'] > 0]
|
|
340
|
-
losing = trades_df[trades_df['pnl'] <= 0]
|
|
341
|
-
|
|
342
|
-
self.metrics['total_trades'] = len(trades_df)
|
|
343
|
-
self.metrics['winning_trades'] = len(winning)
|
|
344
|
-
self.metrics['losing_trades'] = len(losing)
|
|
345
|
-
self.metrics['win_rate'] = (len(winning) / len(trades_df)) * 100 if len(trades_df) > 0 else 0
|
|
346
|
-
|
|
347
|
-
wins_sum = winning['pnl'].sum() if len(winning) > 0 else 0
|
|
348
|
-
losses_sum = abs(losing['pnl'].sum()) if len(losing) > 0 else 0
|
|
349
|
-
self.metrics['profit_factor'] = wins_sum / losses_sum if losses_sum > 0 else float('inf')
|
|
350
|
-
|
|
351
|
-
self.metrics['avg_win'] = winning['pnl'].mean() if len(winning) > 0 else 0
|
|
352
|
-
self.metrics['avg_loss'] = losing['pnl'].mean() if len(losing) > 0 else 0
|
|
353
|
-
self.metrics['best_trade'] = trades_df['pnl'].max()
|
|
354
|
-
self.metrics['worst_trade'] = trades_df['pnl'].min()
|
|
355
|
-
|
|
356
|
-
if len(winning) > 0 and len(losing) > 0:
|
|
357
|
-
self.metrics['avg_win_loss_ratio'] = abs(self.metrics['avg_win'] / self.metrics['avg_loss'])
|
|
358
|
-
else:
|
|
359
|
-
self.metrics['avg_win_loss_ratio'] = 0
|
|
360
|
-
|
|
361
|
-
self.metrics['max_win_streak'], self.metrics['max_loss_streak'] = self._win_loss_streaks(r.trades)
|
|
532
|
+
self._calculate_trading_stats(r.trades)
|
|
362
533
|
else:
|
|
363
|
-
self.
|
|
364
|
-
'total_trades': 0, 'winning_trades': 0, 'losing_trades': 0,
|
|
365
|
-
'win_rate': 0, 'profit_factor': 0, 'avg_win': 0, 'avg_loss': 0,
|
|
366
|
-
'best_trade': 0, 'worst_trade': 0, 'avg_win_loss_ratio': 0,
|
|
367
|
-
'max_win_streak': 0, 'max_loss_streak': 0
|
|
368
|
-
})
|
|
534
|
+
self._set_empty_trading_stats()
|
|
369
535
|
|
|
370
536
|
# Efficiency
|
|
371
537
|
running_max = np.maximum.accumulate(r.equity_curve)
|
|
@@ -381,6 +547,40 @@ class BacktestAnalyzer:
|
|
|
381
547
|
|
|
382
548
|
return self.metrics
|
|
383
549
|
|
|
550
|
+
def _calculate_trading_stats(self, trades):
|
|
551
|
+
trades_df = pd.DataFrame(trades)
|
|
552
|
+
winning = trades_df[trades_df['pnl'] > 0]
|
|
553
|
+
losing = trades_df[trades_df['pnl'] <= 0]
|
|
554
|
+
|
|
555
|
+
self.metrics['total_trades'] = len(trades_df)
|
|
556
|
+
self.metrics['winning_trades'] = len(winning)
|
|
557
|
+
self.metrics['losing_trades'] = len(losing)
|
|
558
|
+
self.metrics['win_rate'] = (len(winning) / len(trades_df)) * 100 if len(trades_df) > 0 else 0
|
|
559
|
+
|
|
560
|
+
wins_sum = winning['pnl'].sum() if len(winning) > 0 else 0
|
|
561
|
+
losses_sum = abs(losing['pnl'].sum()) if len(losing) > 0 else 0
|
|
562
|
+
self.metrics['profit_factor'] = wins_sum / losses_sum if losses_sum > 0 else float('inf')
|
|
563
|
+
|
|
564
|
+
self.metrics['avg_win'] = winning['pnl'].mean() if len(winning) > 0 else 0
|
|
565
|
+
self.metrics['avg_loss'] = losing['pnl'].mean() if len(losing) > 0 else 0
|
|
566
|
+
self.metrics['best_trade'] = trades_df['pnl'].max()
|
|
567
|
+
self.metrics['worst_trade'] = trades_df['pnl'].min()
|
|
568
|
+
|
|
569
|
+
if len(winning) > 0 and len(losing) > 0:
|
|
570
|
+
self.metrics['avg_win_loss_ratio'] = abs(self.metrics['avg_win'] / self.metrics['avg_loss'])
|
|
571
|
+
else:
|
|
572
|
+
self.metrics['avg_win_loss_ratio'] = 0
|
|
573
|
+
|
|
574
|
+
self.metrics['max_win_streak'], self.metrics['max_loss_streak'] = self._win_loss_streaks(trades)
|
|
575
|
+
|
|
576
|
+
def _set_empty_trading_stats(self):
|
|
577
|
+
self.metrics.update({
|
|
578
|
+
'total_trades': 0, 'winning_trades': 0, 'losing_trades': 0,
|
|
579
|
+
'win_rate': 0, 'profit_factor': 0, 'avg_win': 0, 'avg_loss': 0,
|
|
580
|
+
'best_trade': 0, 'worst_trade': 0, 'avg_win_loss_ratio': 0,
|
|
581
|
+
'max_win_streak': 0, 'max_loss_streak': 0
|
|
582
|
+
})
|
|
583
|
+
|
|
384
584
|
def _sharpe_ratio(self, returns):
|
|
385
585
|
if len(returns) < 2:
|
|
386
586
|
return 0
|
|
@@ -532,11 +732,10 @@ class BacktestAnalyzer:
|
|
|
532
732
|
|
|
533
733
|
|
|
534
734
|
# ============================================================
|
|
535
|
-
# RESULTS REPORTER
|
|
536
|
-
# (All unchanged - same as before)
|
|
735
|
+
# RESULTS REPORTER
|
|
537
736
|
# ============================================================
|
|
538
737
|
class ResultsReporter:
|
|
539
|
-
"""
|
|
738
|
+
"""Print comprehensive metrics report"""
|
|
540
739
|
|
|
541
740
|
@staticmethod
|
|
542
741
|
def print_full_report(analyzer):
|
|
@@ -554,7 +753,7 @@ class ResultsReporter:
|
|
|
554
753
|
for debug_msg in r.debug_info[:10]:
|
|
555
754
|
print(debug_msg)
|
|
556
755
|
if len(r.debug_info) > 10:
|
|
557
|
-
print(f"... and {len(r.debug_info) - 10} more
|
|
756
|
+
print(f"... and {len(r.debug_info) - 10} more messages")
|
|
558
757
|
print()
|
|
559
758
|
|
|
560
759
|
print("PROFITABILITY METRICS")
|
|
@@ -602,12 +801,12 @@ class ResultsReporter:
|
|
|
602
801
|
|
|
603
802
|
if abs(m['total_return']) > 200 or m['volatility'] > 150:
|
|
604
803
|
print()
|
|
605
|
-
print("UNREALISTIC RESULTS DETECTED:")
|
|
804
|
+
print("⚠️ UNREALISTIC RESULTS DETECTED:")
|
|
606
805
|
if abs(m['total_return']) > 200:
|
|
607
|
-
print(f" Total return {m['total_return']:.1f}% is extremely high")
|
|
806
|
+
print(f" • Total return {m['total_return']:.1f}% is extremely high")
|
|
608
807
|
if m['volatility'] > 150:
|
|
609
|
-
print(f" Volatility {m['volatility']:.1f}% is higher than leveraged ETFs")
|
|
610
|
-
print(" Review configuration before trusting results")
|
|
808
|
+
print(f" • Volatility {m['volatility']:.1f}% is higher than leveraged ETFs")
|
|
809
|
+
print(" → Review configuration before trusting results")
|
|
611
810
|
|
|
612
811
|
print()
|
|
613
812
|
|
|
@@ -639,13 +838,15 @@ class ResultsReporter:
|
|
|
639
838
|
print("="*80)
|
|
640
839
|
|
|
641
840
|
|
|
841
|
+
# ============================================================
|
|
842
|
+
# CHART GENERATOR
|
|
843
|
+
# ============================================================
|
|
642
844
|
class ChartGenerator:
|
|
643
|
-
"""
|
|
845
|
+
"""Generate 6 professional charts"""
|
|
644
846
|
|
|
645
847
|
@staticmethod
|
|
646
848
|
def create_all_charts(analyzer, filename='backtest_results.png'):
|
|
647
849
|
r = analyzer.results
|
|
648
|
-
m = analyzer.metrics
|
|
649
850
|
|
|
650
851
|
if len(r.trades) == 0:
|
|
651
852
|
print("No trades to visualize")
|
|
@@ -653,13 +854,12 @@ class ChartGenerator:
|
|
|
653
854
|
|
|
654
855
|
trades_df = pd.DataFrame(r.trades)
|
|
655
856
|
fig, axes = plt.subplots(3, 2, figsize=(18, 14))
|
|
656
|
-
fig.suptitle('Backtest Results
|
|
657
|
-
fontsize=16, fontweight='bold', y=0.995)
|
|
857
|
+
fig.suptitle('Backtest Results', fontsize=16, fontweight='bold', y=0.995)
|
|
658
858
|
|
|
659
859
|
dates = pd.to_datetime(r.equity_dates)
|
|
660
860
|
equity_array = np.array(r.equity_curve)
|
|
661
861
|
|
|
662
|
-
# Equity Curve
|
|
862
|
+
# 1. Equity Curve
|
|
663
863
|
ax1 = axes[0, 0]
|
|
664
864
|
ax1.plot(dates, equity_array, linewidth=2.5, color='#2196F3')
|
|
665
865
|
ax1.axhline(y=r.initial_capital, color='gray', linestyle='--', alpha=0.7)
|
|
@@ -669,78 +869,66 @@ class ChartGenerator:
|
|
|
669
869
|
ax1.fill_between(dates, r.initial_capital, equity_array,
|
|
670
870
|
where=(equity_array < r.initial_capital),
|
|
671
871
|
alpha=0.3, color='red', interpolate=True)
|
|
672
|
-
ax1.set_title('
|
|
872
|
+
ax1.set_title('Equity Curve', fontsize=12, fontweight='bold')
|
|
673
873
|
ax1.set_ylabel('Equity ($)')
|
|
674
874
|
ax1.grid(True, alpha=0.3)
|
|
675
|
-
ax1.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'${x/1000:.0f}K'))
|
|
676
875
|
|
|
677
|
-
# Drawdown
|
|
876
|
+
# 2. Drawdown
|
|
678
877
|
ax2 = axes[0, 1]
|
|
679
878
|
running_max = np.maximum.accumulate(equity_array)
|
|
680
879
|
drawdown = (equity_array - running_max) / running_max * 100
|
|
681
880
|
ax2.fill_between(dates, 0, drawdown, alpha=0.6, color='#f44336')
|
|
682
881
|
ax2.plot(dates, drawdown, color='#d32f2f', linewidth=2)
|
|
683
|
-
|
|
684
|
-
ax2.scatter(dates[max_dd_idx], drawdown[max_dd_idx], color='darkred', s=100, zorder=5, marker='v')
|
|
685
|
-
ax2.set_title('Drawdown Over Time', fontsize=12, fontweight='bold')
|
|
882
|
+
ax2.set_title('Drawdown', fontsize=12, fontweight='bold')
|
|
686
883
|
ax2.set_ylabel('Drawdown (%)')
|
|
687
884
|
ax2.grid(True, alpha=0.3)
|
|
688
885
|
|
|
689
|
-
# P&L Distribution
|
|
886
|
+
# 3. P&L Distribution
|
|
690
887
|
ax3 = axes[1, 0]
|
|
691
888
|
pnl_values = trades_df['pnl'].values
|
|
692
889
|
ax3.hist(pnl_values, bins=40, color='#4CAF50', alpha=0.7, edgecolor='black')
|
|
693
890
|
ax3.axvline(x=0, color='red', linestyle='--', linewidth=2)
|
|
694
|
-
ax3.
|
|
695
|
-
ax3.set_title('Trade P&L Distribution', fontsize=12, fontweight='bold')
|
|
891
|
+
ax3.set_title('P&L Distribution', fontsize=12, fontweight='bold')
|
|
696
892
|
ax3.set_xlabel('P&L ($)')
|
|
697
|
-
ax3.set_ylabel('Frequency')
|
|
698
893
|
ax3.grid(True, alpha=0.3, axis='y')
|
|
699
894
|
|
|
700
|
-
# Signal Performance
|
|
895
|
+
# 4. Signal Performance
|
|
701
896
|
ax4 = axes[1, 1]
|
|
702
897
|
if 'signal' in trades_df.columns:
|
|
703
898
|
signal_pnl = trades_df.groupby('signal')['pnl'].sum()
|
|
704
899
|
colors = ['#4CAF50' if x > 0 else '#f44336' for x in signal_pnl.values]
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
height = bar.get_height()
|
|
708
|
-
ax4.text(bar.get_x() + bar.get_width()/2., height,
|
|
709
|
-
f'${height:,.0f}', ha='center', va='bottom' if height > 0 else 'top', fontweight='bold')
|
|
710
|
-
ax4.set_title('P&L by Signal Type', fontsize=12, fontweight='bold')
|
|
900
|
+
ax4.bar(signal_pnl.index, signal_pnl.values, color=colors, alpha=0.7, edgecolor='black')
|
|
901
|
+
ax4.set_title('P&L by Signal', fontsize=12, fontweight='bold')
|
|
711
902
|
else:
|
|
712
903
|
ax4.text(0.5, 0.5, 'No signal data', ha='center', va='center', transform=ax4.transAxes)
|
|
713
|
-
ax4.
|
|
714
|
-
ax4.axhline(y=0, color='black', linestyle='-', linewidth=1)
|
|
904
|
+
ax4.axhline(y=0, color='black', linewidth=1)
|
|
715
905
|
ax4.grid(True, alpha=0.3, axis='y')
|
|
716
906
|
|
|
717
|
-
# Monthly Returns
|
|
907
|
+
# 5. Monthly Returns
|
|
718
908
|
ax5 = axes[2, 0]
|
|
719
909
|
trades_df['exit_date'] = pd.to_datetime(trades_df['exit_date'])
|
|
720
910
|
trades_df['month'] = trades_df['exit_date'].dt.to_period('M')
|
|
721
911
|
monthly_pnl = trades_df.groupby('month')['pnl'].sum()
|
|
722
|
-
|
|
723
|
-
ax5.bar(range(len(monthly_pnl)), monthly_pnl.values, color=
|
|
912
|
+
colors = ['#4CAF50' if x > 0 else '#f44336' for x in monthly_pnl.values]
|
|
913
|
+
ax5.bar(range(len(monthly_pnl)), monthly_pnl.values, color=colors, alpha=0.7, edgecolor='black')
|
|
724
914
|
ax5.set_title('Monthly P&L', fontsize=12, fontweight='bold')
|
|
725
|
-
ax5.set_ylabel('P&L ($)')
|
|
726
915
|
ax5.set_xticks(range(len(monthly_pnl)))
|
|
727
916
|
ax5.set_xticklabels([str(m) for m in monthly_pnl.index], rotation=45, ha='right')
|
|
728
|
-
ax5.axhline(y=0, color='black',
|
|
917
|
+
ax5.axhline(y=0, color='black', linewidth=1)
|
|
729
918
|
ax5.grid(True, alpha=0.3, axis='y')
|
|
730
919
|
|
|
731
|
-
# Top Symbols
|
|
920
|
+
# 6. Top Symbols
|
|
732
921
|
ax6 = axes[2, 1]
|
|
733
922
|
if 'symbol' in trades_df.columns:
|
|
734
923
|
symbol_pnl = trades_df.groupby('symbol')['pnl'].sum().sort_values(ascending=True).tail(10)
|
|
735
|
-
|
|
736
|
-
ax6.barh(range(len(symbol_pnl)), symbol_pnl.values, color=
|
|
924
|
+
colors = ['#4CAF50' if x > 0 else '#f44336' for x in symbol_pnl.values]
|
|
925
|
+
ax6.barh(range(len(symbol_pnl)), symbol_pnl.values, color=colors, alpha=0.7, edgecolor='black')
|
|
737
926
|
ax6.set_yticks(range(len(symbol_pnl)))
|
|
738
927
|
ax6.set_yticklabels(symbol_pnl.index, fontsize=9)
|
|
739
|
-
ax6.set_title('Top
|
|
928
|
+
ax6.set_title('Top Symbols', fontsize=12, fontweight='bold')
|
|
740
929
|
else:
|
|
741
930
|
ax6.text(0.5, 0.5, 'No symbol data', ha='center', va='center', transform=ax6.transAxes)
|
|
742
|
-
ax6.
|
|
743
|
-
ax6.axvline(x=0, color='black', linestyle='-', linewidth=1)
|
|
931
|
+
ax6.axvline(x=0, color='black', linewidth=1)
|
|
744
932
|
ax6.grid(True, alpha=0.3, axis='x')
|
|
745
933
|
|
|
746
934
|
plt.tight_layout()
|
|
@@ -750,8 +938,11 @@ class ChartGenerator:
|
|
|
750
938
|
print(f"Chart saved: {filename}")
|
|
751
939
|
|
|
752
940
|
|
|
941
|
+
# ============================================================
|
|
942
|
+
# RESULTS EXPORTER
|
|
943
|
+
# ============================================================
|
|
753
944
|
class ResultsExporter:
|
|
754
|
-
"""
|
|
945
|
+
"""Export results to CSV"""
|
|
755
946
|
|
|
756
947
|
@staticmethod
|
|
757
948
|
def export_all(analyzer, prefix='backtest'):
|
|
@@ -766,41 +957,35 @@ class ResultsExporter:
|
|
|
766
957
|
trades_df['entry_date'] = pd.to_datetime(trades_df['entry_date']).dt.strftime('%Y-%m-%d')
|
|
767
958
|
trades_df['exit_date'] = pd.to_datetime(trades_df['exit_date']).dt.strftime('%Y-%m-%d')
|
|
768
959
|
trades_df.to_csv(f'{prefix}_trades.csv', index=False)
|
|
769
|
-
print(f"
|
|
960
|
+
print(f"Exported: {prefix}_trades.csv")
|
|
770
961
|
|
|
771
962
|
equity_df = pd.DataFrame({
|
|
772
963
|
'date': pd.to_datetime(r.equity_dates).strftime('%Y-%m-%d'),
|
|
773
964
|
'equity': r.equity_curve
|
|
774
965
|
})
|
|
775
966
|
equity_df.to_csv(f'{prefix}_equity.csv', index=False)
|
|
776
|
-
print(f"
|
|
967
|
+
print(f"Exported: {prefix}_equity.csv")
|
|
777
968
|
|
|
778
969
|
with open(f'{prefix}_summary.txt', 'w') as f:
|
|
779
970
|
f.write("BACKTEST SUMMARY\n")
|
|
780
971
|
f.write("="*70 + "\n\n")
|
|
781
972
|
f.write(f"Strategy: {r.config.get('strategy_name', 'Unknown')}\n")
|
|
782
|
-
f.write(f"Period: {r.config.get('start_date'
|
|
783
|
-
|
|
973
|
+
f.write(f"Period: {r.config.get('start_date')} to {r.config.get('end_date')}\n\n")
|
|
784
974
|
f.write("PERFORMANCE\n")
|
|
785
975
|
f.write("-"*70 + "\n")
|
|
786
|
-
f.write(f"Initial Capital: ${r.initial_capital:,.2f}\n")
|
|
787
|
-
f.write(f"Final Equity: ${r.final_capital:,.2f}\n")
|
|
788
976
|
f.write(f"Total Return: {m['total_return']:.2f}%\n")
|
|
789
|
-
f.write(f"Sharpe
|
|
790
|
-
f.write(f"Max
|
|
791
|
-
f.write(f"
|
|
792
|
-
f.write(f"Total Trades: {m['total_trades']}\n")
|
|
977
|
+
f.write(f"Sharpe: {m['sharpe']:.2f}\n")
|
|
978
|
+
f.write(f"Max DD: {m['max_drawdown']:.2f}%\n")
|
|
979
|
+
f.write(f"Trades: {m['total_trades']}\n")
|
|
793
980
|
|
|
794
|
-
print(f"
|
|
981
|
+
print(f"Exported: {prefix}_summary.txt")
|
|
795
982
|
|
|
796
983
|
|
|
797
984
|
# ============================================================
|
|
798
|
-
#
|
|
985
|
+
# RUN BACKTEST
|
|
799
986
|
# ============================================================
|
|
800
|
-
def run_backtest(strategy_function, config,
|
|
801
|
-
|
|
802
|
-
create_charts=True,
|
|
803
|
-
export_results=True,
|
|
987
|
+
def run_backtest(strategy_function, config, print_report=True,
|
|
988
|
+
create_charts=True, export_results=True,
|
|
804
989
|
chart_filename='backtest_results.png',
|
|
805
990
|
export_prefix='backtest'):
|
|
806
991
|
"""Run complete backtest with one command"""
|
|
@@ -809,7 +994,7 @@ def run_backtest(strategy_function, config,
|
|
|
809
994
|
print(" "*25 + "STARTING BACKTEST")
|
|
810
995
|
print("="*80)
|
|
811
996
|
print(f"Strategy: {config.get('strategy_name', 'Unknown')}")
|
|
812
|
-
print(f"Period: {config.get('start_date'
|
|
997
|
+
print(f"Period: {config.get('start_date')} to {config.get('end_date')}")
|
|
813
998
|
print(f"Capital: ${config.get('initial_capital', 0):,.0f}")
|
|
814
999
|
print("="*80 + "\n")
|
|
815
1000
|
|
|
@@ -827,41 +1012,22 @@ def run_backtest(strategy_function, config,
|
|
|
827
1012
|
print(f"\n[*] Creating charts: {chart_filename}")
|
|
828
1013
|
try:
|
|
829
1014
|
ChartGenerator.create_all_charts(analyzer, chart_filename)
|
|
830
|
-
print(f"[OK] Charts saved: {chart_filename}")
|
|
831
1015
|
except Exception as e:
|
|
832
|
-
print(f"[ERROR]
|
|
833
|
-
elif create_charts and len(results.trades) == 0:
|
|
834
|
-
print("\n[!] No trades - skipping charts")
|
|
1016
|
+
print(f"[ERROR] Charts failed: {e}")
|
|
835
1017
|
|
|
836
1018
|
if export_results and len(results.trades) > 0:
|
|
837
|
-
print(f"\n[*] Exporting
|
|
1019
|
+
print(f"\n[*] Exporting: {export_prefix}_*")
|
|
838
1020
|
try:
|
|
839
1021
|
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
1022
|
except Exception as e:
|
|
845
1023
|
print(f"[ERROR] Export failed: {e}")
|
|
846
|
-
elif export_results and len(results.trades) == 0:
|
|
847
|
-
print("\n[!] No trades - skipping export")
|
|
848
1024
|
|
|
849
1025
|
return analyzer
|
|
850
1026
|
|
|
851
1027
|
|
|
852
|
-
# ============================================================
|
|
853
|
-
# EXPORTS
|
|
854
|
-
# ============================================================
|
|
855
1028
|
__all__ = [
|
|
856
|
-
'BacktestResults',
|
|
857
|
-
'
|
|
858
|
-
'
|
|
859
|
-
'
|
|
860
|
-
|
|
861
|
-
'run_backtest',
|
|
862
|
-
'init_api',
|
|
863
|
-
'get_api_method',
|
|
864
|
-
'api_call', # NEW!
|
|
865
|
-
'APIHelper', # NEW!
|
|
866
|
-
'APIManager'
|
|
867
|
-
]
|
|
1029
|
+
'BacktestResults', 'BacktestAnalyzer', 'ResultsReporter',
|
|
1030
|
+
'ChartGenerator', 'ResultsExporter', 'run_backtest',
|
|
1031
|
+
'init_api', 'api_call', 'APIHelper', 'APIManager',
|
|
1032
|
+
'ResourceMonitor', 'create_progress_bar', 'update_progress', 'format_time'
|
|
1033
|
+
]
|
{ivolatility_backtesting-1.1.0.dist-info → ivolatility_backtesting-1.3.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.3.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=xyvYFfUp4jNrfso5MpbUNBY-kK4lUnvyj0lmoMelCYQ,42141
|
|
3
|
+
ivolatility_backtesting-1.3.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
ivolatility_backtesting-1.3.0.dist-info/METADATA,sha256=bf-tS5-RMyzIaHVGAeOOfxUtcN5BCkV9X4JKR83TK5I,2052
|
|
5
|
+
ivolatility_backtesting-1.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
ivolatility_backtesting-1.3.0.dist-info/top_level.txt,sha256=Qv3irUBntr8b11WIKNN6zzCSguwaWC4nWR-ZKq8NsjY,24
|
|
7
|
+
ivolatility_backtesting-1.3.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.3.0.dist-info/licenses}/LICENSE
RENAMED
|
File without changes
|
{ivolatility_backtesting-1.1.0.dist-info → ivolatility_backtesting-1.3.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|