ivolatility-backtesting 1.0.1__tar.gz → 1.1.0__tar.gz

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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ivolatility_backtesting
3
- Version: 1.0.1
3
+ Version: 1.1.0
4
4
  Summary: A universal backtesting framework for financial strategies using the IVolatility API.
5
5
  Author-email: IVolatility <support@ivolatility.com>
6
6
  Project-URL: Homepage, https://ivolatility.com
@@ -1,6 +1,12 @@
1
1
  """
2
- ivolatility_backtesting.py
3
- Universal Backtest Framework with One-Command Runner
2
+ ivolatility_backtesting.py - UPDATED VERSION
3
+ Universal Backtest Framework with API Response Normalization
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
4
10
 
5
11
  Usage:
6
12
  from ivolatility_backtesting import *
@@ -8,8 +14,10 @@ Usage:
8
14
  # Initialize API once
9
15
  init_api(os.getenv("API_KEY"))
10
16
 
11
- CONFIG = {...}
12
- analyzer = run_backtest(my_strategy, CONFIG)
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)
13
21
  """
14
22
 
15
23
  import pandas as pd
@@ -26,21 +34,96 @@ plt.rcParams['figure.figsize'] = (15, 8)
26
34
 
27
35
 
28
36
  # ============================================================
29
- # GLOBAL API MANAGER
37
+ # API HELPER - NEW!
30
38
  # ============================================================
31
- class APIManager:
39
+ class APIHelper:
32
40
  """
33
- Centralized API key management for IVolatility API
41
+ Helper class for normalized API responses
42
+ Automatically handles both dict and DataFrame responses
43
+ """
44
+
45
+ @staticmethod
46
+ def normalize_response(response, debug=False):
47
+ """
48
+ Convert API response to consistent dict format
49
+
50
+ Args:
51
+ response: API response (dict, DataFrame, or other)
52
+ debug: Print debug information
53
+
54
+ Returns:
55
+ dict with 'data' key containing list of records, or None if invalid
56
+ """
57
+ if response is None:
58
+ if debug:
59
+ print("[APIHelper] Response is None")
60
+ return None
61
+
62
+ # Case 1: Already a dict with 'data' key
63
+ if isinstance(response, dict):
64
+ if 'data' in response:
65
+ if debug:
66
+ print(f"[APIHelper] Dict response with {len(response['data'])} records")
67
+ return response
68
+ else:
69
+ if debug:
70
+ print("[APIHelper] Dict response without 'data' key")
71
+ return None
72
+
73
+ # Case 2: DataFrame - convert to dict
74
+ if isinstance(response, pd.DataFrame):
75
+ if response.empty:
76
+ if debug:
77
+ print("[APIHelper] Empty DataFrame")
78
+ return None
79
+
80
+ records = response.to_dict('records')
81
+ if debug:
82
+ print(f"[APIHelper] Converted DataFrame to dict with {len(records)} records")
83
+ return {'data': records, 'status': 'success'}
84
+
85
+ # Case 3: Unknown type
86
+ if debug:
87
+ print(f"[APIHelper] Unexpected response type: {type(response)}")
88
+ return None
34
89
 
