ivolatility-backtesting 1.28__tar.gz → 1.29__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ivolatility_backtesting
3
- Version: 1.28
3
+ Version: 1.29
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
@@ -116,9 +116,9 @@ STRATEGIES = {
116
116
  # Indicators (optional - for entry filtering)
117
117
  'indicators': [
118
118
  {
119
- 'name': 'iv_rank',
119
+ 'name': 'iv_rank_ivx', # ✅ NEW: Uses IVX data (no lookback needed!)
120
120
  'required': False,
121
- 'params_from_config': ['lookback_period', 'dte_target'] # lookback_period auto-calculated from lookback_ratio
121
+ 'params_from_config': [] # No params needed - IVX has built-in high/low!
122
122
  },
123
123
  {
124
124
  'name': 'iv_skew',
@@ -694,6 +694,16 @@ INDICATOR_REGISTRY = {
694
694
  'cache_key_params': ['lookback_period', 'dte_target']
695
695
  },
696
696
 
697
+ 'iv_rank_ivx': {
698
+ 'description': 'IV Rank from IVX data (uses IVolatility pre-calculated high/low)',
699
+ 'inputs': ['ivx_df'], # ← Uses IVX instead of options!
700
+ 'required_params': [], # No lookback needed!
701
+ 'optional_params': {'dte_target': 60}, # Not used but kept for compatibility
702
+ 'calculator': 'calculate_iv_rank_from_ivx',
703
+ 'outputs': ['date', 'atm_iv', 'iv_rank', 'iv_high', 'iv_low'],
704
+ 'cache_key_params': [] # No params needed for cache key
705
+ },
706
+
697
707
  'iv_skew': {
698
708
  'description': 'Put/Call IV Skew',
699
709
  'inputs': ['options_df'],
@@ -900,6 +910,7 @@ def calculate_iv_lean_indicator(options_df, lookback_period, dte_target, dte_tol
900
910
  def calculate_iv_rank_indicator(options_df, lookback_period, dte_target):
901
911
  """
902
912
  Calculate IV Rank timeseries (VECTORIZED!)
913
+ Supports MULTI-SYMBOL data (if 'symbol' column present)
903
914
 
904
915
  Args:
905
916
  options_df: Options data
@@ -907,11 +918,38 @@ def calculate_iv_rank_indicator(options_df, lookback_period, dte_target):
907
918
  dte_target: Target DTE
908
919
 
909
920
  Returns:
910
- pd.DataFrame with columns: ['date', 'atm_iv', 'iv_rank', 'iv_high', 'iv_low']
921
+ pd.DataFrame with columns: ['date', 'symbol', 'atm_iv', 'iv_rank', 'iv_high', 'iv_low']
911
922
  """
912
923
  import pandas as pd
913
924
  import numpy as np
914
925
 
926
+ # Check if multi-symbol data
927
+ has_symbol = 'symbol' in options_df.columns
928
+
929
+ if has_symbol:
930
+ # Process each symbol separately
931
+ all_results = []
932
+ for symbol in options_df['symbol'].unique():
933
+ symbol_df = options_df[options_df['symbol'] == symbol]
934
+ symbol_result = _calculate_iv_rank_single_symbol(symbol_df, lookback_period, dte_target)
935
+ if not symbol_result.empty:
936
+ symbol_result['symbol'] = symbol
937
+ all_results.append(symbol_result)
938
+
939
+ if all_results:
940
+ return pd.concat(all_results, ignore_index=True)
941
+ else:
942
+ return pd.DataFrame()
943
+ else:
944
+ # Single symbol
945
+ return _calculate_iv_rank_single_symbol(options_df, lookback_period, dte_target)
946
+
947
+
948
+ def _calculate_iv_rank_single_symbol(options_df, lookback_period, dte_target):
949
+ """Helper function to calculate IV Rank for a single symbol"""
950
+ import pandas as pd
951
+ import numpy as np # Needed for np.where
952
+
915
953
  trading_dates = sorted(options_df['date'].unique())
916
954
  iv_history = []
917
955
 
@@ -960,17 +998,92 @@ def calculate_iv_rank_indicator(options_df, lookback_period, dte_target):
960
998
  # ✅ VECTORIZED: Rolling high/low
961
999
  iv_df = pd.DataFrame(iv_history).sort_values('date').reset_index(drop=True)
962
1000
 
963
- iv_df['iv_high'] = iv_df['atm_iv'].rolling(window=lookback_period, min_periods=10).max()
964
- iv_df['iv_low'] = iv_df['atm_iv'].rolling(window=lookback_period, min_periods=10).min()
1001
+ # Use at least 50% of lookback_period as min_periods (more reliable)
1002
+ min_periods_required = max(lookback_period // 2, 30)
1003
+
1004
+ iv_df['iv_high'] = iv_df['atm_iv'].rolling(window=lookback_period, min_periods=min_periods_required).max()
1005
+ iv_df['iv_low'] = iv_df['atm_iv'].rolling(window=lookback_period, min_periods=min_periods_required).min()
965
1006
 
966
1007
  # IV Rank calculation
967
- iv_df['iv_rank'] = ((iv_df['atm_iv'] - iv_df['iv_low']) /
968
- (iv_df['iv_high'] - iv_df['iv_low'])) * 100
969
- iv_df['iv_rank'] = iv_df['iv_rank'].fillna(50.0) # Default to 50% if can't calculate
1008
+ # Avoid division by zero when high == low
1009
+ iv_range = iv_df['iv_high'] - iv_df['iv_low']
1010
+ iv_df['iv_rank'] = np.where(
1011
+ iv_range > 0.001, # Minimum range threshold
1012
+ ((iv_df['atm_iv'] - iv_df['iv_low']) / iv_range) * 100,
1013
+ 50.0 # Default when range is too small or missing
1014
+ )
970
1015
 
971
1016
  return iv_df
972
1017
 
973
1018
 
1019
+ def calculate_iv_rank_from_ivx(ivx_df, dte_target=None):
1020
+ """
1021
+ Calculate IV Rank from IVX data (uses pre-calculated high/low from IVolatility)
1022
+
1023
+ ✅ ADVANTAGES over calculate_iv_rank_indicator:
1024
+ - No lookback_period needed (IVolatility already calculated high/low over 1 year)
1025
+ - Works from day 1 (no NaN values at start)
1026
+ - More reliable (standardized calculation from IVolatility)
1027
+
1028
+ Args:
1029
+ ivx_df: IVX data with columns ['date', 'IVX Mean', 'IVX High', 'IVX Low']
1030
+ dte_target: Not used, kept for compatibility with indicator framework
1031
+
1032
+ Returns:
1033
+ pd.DataFrame with columns: ['date', 'symbol', 'atm_iv', 'iv_rank', 'iv_high', 'iv_low']
1034
+ """
1035
+ import pandas as pd
1036
+ import numpy as np
1037
+
1038
+ # Check if multi-symbol data
1039
+ has_symbol = 'symbol' in ivx_df.columns
1040
+
1041
+ if ivx_df.empty:
1042
+ return pd.DataFrame()
1043
+
1044
+ results = []
1045
+
1046
+ for _, row in ivx_df.iterrows():
1047
+ try:
1048
+ # Get IVX values (already calculated by IVolatility)
1049
+ ivx_mean = float(row.get('IVX Mean', row.get('ivx_mean', np.nan)))
1050
+ ivx_high = float(row.get('IVX High', row.get('ivx_high', np.nan)))
1051
+ ivx_low = float(row.get('IVX Low', row.get('ivx_low', np.nan)))
1052
+
1053
+ # Calculate IV Rank
1054
+ if pd.notna(ivx_mean) and pd.notna(ivx_high) and pd.notna(ivx_low):
1055
+ iv_range = ivx_high - ivx_low
1056
+ if iv_range > 0.001: # Minimum threshold
1057
+ iv_rank = ((ivx_mean - ivx_low) / iv_range) * 100
1058
+ else:
1059
+ iv_rank = 50.0
1060
+ else:
1061
+ # Missing data
1062
+ continue
1063
+
1064
+ result = {
1065
+ 'date': row['date'],
1066
+ 'atm_iv': ivx_mean,
1067
+ 'iv_rank': iv_rank,
1068
+ 'iv_high': ivx_high,
1069
+ 'iv_low': ivx_low
1070
+ }
1071
+
1072
+ if has_symbol:
1073
+ result['symbol'] = row['symbol']
1074
+
1075
+ results.append(result)
1076
+
1077
+ except Exception as e:
1078
+ # Skip rows with errors
1079
+ continue
1080
+
1081
+ if not results:
1082
+ return pd.DataFrame()
1083
+
1084
+ return pd.DataFrame(results)
1085
+
1086
+
974
1087
  def calculate_iv_skew_indicator(options_df, dte_target, delta_otm=0.25):
975
1088
  """
976
1089
  Calculate Put/Call IV Skew timeseries
@@ -1914,30 +2027,37 @@ class StrategyRegistry:
1914
2027
  contracts = position_params.get('contracts', 1)
1915
2028
 
1916
2029
  if category == 'DEBIT':
1917
- # DEBIT: max risk = premium paid
2030
+ # DEBIT: max risk = premium paid (total_cost already includes contracts)
1918
2031
  return abs(total_cost)
1919
2032
 
1920
2033
  # CREDIT: max risk depends on strategy
2034
+ # NOTE: total_cost is ALREADY multiplied by contracts in calculate_entry_cost()!
1921
2035
  if strategy_type == 'IRON_CONDOR':
1922
2036
  wing_width = position_params.get('wing_width', 0)
1923
- # Max risk = (wing_width * 100) - credit (only ONE side can lose)
2037
+ # Max risk = (wing_width * 100 * contracts) - credit (only ONE side can lose)
1924
2038
  # total_cost is negative for credit, so we add it
1925
- return (wing_width * 100 + total_cost) * contracts # +total_cost because it's negative
2039
+ return (wing_width * 100 * contracts) + total_cost # ✅ FIXED: total_cost already includes contracts
1926
2040
 
1927
- elif strategy_type in ['BULL_PUT_SPREAD', 'BEAR_CALL_SPREAD']:
2041
+ elif strategy_type in ['BULL_PUT_SPREAD', 'BEAR_CALL_SPREAD', 'CREDIT_SPREAD']:
1928
2042
  spread_width = position_params.get('spread_width', 0)
1929
- # Max risk = (spread_width * 100) - credit
1930
- return (spread_width * 100 + total_cost) * contracts
2043
+ # Max risk = (spread_width * 100 * contracts) - credit
2044
+ return (spread_width * 100 * contracts) + total_cost # FIXED: total_cost already includes contracts
2045
+
2046
+ elif strategy_type == 'IRON_BUTTERFLY':
2047
+ wing_width = position_params.get('wing_width', 0)
2048
+ # Max risk = (wing_width * 100 * contracts) - credit (similar to Iron Condor)
2049
+ return (wing_width * 100 * contracts) + total_cost # ✅ FIXED: total_cost already includes contracts
1931
2050
 
1932
2051
  elif strategy_type == 'COVERED_CALL':
1933
- # Max risk = stock cost (unlimited downside, but capped by entry)
2052
+ # Max risk = stock cost - call premium (stock can go to $0, but we keep premium)
1934
2053
  stock_price = position_params.get('stock_price', 0)
1935
- return stock_price * 100 * contracts
2054
+ # total_cost is negative (credit from selling call), so adding it reduces risk
2055
+ return (stock_price * 100 * contracts) + total_cost # ✅ FIXED: subtract call premium
1936
2056
 
1937
2057
  elif strategy_type == 'CASH_SECURED_PUT':
1938
2058
  strike = position_params.get('strike', 0)
1939
- # Max risk = strike price (if stock goes to $0)
1940
- return (strike * 100 + total_cost) * contracts
2059
+ # Max risk = (strike * contracts) - premium (if stock goes to $0)
2060
+ return (strike * 100 * contracts) + total_cost # FIXED: total_cost already includes contracts
1941
2061
 
1942
2062
  else:
1943
2063
  # Default: assume max risk is 2x credit (conservative)
@@ -4239,11 +4359,13 @@ class PositionManager:
4239
4359
  'exit_date': exit_date,
4240
4360
  'symbol': position['symbol'],
4241
4361
  'signal': position['type'],
4362
+ 'type': position.get('type', ''), # ✅ CRITICAL: Export 'type' field (BUY_IRON_CONDOR/SELL_IRON_CONDOR)
4242
4363
  'entry_price': position['entry_price'],
4243
4364
  'exit_price': exit_price,
4244
4365
  'quantity': position['quantity'],
4245
4366
  'pnl': pnl,
4246
4367
  'return_pct': pnl_pct,
4368
+ 'pnl_pct': pnl_pct, # ✅ CRITICAL: Export 'pnl_pct' (same as return_pct for compatibility)
4247
4369
  'exit_reason': close_reason,
4248
4370
  'stop_type': self.sl_config.get('type', 'none') if self.sl_enabled else 'none',
4249
4371
  **kwargs
@@ -4257,21 +4379,37 @@ class PositionManager:
4257
4379
  'short_strike', 'long_strike', # For spreads (iron condor, butterfly, etc.)
4258
4380
  'short_expiration', 'long_expiration', # For calendar spreads (different expirations)
4259
4381
  'opt_type', 'spread_type',
4382
+ # ✅ CRITICAL: Iron Condor strikes (4 legs)
4383
+ 'short_call_strike', 'long_call_strike', 'short_put_strike', 'long_put_strike',
4384
+ # ✅ CRITICAL: Position metadata
4385
+ 'dte', 'position_size_pct', 'total_cost', 'strategy_type',
4386
+ 'capital_at_entry', 'target_allocation', 'actual_allocation',
4387
+ 'available_equity_at_entry', 'locked_capital_at_entry', 'open_positions_at_entry',
4388
+ 'highest_price', 'lowest_price',
4260
4389
  # IV Lean specific
4261
4390
  'entry_z_score', 'entry_lean', 'exit_lean', 'iv_lean_entry',
4262
4391
  # IV data
4263
4392
  'call_iv_entry', 'put_iv_entry', 'iv_entry',
4264
4393
  'iv_rank_entry', 'iv_percentile_entry',
4394
+ # Iron Condor IV at entry (4 separate legs)
4395
+ 'short_call_iv_entry', 'long_call_iv_entry', 'short_put_iv_entry', 'long_put_iv_entry',
4265
4396
  # Greeks at entry (✅ EXPORTED AT ENTRY!)
4266
4397
  'call_vega_entry', 'call_theta_entry', 'put_vega_entry', 'put_theta_entry',
4267
4398
  'call_delta_entry', 'call_gamma_entry', 'put_delta_entry', 'put_gamma_entry',
4268
4399
  'net_delta_entry', 'net_gamma_entry', 'net_vega_entry', 'net_theta_entry',
4400
+ # Iron Condor Greeks at entry (4 separate legs)
4401
+ 'short_call_delta_entry', 'short_call_gamma_entry', 'short_call_vega_entry', 'short_call_theta_entry',
4402
+ 'long_call_delta_entry', 'long_call_gamma_entry', 'long_call_vega_entry', 'long_call_theta_entry',
4403
+ 'short_put_delta_entry', 'short_put_gamma_entry', 'short_put_vega_entry', 'short_put_theta_entry',
4404
+ 'long_put_delta_entry', 'long_put_gamma_entry', 'long_put_vega_entry', 'long_put_theta_entry',
4269
4405
  # Entry criteria (universal for all strategies)
4270
4406
  'target_delta_entry', 'delta_threshold_entry',
4271
4407
  'entry_price_pct', 'distance_from_strike_entry',
4272
4408
  'dte_entry', 'target_dte_entry',
4273
4409
  'volume_entry', 'open_interest_entry', 'volume_ratio_entry',
4274
- 'entry_criteria', 'entry_signal', 'entry_reason']:
4410
+ 'entry_criteria', 'entry_signal', 'entry_reason',
4411
+ # High Vega specific entry data
4412
+ 'entry_iv_rank', 'entry_signal_type', 'entry_wing_width', 'entry_vega_per_contract']:
4275
4413
  if key in position:
4276
4414
  trade[key] = position[key]
4277
4415
 
@@ -4280,6 +4418,11 @@ class PositionManager:
4280
4418
  # Call/Put entry prices (for straddle/strangle strategies)
4281
4419
  'call_entry_bid', 'call_entry_ask', 'call_entry_mid',
4282
4420
  'put_entry_bid', 'put_entry_ask', 'put_entry_mid',
4421
+ # Iron Condor entry prices (4 separate legs)
4422
+ 'short_call_entry_bid', 'short_call_entry_ask', 'short_call_entry_mid',
4423
+ 'long_call_entry_bid', 'long_call_entry_ask', 'long_call_entry_mid',
4424
+ 'short_put_entry_bid', 'short_put_entry_ask', 'short_put_entry_mid',
4425
+ 'long_put_entry_bid', 'long_put_entry_ask', 'long_put_entry_mid',
4283
4426
  'underlying_entry_price']:
4284
4427
  if key in position:
4285
4428
  trade[key] = position[key]
@@ -4288,17 +4431,29 @@ class PositionManager:
4288
4431
  'long_exit_bid', 'long_exit_ask',
4289
4432
  # Call/Put exit prices (for straddle/strangle strategies)
4290
4433
  'call_exit_bid', 'call_exit_ask', 'put_exit_bid', 'put_exit_ask',
4434
+ # Iron Condor exit prices (4 separate legs)
4435
+ 'short_call_exit_bid', 'short_call_exit_ask',
4436
+ 'long_call_exit_bid', 'long_call_exit_ask',
4437
+ 'short_put_exit_bid', 'short_put_exit_ask',
4438
+ 'long_put_exit_bid', 'long_put_exit_ask',
4291
4439
  'underlying_exit_price', 'underlying_change_pct',
4292
4440
  'stop_threshold', 'actual_value',
4293
4441
  # IV data at exit
4294
4442
  'call_iv_exit', 'put_iv_exit', 'iv_lean_exit', 'iv_exit',
4295
4443
  'iv_rank_exit', 'iv_percentile_exit',
4444
+ # Iron Condor IV at exit (4 separate legs)
4445
+ 'short_call_iv_exit', 'long_call_iv_exit', 'short_put_iv_exit', 'long_put_iv_exit',
4296
4446
  # IV Lean Z-score at exit (for IV Lean strategies)
4297
4447
  'exit_z_score',
4298
4448
  # Greeks at exit (✅ EXPORTED AT EXIT!)
4299
4449
  'call_vega_exit', 'call_theta_exit', 'put_vega_exit', 'put_theta_exit',
4300
4450
  'call_delta_exit', 'call_gamma_exit', 'put_delta_exit', 'put_gamma_exit',
4301
4451
  'net_delta_exit', 'net_gamma_exit', 'net_vega_exit', 'net_theta_exit',
4452
+ # Iron Condor Greeks at exit (4 separate legs)
4453
+ 'short_call_delta_exit', 'short_call_gamma_exit', 'short_call_vega_exit', 'short_call_theta_exit',
4454
+ 'long_call_delta_exit', 'long_call_gamma_exit', 'long_call_vega_exit', 'long_call_theta_exit',
4455
+ 'short_put_delta_exit', 'short_put_gamma_exit', 'short_put_vega_exit', 'short_put_theta_exit',
4456
+ 'long_put_delta_exit', 'long_put_gamma_exit', 'long_put_vega_exit', 'long_put_theta_exit',
4302
4457
  # Exit criteria (universal for all strategies)
4303
4458
  'target_delta_exit', 'delta_threshold_exit',
4304
4459
  'exit_price_pct', 'distance_from_strike_exit',
@@ -5824,75 +5979,114 @@ def create_stoploss_charts(analyzer, filename='stoploss_analysis.png', show_plot
5824
5979
  # RESULTS EXPORTER (unchanged)
5825
5980
  # ============================================================
5826
5981
  # ============================================================
5827
- # OPTIMAL COLUMN ORDER (130 columns)
5982
+ # OPTIMAL COLUMN ORDER (150+ columns) - ✅ Added Iron Condor support
5828
5983
  # ============================================================
5829
5984
  OPTIMAL_COLUMN_ORDER = [
5830
5985
  # 1. IDENTIFIERS (4)
5831
5986
  'entry_date', 'exit_date', 'symbol', 'signal',
5832
5987
 
5833
- # 2. RESULTS (4)
5988
+ # 2. RESULTS (6) - Added type and pnl_pct
5834
5989
  'pnl', 'return_pct', 'exit_reason', 'stop_type',
5990
+ 'type', # ✅ BUY_IRON_CONDOR / SELL_IRON_CONDOR
5991
+ 'pnl_pct', # ✅ P&L percentage (same as return_pct for compatibility)
5835
5992
 
5836
- # 3. OPTION PARAMETERS (14) - Added quantity after contracts
5993
+ # 3. OPTION PARAMETERS (18) - Added Iron Condor strikes
5837
5994
  'strike', 'call_strike', 'put_strike',
5838
5995
  'expiration', 'call_expiration', 'put_expiration',
5839
5996
  'contracts', 'quantity',
5840
5997
  'short_strike', 'long_strike',
5841
5998
  'short_expiration', 'long_expiration',
5842
5999
  'opt_type', 'spread_type',
6000
+ # ✅ Iron Condor strikes (4 legs)
6001
+ 'short_call_strike', 'long_call_strike',
6002
+ 'short_put_strike', 'long_put_strike',
5843
6003
 
5844
- # 4. ENTRY PRICES (13)
6004
+ # 4. POSITION METADATA (12) - ✅ Iron Condor specific
6005
+ 'dte', 'position_size_pct', 'total_cost', 'strategy_type',
6006
+ 'capital_at_entry', 'target_allocation', 'actual_allocation',
6007
+ 'available_equity_at_entry', 'locked_capital_at_entry', 'open_positions_at_entry',
6008
+ 'highest_price', 'lowest_price',
6009
+
6010
+ # 5. ENTRY PRICES (25) - Added Iron Condor 4 legs
5845
6011
  'entry_price', 'underlying_entry_price',
5846
6012
  'call_entry_bid', 'call_entry_ask', 'call_entry_mid',
5847
6013
  'put_entry_bid', 'put_entry_ask', 'put_entry_mid',
5848
6014
  'short_entry_bid', 'short_entry_ask', 'short_entry_mid',
5849
6015
  'long_entry_bid', 'long_entry_ask', 'long_entry_mid',
6016
+ # ✅ Iron Condor entry prices (4 legs × 3 prices)
6017
+ 'short_call_entry_bid', 'short_call_entry_ask', 'short_call_entry_mid',
6018
+ 'long_call_entry_bid', 'long_call_entry_ask', 'long_call_entry_mid',
6019
+ 'short_put_entry_bid', 'short_put_entry_ask', 'short_put_entry_mid',
6020
+ 'long_put_entry_bid', 'long_put_entry_ask', 'long_put_entry_mid',
5850
6021
 
5851
- # 5. ENTRY METRICS (8)
6022
+ # 6. ENTRY METRICS (11) - Added Iron Condor IV
5852
6023
  'entry_z_score', 'entry_lean', 'iv_lean_entry',
5853
6024
  'call_iv_entry', 'put_iv_entry', 'iv_entry',
5854
6025
  'iv_rank_entry', 'iv_percentile_entry',
6026
+ # ✅ Iron Condor entry IV (4 legs)
6027
+ 'short_call_iv_entry', 'long_call_iv_entry',
6028
+ 'short_put_iv_entry', 'long_put_iv_entry',
5855
6029
 
5856
- # 6. ENTRY GREEKS (12)
6030
+ # 7. ENTRY GREEKS (28) - Added Iron Condor Greeks
5857
6031
  'call_delta_entry', 'call_gamma_entry', 'call_vega_entry', 'call_theta_entry',
5858
6032
  'put_delta_entry', 'put_gamma_entry', 'put_vega_entry', 'put_theta_entry',
5859
6033
  'net_delta_entry', 'net_gamma_entry', 'net_vega_entry', 'net_theta_entry',
6034
+ # ✅ Iron Condor entry Greeks (4 legs × 4 greeks)
6035
+ 'short_call_delta_entry', 'short_call_gamma_entry', 'short_call_vega_entry', 'short_call_theta_entry',
6036
+ 'long_call_delta_entry', 'long_call_gamma_entry', 'long_call_vega_entry', 'long_call_theta_entry',
6037
+ 'short_put_delta_entry', 'short_put_gamma_entry', 'short_put_vega_entry', 'short_put_theta_entry',
6038
+ 'long_put_delta_entry', 'long_put_gamma_entry', 'long_put_vega_entry', 'long_put_theta_entry',
5860
6039
 
5861
- # 7. ENTRY CRITERIA (12)
6040
+ # 8. ENTRY CRITERIA (15) - Added Iron Condor signals
5862
6041
  'target_delta_entry', 'delta_threshold_entry',
5863
6042
  'entry_price_pct', 'distance_from_strike_entry',
5864
6043
  'dte_entry', 'target_dte_entry',
5865
6044
  'volume_entry', 'open_interest_entry', 'volume_ratio_entry',
5866
6045
  'entry_criteria', 'entry_signal', 'entry_reason',
6046
+ # ✅ Iron Condor strategy signals
6047
+ 'entry_iv_rank', 'entry_signal_type', 'entry_wing_width',
5867
6048
 
5868
- # 8. STOP-LOSS (2)
6049
+ # 9. STOP-LOSS (2)
5869
6050
  'stop_threshold', 'actual_value',
5870
6051
 
5871
- # 9. EXIT PRICES (11) - Added exit_price first
6052
+ # 10. EXIT PRICES (19) - Added Iron Condor 4 legs
5872
6053
  'exit_price', 'underlying_exit_price', 'underlying_change_pct',
5873
6054
  'call_exit_bid', 'call_exit_ask',
5874
6055
  'put_exit_bid', 'put_exit_ask',
5875
6056
  'short_exit_bid', 'short_exit_ask',
5876
6057
  'long_exit_bid', 'long_exit_ask',
6058
+ # ✅ Iron Condor exit prices (4 legs × 2 prices)
6059
+ 'short_call_exit_bid', 'short_call_exit_ask',
6060
+ 'long_call_exit_bid', 'long_call_exit_ask',
6061
+ 'short_put_exit_bid', 'short_put_exit_ask',
6062
+ 'long_put_exit_bid', 'long_put_exit_ask',
5877
6063
 
5878
- # 10. EXIT METRICS (8)
6064
+ # 11. EXIT METRICS (12) - Added Iron Condor IV
5879
6065
  'exit_z_score', 'exit_lean', 'iv_lean_exit',
5880
6066
  'call_iv_exit', 'put_iv_exit', 'iv_exit',
5881
6067
  'iv_rank_exit', 'iv_percentile_exit',
6068
+ # ✅ Iron Condor exit IV (4 legs)
6069
+ 'short_call_iv_exit', 'long_call_iv_exit',
6070
+ 'short_put_iv_exit', 'long_put_iv_exit',
5882
6071
 
5883
- # 11. EXIT GREEKS (12)
6072
+ # 12. EXIT GREEKS (28) - Added Iron Condor Greeks
5884
6073
  'call_delta_exit', 'call_gamma_exit', 'call_vega_exit', 'call_theta_exit',
5885
6074
  'put_delta_exit', 'put_gamma_exit', 'put_vega_exit', 'put_theta_exit',
5886
6075
  'net_delta_exit', 'net_gamma_exit', 'net_vega_exit', 'net_theta_exit',
6076
+ # ✅ Iron Condor exit Greeks (4 legs × 4 greeks)
6077
+ 'short_call_delta_exit', 'short_call_gamma_exit', 'short_call_vega_exit', 'short_call_theta_exit',
6078
+ 'long_call_delta_exit', 'long_call_gamma_exit', 'long_call_vega_exit', 'long_call_theta_exit',
6079
+ 'short_put_delta_exit', 'short_put_gamma_exit', 'short_put_vega_exit', 'short_put_theta_exit',
6080
+ 'long_put_delta_exit', 'long_put_gamma_exit', 'long_put_vega_exit', 'long_put_theta_exit',
5887
6081
 
5888
- # 12. EXIT CRITERIA (11)
6082
+ # 13. EXIT CRITERIA (11)
5889
6083
  'target_delta_exit', 'delta_threshold_exit',
5890
6084
  'exit_price_pct', 'distance_from_strike_exit',
5891
6085
  'dte_exit', 'target_dte_exit',
5892
6086
  'volume_exit', 'open_interest_exit', 'volume_ratio_exit',
5893
6087
  'exit_criteria', 'exit_signal',
5894
6088
 
5895
- # 13. INTRADAY DATA (18)
6089
+ # 14. INTRADAY DATA (18)
5896
6090
  'stock_intraday_high', 'stock_intraday_low', 'stock_intraday_close',
5897
6091
  'stock_stop_trigger_time', 'stock_stop_trigger_price',
5898
6092
  'stock_stop_trigger_bid', 'stock_stop_trigger_ask', 'stock_stop_trigger_last',
@@ -5901,8 +6095,9 @@ OPTIMAL_COLUMN_ORDER = [
5901
6095
  'intraday_bar_index', 'intraday_volume',
5902
6096
  'intraday_trigger_bid_time', 'intraday_trigger_ask_time',
5903
6097
 
5904
- # 14. ADDITIONAL FIELDS (for compatibility)
6098
+ # 15. ADDITIONAL FIELDS (for compatibility)
5905
6099
  'is_short_bias',
6100
+ 'underlying_price', # ✅ Final underlying price
5906
6101
  ]
5907
6102
 
5908
6103
 
@@ -7752,14 +7947,15 @@ def precalculate_indicators_from_config(config, preloaded_data, param_grid=None)
7752
7947
 
7753
7948
  def build_indicator_lookup(indicator_cache, config):
7754
7949
  """
7755
- Creates unified dict for fast access to ALL indicators by date
7950
+ Creates unified dict for fast access to ALL indicators by (symbol, date)
7951
+ Supports multi-symbol data (if 'symbol' column present in indicator DataFrames)
7756
7952
 
7757
7953
  Args:
7758
7954
  indicator_cache: Dict from precalculate_indicators_from_config()
7759
7955
  config: Strategy config
7760
7956
 
7761
7957
  Returns:
7762
- dict: {date: {'z_score': 2.1, 'iv_rank': 45.2, ...}}
7958
+ dict: {(symbol, date): {'iv_rank': 45.2, ...}} or {date: {...}} for single-symbol
7763
7959
  """
7764
7960
  strategy_type = config.get('strategy_type')
7765
7961
  strategy = STRATEGIES.get(strategy_type)
@@ -7768,7 +7964,13 @@ def build_indicator_lookup(indicator_cache, config):
7768
7964
  return {}
7769
7965
 
7770
7966
  # Unified lookup
7771
- by_date = {}
7967
+ by_key = {}
7968
+
7969
+ if config.get('debuginfo', 0) >= 1:
7970
+ print(f"\n[build_indicator_lookup] Building lookup for {len(strategy['indicators'])} indicators...")
7971
+ print(f" Cache contains {len(indicator_cache)} entries:")
7972
+ for cache_key in indicator_cache.keys():
7973
+ print(f" {cache_key}")
7772
7974
 
7773
7975
  for indicator_spec in strategy['indicators']:
7774
7976
  indicator_name = indicator_spec['name']
@@ -7778,6 +7980,9 @@ def build_indicator_lookup(indicator_cache, config):
7778
7980
  for param_name in indicator_spec.get('params_from_config', []):
7779
7981
  if param_name in config:
7780
7982
  params[param_name] = config[param_name]
7983
+ elif param_name == 'lookback_period':
7984
+ # Auto-calculate from lookback_ratio
7985
+ params[param_name] = auto_calculate_lookback_period(config, indicator_name)
7781
7986
 
7782
7987
  # Add optional params
7783
7988
  registry_entry = INDICATOR_REGISTRY.get(indicator_name, {})
@@ -7787,23 +7992,35 @@ def build_indicator_lookup(indicator_cache, config):
7787
7992
  cache_key_params = tuple(params.get(p) for p in registry_entry.get('cache_key_params', []))
7788
7993
  cache_key = (indicator_name, cache_key_params)
7789
7994
 
7995
+ # Debug
7996
+ if config.get('debuginfo', 0) >= 2:
7997
+ print(f"[build_indicator_lookup] Looking for {indicator_name} with key: {cache_key}")
7998
+ print(f" Available keys in cache: {list(indicator_cache.keys())}")
7999
+
7790
8000
  # Find in cache
7791
8001
  indicator_df = indicator_cache.get(cache_key)
7792
8002
  if indicator_df is None or indicator_df.empty:
8003
+ if config.get('debuginfo', 0) >= 1:
8004
+ print(f"⚠️ Indicator '{indicator_name}' not found in cache (key: {cache_key})")
7793
8005
  continue
7794
8006
 
7795
8007
  # Add all fields from this indicator
7796
8008
  for _, row in indicator_df.iterrows():
7797
8009
  date = row['date']
7798
- if date not in by_date:
7799
- by_date[date] = {}
8010
+ symbol = row.get('symbol', None)
8011
+
8012
+ # Create key: (symbol, date) for multi-symbol, date for single-symbol
8013
+ key = (symbol, date) if symbol is not None else date
8014
+
8015
+ if key not in by_key:
8016
+ by_key[key] = {}
7800
8017
 
7801
- # Add all output fields
8018
+ # Add all output fields (exclude 'date' and 'symbol')
7802
8019
  for field in registry_entry.get('outputs', []):
7803
- if field != 'date' and field in row:
7804
- by_date[date][field] = row[field]
8020
+ if field not in ['date', 'symbol'] and field in row:
8021
+ by_key[key][field] = row[field]
7805
8022
 
7806
- return by_date
8023
+ return by_key
7807
8024
 
7808
8025
 
7809
8026
  # ============================================================
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ivolatility_backtesting
3
- Version: 1.28
3
+ Version: 1.29
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.28"
7
+ version = "1.29"
8
8
  description = "A universal backtesting framework for financial strategies using the IVolatility API."
9
9
  readme = "README.md"
10
10
  authors = [