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.

@@ -1,17 +1,23 @@
1
1
  from .ivolatility_backtesting import (
2
2
  BacktestResults, BacktestAnalyzer, ResultsReporter,
3
3
  ChartGenerator, ResultsExporter, run_backtest,
4
- init_api, get_api_method, APIManager
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
- 'get_api_method',
16
- 'APIManager'
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 - UPDATED VERSION
3
- Universal Backtest Framework with API Response Normalization
2
+ ivolatility_backtesting.py - UNIVERSAL BACKTEST FRAMEWORK
3
+ Version 4.1 - Dual RAM Display (Process + Container)
4
4
 
5
- NEW FEATURES:
6
- - APIHelper class for automatic response normalization
7
- - Handles both dict and DataFrame responses from IVolatility API
8
- - Safe data extraction with proper error handling
9
- - Unified interface for all API calls
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
- # Use API helper for normalized responses
18
- data = api_call('/equities/eod/stock-prices', symbol='AAPL', from_='2024-01-01')
19
- if data: # Always returns dict or None
20
- df = pd.DataFrame(data)
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
- # API HELPER - NEW!
53
+ # RESOURCE MONITOR
38
54
  # ============================================================
39
- class APIHelper:
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
- @staticmethod
46
- def normalize_response(response, debug=False):
47
- """
48
- Convert API response to consistent dict format
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
- Args:
51
- response: API response (dict, DataFrame, or other)
52
- debug: Print debug information
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
- dict with 'data' key containing list of records, or None if invalid
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 with {len(response['data'])} records")
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 response without 'data' key")
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] Converted DataFrame to dict with {len(records)} records")
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 response type: {type(response)}")
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 with key: {api_key[:10]}...{api_key[-5:]}")
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 with key"""
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
- method = get_api_method(endpoint)
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 valid data from {endpoint}")
210
- print(f"[api_call] Parameters: {kwargs}")
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] Parameters: {kwargs}")
424
+ print(f"[api_call] Params: {kwargs}")
219
425
  return None
220
426
 
221
427
 
222
428
  # ============================================================
223
- # BACKTEST RESULTS (Unchanged)
429
+ # BACKTEST RESULTS
224
430
  # ============================================================
225
431
  class BacktestResults:
226
432
  """Universal container for backtest results"""
227
- def __init__(self,
228
- equity_curve,
229
- equity_dates,
230
- trades,
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 (Unchanged - same as before)
467
+ # BACKTEST ANALYZER (30+ METRICS)
268
468
  # ============================================================
269
469
  class BacktestAnalyzer:
270
- """Universal metrics calculator"""
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
- # Basic profitability
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 with protection
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
- trades_df = pd.DataFrame(r.trades)
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.metrics.update({
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, CHART GENERATOR, RESULTS EXPORTER
536
- # (All unchanged - same as before)
735
+ # RESULTS REPORTER
537
736
  # ============================================================
538
737
  class ResultsReporter:
539
- """Universal results printer"""
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 debug messages")
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
- """Universal chart creator"""
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 - Comprehensive Analysis',
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('Portfolio Equity Curve', fontsize=12, fontweight='bold')
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
- max_dd_idx = np.argmin(drawdown)
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.axvline(x=np.median(pnl_values), color='blue', linestyle='--', linewidth=2)
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
- bars = ax4.bar(signal_pnl.index, signal_pnl.values, color=colors, alpha=0.7, edgecolor='black')
706
- for bar in bars:
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.set_ylabel('Total P&L ($)')
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
- colors_monthly = ['#4CAF50' if x > 0 else '#f44336' for x in monthly_pnl.values]
723
- ax5.bar(range(len(monthly_pnl)), monthly_pnl.values, color=colors_monthly, alpha=0.7, edgecolor='black')
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', linestyle='-', linewidth=1)
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
- colors_symbols = ['#4CAF50' if x > 0 else '#f44336' for x in symbol_pnl.values]
736
- ax6.barh(range(len(symbol_pnl)), symbol_pnl.values, color=colors_symbols, alpha=0.7, edgecolor='black')
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 10 Symbols by P&L', fontsize=12, fontweight='bold')
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.set_xlabel('Total P&L ($)')
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
- """Universal results exporter"""
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"Trades exported: {prefix}_trades.csv")
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"Equity exported: {prefix}_equity.csv")
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', 'N/A')} to {r.config.get('end_date', 'N/A')}\n\n")
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 Ratio: {m['sharpe']:.2f}\n")
790
- f.write(f"Max Drawdown: {m['max_drawdown']:.2f}%\n")
791
- f.write(f"Win Rate: {m['win_rate']:.2f}%\n")
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"Summary exported: {prefix}_summary.txt")
981
+ print(f"Exported: {prefix}_summary.txt")
795
982
 
796
983
 
797
984
  # ============================================================
798
- # ONE-COMMAND RUNNER (Unchanged)
985
+ # RUN BACKTEST
799
986
  # ============================================================
800
- def run_backtest(strategy_function, config,
801
- print_report=True,
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', 'N/A')} to {config.get('end_date', 'N/A')}")
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] Chart creation failed: {e}")
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 results: {export_prefix}_*.csv")
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
- 'BacktestAnalyzer',
858
- 'ResultsReporter',
859
- 'ChartGenerator',
860
- 'ResultsExporter',
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
+ ]
@@ -1,70 +1,72 @@
1
- Metadata-Version: 2.1
2
- Name: ivolatility_backtesting
3
- Version: 1.1.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
-
25
- # IVolatility Backtesting
26
- A universal backtesting framework for financial strategies using the IVolatility API.
27
-
28
- ## Installation
29
- ```bash
30
- pip install ivolatility_backtesting
31
- ```
32
-
33
- ## Usage
34
- ```python
35
- from ivolatility_backtesting import run_backtest, init_api
36
-
37
- # Initialize API
38
- init_api("your-api-key")
39
-
40
- # Define your strategy
41
- def my_strategy(config):
42
- # Strategy logic
43
- return BacktestResults(
44
- equity_curve=[100000, 110000],
45
- equity_dates=["2023-01-01", "2023-01-02"],
46
- trades=[{"pnl": 1000, "entry_date": "2023-01-01", "exit_date": "2023-01-02"}],
47
- initial_capital=100000,
48
- config=config
49
- )
50
-
51
- # Run backtest
52
- CONFIG = {
53
- "initial_capital": 100000,
54
- "start_date": "2023-01-01",
55
- "end_date": "2024-01-01",
56
- "strategy_name": "My Strategy"
57
- }
58
- analyzer = run_backtest(my_strategy, CONFIG)
59
-
60
- # Access metrics
61
- print(f"Sharpe Ratio: {analyzer.metrics['sharpe']:.2f}")
62
- ```
63
-
64
- ## Requirements
65
- - Python >= 3.8
66
- - pandas >= 1.5.0
67
- - numpy >= 1.21.0
68
- - matplotlib >= 3.5.0
69
- - seaborn >= 0.11.0
70
- - ivolatility >= 1.8.2
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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.3.2)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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,,