35
- Usage:
36
- from ivolatility_backtesting import init_api, get_api_method
90
+ @staticmethod
91
+ def safe_dataframe(response, debug=False):
92
+ """
93
+ Safely convert API response to DataFrame
37
94
 
38
- # Initialize once at the start
39
- init_api(os.getenv("API_KEY"))
95
+ Args:
96
+ response: API response (any type)
97
+ debug: Print debug information
40
98
 
41
- # Use anywhere in your code
42
- getOptionsData = get_api_method('/equities/eod/stock-opts-by-param')
43
- data = getOptionsData(symbol='SPY', tradeDate='2024-01-01', ...)
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
+
119
+
120
+ # ============================================================
121
+ # GLOBAL API MANAGER (Updated)
122
+ # ============================================================
123
+ class APIManager:
124
+ """
125
+ Centralized API key management for IVolatility API
126
+ Now includes response normalization
44
127
  """
45
128
  _api_key = None
46
129
  _methods = {}
@@ -56,17 +139,8 @@ class APIManager:
56
139
 
57
140
  @classmethod
58
141
  def get_method(cls, endpoint):
59
- """
60
- Get API method with automatic key injection
61
-
62
- Args:
63
- endpoint: API endpoint path (e.g. '/equities/eod/stock-opts-by-param')
64
-
65
- Returns:
66
- Callable API method
67
- """
142
+ """Get API method with automatic key injection"""
68
143
  if cls._api_key is None:
69
- # Auto-initialize from environment if not set
70
144
  api_key = os.getenv("API_KEY")
71
145
  if not api_key:
72
146
  raise ValueError(
@@ -74,9 +148,7 @@ class APIManager:
74
148
  )
75
149
  cls.initialize(api_key)
76
150
 
77
- # Cache methods to avoid recreation
78
151
  if endpoint not in cls._methods:
79
- # Re-set login params before creating method
80
152
  ivol.setLoginParams(apiKey=cls._api_key)
81
153
  cls._methods[endpoint] = ivol.setMethod(endpoint)
82
154
 
@@ -88,56 +160,80 @@ class APIManager:
88
160
  return cls._api_key is not None
89
161
 
90
162
 
91
- # Public API functions
163
+ # Public API functions (Updated)
92
164
  def init_api(api_key=None):
93
- """
94
- Initialize IVolatility API with key
95
-
96
- Args:
97
- api_key: API key string. If None, tries to load from API_KEY env variable
98
-
99
- Example:
100
- init_api("your-api-key")
101
- # or
102
- init_api() # Auto-loads from environment
103
- """
165
+ """Initialize IVolatility API with key"""
104
166
  if api_key is None:
105
167
  api_key = os.getenv("API_KEY")
106
168
  APIManager.initialize(api_key)
107
169
 
108
170
 
109
171
  def get_api_method(endpoint):
172
+ """Get API method for specified endpoint"""
173
+ return APIManager.get_method(endpoint)
174
+
175
+
176
+ def api_call(endpoint, debug=False, **kwargs):
110
177
  """
111
- Get API method for specified endpoint
178
+ Make API call with automatic response normalization
112
179
 
113
180
  Args:
114
181
  endpoint: API endpoint path
182
+ debug: Enable debug output
183
+ **kwargs: API parameters
115
184
 
116
185
  Returns:
117
- Callable API method with key already configured
186
+ dict with 'data' key (normalized format) or None if error
118
187
 
119
188
  Example:
