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.
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.29}/PKG-INFO +1 -1
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.29}/ivolatility_backtesting/ivolatility_backtesting.py +259 -42
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.29}/ivolatility_backtesting.egg-info/PKG-INFO +1 -1
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.29}/pyproject.toml +1 -1
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.29}/LICENSE +0 -0
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.29}/README.md +0 -0
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.29}/ivolatility_backtesting/__init__.py +0 -0
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.29}/ivolatility_backtesting.egg-info/SOURCES.txt +0 -0
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.29}/ivolatility_backtesting.egg-info/dependency_links.txt +0 -0
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.29}/ivolatility_backtesting.egg-info/requires.txt +0 -0
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.29}/ivolatility_backtesting.egg-info/top_level.txt +0 -0
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.29}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ivolatility_backtesting
|
|
3
|
-
Version: 1.
|
|
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': '
|
|
119
|
+
'name': 'iv_rank_ivx', # ✅ NEW: Uses IVX data (no lookback needed!)
|
|
120
120
|
'required': False,
|
|
121
|
-
'params_from_config': [
|
|
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
|
-
|
|
964
|
-
|
|
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
|
-
|
|
968
|
-
|
|
969
|
-
iv_df['iv_rank'] =
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
1940
|
-
return (strike * 100 + total_cost
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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.
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
6049
|
+
# 9. STOP-LOSS (2)
|
|
5869
6050
|
'stop_threshold', 'actual_value',
|
|
5870
6051
|
|
|
5871
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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: {'
|
|
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
|
-
|
|
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
|
-
|
|
7799
|
-
|
|
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
|
|
7804
|
-
|
|
8020
|
+
if field not in ['date', 'symbol'] and field in row:
|
|
8021
|
+
by_key[key][field] = row[field]
|
|
7805
8022
|
|
|
7806
|
-
return
|
|
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.
|
|
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.
|
|
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 = [
|
|
File without changes
|
|
File without changes
|
{ivolatility_backtesting-1.28 → ivolatility_backtesting-1.29}/ivolatility_backtesting/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|