120
- getOptionsData = get_api_method('/equities/eod/stock-opts-by-param')
121
- data = getOptionsData(symbol='SPY', tradeDate='2024-01-01', cp='C')
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'])
122
201
  """
123
- return APIManager.get_method(endpoint)
202
+ try:
203
+ method = get_api_method(endpoint)
204
+ response = method(**kwargs)
205
+
206
+ normalized = APIHelper.normalize_response(response, debug=debug)
207
+
208
+ 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}")
211
+
212
+ return normalized
213
+
214
+ except Exception as e:
215
+ if debug:
216
+ print(f"[api_call] Exception: {e}")
217
+ print(f"[api_call] Endpoint: {endpoint}")
218
+ print(f"[api_call] Parameters: {kwargs}")
219
+ return None
124
220
 
125
221
 
222
+ # ============================================================
223
+ # BACKTEST RESULTS (Unchanged)
224
+ # ============================================================
126
225
  class BacktestResults:
127
- """
128
- Universal container for backtest results
129
- ANY strategy must return this format
130
- """
226
+ """Universal container for backtest results"""
131
227
  def __init__(self,
132
- equity_curve, # List of equity values
133
- equity_dates, # List of dates (matching equity_curve)
134
- trades, # List of dicts with trade info
135
- initial_capital, # Starting capital
136
- config, # Strategy config dict
137
- benchmark_prices=None, # Optional: dict {date: price}
138
- benchmark_symbol='SPY', # Optional: benchmark ticker
139
- daily_returns=None, # Optional: if not provided, calculated
140
- debug_info=None): # Optional: debug information
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):
141
237
 
142
238
  self.equity_curve = equity_curve
143
239
  self.equity_dates = equity_dates
@@ -149,7 +245,6 @@ class BacktestResults:
149
245
  self.benchmark_symbol = benchmark_symbol
150
246
  self.debug_info = debug_info if debug_info else []
151
247
 
152
- # Calculate daily returns if not provided
153
248
  if daily_returns is None and len(equity_curve) > 1:
154
249
  self.daily_returns = [
155
250
  (equity_curve[i] - equity_curve[i-1]) / equity_curve[i-1]
@@ -158,7 +253,6 @@ class BacktestResults:
158
253
  else:
159
254
  self.daily_returns = daily_returns if daily_returns else []
160
255
 
161
- # Calculate max drawdown
162
256
  self.max_drawdown = self._calculate_max_drawdown()
163
257
 
164
258
  def _calculate_max_drawdown(self):
@@ -169,11 +263,11 @@ class BacktestResults:
169
263
  return abs(np.min(drawdowns))
170
264
 
171
265
 
266
+ # ============================================================
267
+ # BACKTEST ANALYZER (Unchanged - same as before)
268
+ # ============================================================
172
269
  class BacktestAnalyzer:
173
- """
174
- Universal metrics calculator
175
- Works with any BacktestResults object
176
- """
270
+ """Universal metrics calculator"""
177
271
  def __init__(self, results):
178
272
  self.results = results
179
273
  self.metrics = {}
@@ -186,13 +280,12 @@ class BacktestAnalyzer:
186
280
  self.metrics['total_pnl'] = r.final_capital - r.initial_capital
187
281
  self.metrics['total_return'] = (self.metrics['total_pnl'] / r.initial_capital) * 100
188
282
 
189
- # CAGR - WITH PROTECTION AGAINST DIVISION BY ZERO
283
+ # CAGR with protection
190
284
  if len(r.equity_dates) > 0:
191
285
  start_date = min(r.equity_dates)
192
286
  end_date = max(r.equity_dates)
193
287
  days_diff = (end_date - start_date).days
194
288
 
195
- # PROTECTION: если даты одинаковые или разница < 1 день
196
289
  if days_diff <= 0:
197
290
  self.metrics['cagr'] = 0
198
291
  self.metrics['show_cagr'] = False
@@ -438,6 +531,10 @@ class BacktestAnalyzer:
438
531
  return min(exposure_pct, 100.0)
439
532
 
440
533
 
534
+ # ============================================================
535
+ # RESULTS REPORTER, CHART GENERATOR, RESULTS EXPORTER
536
+ # (All unchanged - same as before)
537
+ # ============================================================
441
538
  class ResultsReporter:
442
539
  """Universal results printer"""
443
540
 
@@ -451,17 +548,15 @@ class ResultsReporter:
451
548
  print("="*80)
452
549
  print()
453
550
 
454
- # PRINT DEBUG INFO IF AVAILABLE
455
551
  if hasattr(r, 'debug_info') and len(r.debug_info) > 0:
456
552
  print("DEBUG INFORMATION")
457
553
  print("-"*80)
458
- for debug_msg in r.debug_info[:10]: # First 10 messages
554
+ for debug_msg in r.debug_info[:10]:
459
555
  print(debug_msg)
460
556
  if len(r.debug_info) > 10:
461
557
  print(f"... and {len(r.debug_info) - 10} more debug messages")
462
558
  print()
463
559
 
464
- # Profitability
465
560
  print("PROFITABILITY METRICS")
466
561
  print("-"*80)
467
562
  print(f"Initial Capital: ${r.initial_capital:>15,.2f}")
@@ -475,7 +570,6 @@ class ResultsReporter:
475
570
  print(f"Annualized Return: {m['cagr']:>15.2f}% (extrapolated to 1 year)")
476
571
  print()
477
572
 
478
- # Risk
479
573
  print("RISK METRICS")
480
574
  print("-"*80)
481
575
  print(f"Sharpe Ratio: {m['sharpe']:>15.2f} (>1 good, >2 excellent)")
@@ -506,7 +600,6 @@ class ResultsReporter:
506
600
  print(f"Beta (vs {r.benchmark_symbol}): {m['beta']:>15.2f} (<1 defensive, >1 aggressive)")
507
601
  print(f"R^2 (vs {r.benchmark_symbol}): {m['r_squared']:>15.2f} (market correlation 0-1)")
508
602
 
509
- # Warning for unrealistic results
510
603
  if abs(m['total_return']) > 200 or m['volatility'] > 150:
511
604
  print()
512
605
  print("UNREALISTIC RESULTS DETECTED:")
@@ -518,7 +611,6 @@ class ResultsReporter:
518
611
 
519
612
  print()
520
613
 
521
- # Efficiency
522
614
  print("EFFICIENCY METRICS")
523
615
  print("-"*80)
524
616
  if m['recovery_factor'] != 0:
@@ -527,7 +619,6 @@ class ResultsReporter:
527
619
  print(f"Exposure Time: {m['exposure_time']:>15.1f}% (time in market)")
528
620
  print()
529
621
 
530
- # Trading stats
531
622
  print("TRADING STATISTICS")
532
623
  print("-"*80)
533
624
  print(f"Total Trades: {m['total_trades']:>15}")
@@ -671,14 +762,12 @@ class ResultsExporter:
671
762
  print("No trades to export")
672
763
  return
673
764
 
674
- # Export trades
675
765
  trades_df = pd.DataFrame(r.trades)
676
766
  trades_df['entry_date'] = pd.to_datetime(trades_df['entry_date']).dt.strftime('%Y-%m-%d')
677
767
  trades_df['exit_date'] = pd.to_datetime(trades_df['exit_date']).dt.strftime('%Y-%m-%d')
678
768
  trades_df.to_csv(f'{prefix}_trades.csv', index=False)
679
769
  print(f"Trades exported: {prefix}_trades.csv")
680
770
 
681
- # Export equity curve
682
771
  equity_df = pd.DataFrame({
683
772
  'date': pd.to_datetime(r.equity_dates).strftime('%Y-%m-%d'),
684
773
  'equity': r.equity_curve
@@ -686,7 +775,6 @@ class ResultsExporter:
686
775
  equity_df.to_csv(f'{prefix}_equity.csv', index=False)
687
776
  print(f"Equity exported: {prefix}_equity.csv")
688
777
 
689
- # Export summary
690
778
  with open(f'{prefix}_summary.txt', 'w') as f:
691
779
  f.write("BACKTEST SUMMARY\n")
692
780
  f.write("="*70 + "\n\n")
@@ -707,7 +795,7 @@ class ResultsExporter:
707
795
 
708
796
 
709
797
  # ============================================================
710
- # ONE-COMMAND RUNNER
798
+ # ONE-COMMAND RUNNER (Unchanged)
711
799
  # ============================================================
712
800
  def run_backtest(strategy_function, config,
713
801
  print_report=True,
@@ -715,36 +803,7 @@ def run_backtest(strategy_function, config,
715
803
  export_results=True,
716
804
  chart_filename='backtest_results.png',
717
805
  export_prefix='backtest'):
718
- """
719
- Run complete backtest with one command
720
-
721
- Args:
722
- strategy_function: Your strategy function that returns BacktestResults
723
- config: Configuration dictionary for the strategy
724
- print_report: Print full metrics report (default: True)
725
- create_charts: Generate 6 charts (default: True)
726
- export_results: Export to CSV files (default: True)
727
- chart_filename: Name for chart file (default: 'backtest_results.png')
728
- export_prefix: Prefix for exported files (default: 'backtest')
729
-
730
- Returns:
731
- BacktestAnalyzer object with all metrics
732
-
733
- Example:
734
- from ivolatility_backtesting import run_backtest
735
-
736
- def my_strategy(config):
737
- # ... your strategy logic
738
- return BacktestResults(...)
739
-
740
- CONFIG = {'initial_capital': 100000, ...}
741
-
742
- # Run everything with one command!
743
- analyzer = run_backtest(my_strategy, CONFIG)
744
-
745
- # Access metrics
746
- print(f"Sharpe: {analyzer.metrics['sharpe']:.2f}")
747
- """
806
+ """Run complete backtest with one command"""
748
807
 
749
808
  print("="*80)
750
809
  print(" "*25 + "STARTING BACKTEST")
@@ -754,20 +813,16 @@ def run_backtest(strategy_function, config,
754
813
  print(f"Capital: ${config.get('initial_capital', 0):,.0f}")
755
814
  print("="*80 + "\n")
756
815
 
757
- # Run strategy
758
816
  results = strategy_function(config)
759
817
 
760
- # Calculate metrics
761
818
  print("\n[*] Calculating metrics...")
762
819
  analyzer = BacktestAnalyzer(results)
763
820
  analyzer.calculate_all_metrics()
764
821
 
765
- # Print report
766
822
  if print_report:
767
823
  print("\n" + "="*80)
768
824
  ResultsReporter.print_full_report(analyzer)
769
825
 
770
- # Create charts
771
826
  if create_charts and len(results.trades) > 0:
772
827
  print(f"\n[*] Creating charts: {chart_filename}")
773
828
  try:
@@ -778,7 +833,6 @@ def run_backtest(strategy_function, config,
778
833
  elif create_charts and len(results.trades) == 0:
779
834
  print("\n[!] No trades - skipping charts")
780
835
 
781
- # Export results
782
836
  if export_results and len(results.trades) > 0:
783
837
  print(f"\n[*] Exporting results: {export_prefix}_*.csv")
784
838
  try:
@@ -792,22 +846,12 @@ def run_backtest(strategy_function, config,
792
846
  elif export_results and len(results.trades) == 0:
793
847
  print("\n[!] No trades - skipping export")
794
848
 
795
- # Final summary
796
- #print("\n" + "="*80)
797
- #print(" "*30 + "SUMMARY")
798
- #print("="*80)
799
- #print(f"Total Return: {analyzer.metrics['total_return']:>10.2f}%")
800
- #print(f"Sharpe Ratio: {analyzer.metrics['sharpe']:>10.2f}")
801
- #print(f"Max Drawdown: {analyzer.metrics['max_drawdown']:>10.2f}%")
802
- #print(f"Win Rate: {analyzer.metrics['win_rate']:>10.1f}%")
803
- #print(f"Total Trades: {analyzer.metrics['total_trades']:>10}")
804
- #print(f"Profit Factor: {analyzer.metrics['profit_factor']:>10.2f}")
805
- #print("="*80)
806
-
807
849
  return analyzer
808
850
 
809
851
 
810
- # Export all classes and functions
852
+ # ============================================================
853
+ # EXPORTS
854
+ # ============================================================
811
855
  __all__ = [
812
856
  'BacktestResults',
813
857
  'BacktestAnalyzer',
@@ -817,5 +861,7 @@ __all__ = [
817
861
  'run_backtest',
818
862
  'init_api',
819
863
  'get_api_method',
864
+ 'api_call', # NEW!
865
+ 'APIHelper', # NEW!
820
866
  'APIManager'
821
- ]
867
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ivolatility_backtesting
3
- Version: 1.0.1
3
+ Version: 1.1.0
4
4
  Summary: A universal backtesting framework for financial strategies using the IVolatility API.
5
5
  Author-email: IVolatility <support@ivolatility.com>
6
6
  Project-URL: Homepage, https://ivolatility.com
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ivolatility_backtesting"
7
- version = "1.0.1"
7
+ version = "1.1.0"
8
8
  description = "A universal backtesting framework for financial strategies using the IVolatility API."
9
9
  readme = "README.md"
10
10
  authors = [