ivolatility-backtesting 1.28__tar.gz → 1.30__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.30}/PKG-INFO +1 -1
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.30}/ivolatility_backtesting/ivolatility_backtesting.py +274 -42
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.30}/ivolatility_backtesting.egg-info/PKG-INFO +1 -1
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.30}/pyproject.toml +1 -1
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.30}/LICENSE +0 -0
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.30}/README.md +0 -0
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.30}/ivolatility_backtesting/__init__.py +0 -0
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.30}/ivolatility_backtesting.egg-info/SOURCES.txt +0 -0
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.30}/ivolatility_backtesting.egg-info/dependency_links.txt +0 -0
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.30}/ivolatility_backtesting.egg-info/requires.txt +0 -0
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.30}/ivolatility_backtesting.egg-info/top_level.txt +0 -0
- {ivolatility_backtesting-1.28 → ivolatility_backtesting-1.30}/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.30
|
|
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',
|
|
@@ -4317,6 +4472,16 @@ class PositionManager:
|
|
|
4317
4472
|
if key in kwargs:
|
|
4318
4473
|
trade[key] = kwargs[key]
|
|
4319
4474
|
|
|
4475
|
+
# ========================================================
|
|
4476
|
+
# AUTO-COPY ALL CUSTOM ENTRY FIELDS
|
|
4477
|
+
# ========================================================
|
|
4478
|
+
# Universal mechanism: copy ANY field with 'entry' from position to trade
|
|
4479
|
+
# This allows strategies to add custom fields without modifying the framework
|
|
4480
|
+
# Matches: entry_*, *_entry, *entry* (e.g., entry_earnings_date, long_delta_entry, etc.)
|
|
4481
|
+
for key in position.keys():
|
|
4482
|
+
if 'entry' in key and key not in trade:
|
|
4483
|
+
trade[key] = position[key]
|
|
4484
|
+
|
|
4320
4485
|
self.closed_trades.append(trade)
|
|
4321
4486
|
|
|
4322
4487
|
if self.sl_enabled and self.sl_manager:
|
|
@@ -5824,75 +5989,119 @@ def create_stoploss_charts(analyzer, filename='stoploss_analysis.png', show_plot
|
|
|
5824
5989
|
# RESULTS EXPORTER (unchanged)
|
|
5825
5990
|
# ============================================================
|
|
5826
5991
|
# ============================================================
|
|
5827
|
-
# OPTIMAL COLUMN ORDER (
|
|
5992
|
+
# OPTIMAL COLUMN ORDER (150+ columns) - ✅ Added Iron Condor support
|
|
5828
5993
|
# ============================================================
|
|
5829
5994
|
OPTIMAL_COLUMN_ORDER = [
|
|
5830
5995
|
# 1. IDENTIFIERS (4)
|
|
5831
5996
|
'entry_date', 'exit_date', 'symbol', 'signal',
|
|
5832
5997
|
|
|
5833
|
-
# 2. RESULTS (
|
|
5998
|
+
# 2. RESULTS (6) - Added type and pnl_pct
|
|
5834
5999
|
'pnl', 'return_pct', 'exit_reason', 'stop_type',
|
|
6000
|
+
'type', # ✅ BUY_IRON_CONDOR / SELL_IRON_CONDOR
|
|
6001
|
+
'pnl_pct', # ✅ P&L percentage (same as return_pct for compatibility)
|
|
5835
6002
|
|
|
5836
|
-
# 3. OPTION PARAMETERS (
|
|
6003
|
+
# 3. OPTION PARAMETERS (18) - Added Iron Condor strikes
|
|
5837
6004
|
'strike', 'call_strike', 'put_strike',
|
|
5838
6005
|
'expiration', 'call_expiration', 'put_expiration',
|
|
5839
6006
|
'contracts', 'quantity',
|
|
5840
6007
|
'short_strike', 'long_strike',
|
|
5841
6008
|
'short_expiration', 'long_expiration',
|
|
5842
6009
|
'opt_type', 'spread_type',
|
|
6010
|
+
# ✅ Iron Condor strikes (4 legs)
|
|
6011
|
+
'short_call_strike', 'long_call_strike',
|
|
6012
|
+
'short_put_strike', 'long_put_strike',
|
|
6013
|
+
|
|
6014
|
+
# 4. POSITION METADATA (12) - ✅ Iron Condor specific
|
|
6015
|
+
'dte', 'position_size_pct', 'total_cost', 'strategy_type',
|
|
6016
|
+
'capital_at_entry', 'target_allocation', 'actual_allocation',
|
|
6017
|
+
'available_equity_at_entry', 'locked_capital_at_entry', 'open_positions_at_entry',
|
|
6018
|
+
'highest_price', 'lowest_price',
|
|
5843
6019
|
|
|
5844
|
-
#
|
|
6020
|
+
# 5. ENTRY PRICES (25) - Added Iron Condor 4 legs
|
|
5845
6021
|
'entry_price', 'underlying_entry_price',
|
|
5846
6022
|
'call_entry_bid', 'call_entry_ask', 'call_entry_mid',
|
|
5847
6023
|
'put_entry_bid', 'put_entry_ask', 'put_entry_mid',
|
|
5848
6024
|
'short_entry_bid', 'short_entry_ask', 'short_entry_mid',
|
|
5849
6025
|
'long_entry_bid', 'long_entry_ask', 'long_entry_mid',
|
|
6026
|
+
# ✅ Iron Condor entry prices (4 legs × 3 prices)
|
|
6027
|
+
'short_call_entry_bid', 'short_call_entry_ask', 'short_call_entry_mid',
|
|
6028
|
+
'long_call_entry_bid', 'long_call_entry_ask', 'long_call_entry_mid',
|
|
6029
|
+
'short_put_entry_bid', 'short_put_entry_ask', 'short_put_entry_mid',
|
|
6030
|
+
'long_put_entry_bid', 'long_put_entry_ask', 'long_put_entry_mid',
|
|
5850
6031
|
|
|
5851
|
-
#
|
|
6032
|
+
# 6. ENTRY METRICS (15) - Added Iron Condor IV & Spread Greeks
|
|
5852
6033
|
'entry_z_score', 'entry_lean', 'iv_lean_entry',
|
|
5853
6034
|
'call_iv_entry', 'put_iv_entry', 'iv_entry',
|
|
5854
6035
|
'iv_rank_entry', 'iv_percentile_entry',
|
|
6036
|
+
# ✅ Spread strategies (BULL_CALL_SPREAD, BEAR_PUT_SPREAD, etc.)
|
|
6037
|
+
'long_delta_entry', 'short_delta_entry', 'long_iv_entry', 'short_iv_entry',
|
|
6038
|
+
# ✅ Iron Condor entry IV (4 legs)
|
|
6039
|
+
'short_call_iv_entry', 'long_call_iv_entry',
|
|
6040
|
+
'short_put_iv_entry', 'long_put_iv_entry',
|
|
5855
6041
|
|
|
5856
|
-
#
|
|
6042
|
+
# 7. ENTRY GREEKS (28) - Added Iron Condor Greeks
|
|
5857
6043
|
'call_delta_entry', 'call_gamma_entry', 'call_vega_entry', 'call_theta_entry',
|
|
5858
6044
|
'put_delta_entry', 'put_gamma_entry', 'put_vega_entry', 'put_theta_entry',
|
|
5859
6045
|
'net_delta_entry', 'net_gamma_entry', 'net_vega_entry', 'net_theta_entry',
|
|
6046
|
+
# ✅ Iron Condor entry Greeks (4 legs × 4 greeks)
|
|
6047
|
+
'short_call_delta_entry', 'short_call_gamma_entry', 'short_call_vega_entry', 'short_call_theta_entry',
|
|
6048
|
+
'long_call_delta_entry', 'long_call_gamma_entry', 'long_call_vega_entry', 'long_call_theta_entry',
|
|
6049
|
+
'short_put_delta_entry', 'short_put_gamma_entry', 'short_put_vega_entry', 'short_put_theta_entry',
|
|
6050
|
+
'long_put_delta_entry', 'long_put_gamma_entry', 'long_put_vega_entry', 'long_put_theta_entry',
|
|
5860
6051
|
|
|
5861
|
-
#
|
|
6052
|
+
# 8. ENTRY CRITERIA (20) - Added Iron Condor signals & Earnings data
|
|
5862
6053
|
'target_delta_entry', 'delta_threshold_entry',
|
|
5863
6054
|
'entry_price_pct', 'distance_from_strike_entry',
|
|
5864
6055
|
'dte_entry', 'target_dte_entry',
|
|
5865
6056
|
'volume_entry', 'open_interest_entry', 'volume_ratio_entry',
|
|
5866
6057
|
'entry_criteria', 'entry_signal', 'entry_reason',
|
|
6058
|
+
# ✅ Iron Condor strategy signals
|
|
6059
|
+
'entry_iv_rank', 'entry_signal_type', 'entry_wing_width',
|
|
6060
|
+
# ✅ Earnings Momentum strategy data
|
|
6061
|
+
'entry_earnings_date', 'entry_earnings_surprise_pct', 'entry_earnings_estimate',
|
|
6062
|
+
'entry_earnings_reported', 'entry_earnings_direction',
|
|
5867
6063
|
|
|
5868
|
-
#
|
|
6064
|
+
# 9. STOP-LOSS (2)
|
|
5869
6065
|
'stop_threshold', 'actual_value',
|
|
5870
6066
|
|
|
5871
|
-
#
|
|
6067
|
+
# 10. EXIT PRICES (19) - Added Iron Condor 4 legs
|
|
5872
6068
|
'exit_price', 'underlying_exit_price', 'underlying_change_pct',
|
|
5873
6069
|
'call_exit_bid', 'call_exit_ask',
|
|
5874
6070
|
'put_exit_bid', 'put_exit_ask',
|
|
5875
6071
|
'short_exit_bid', 'short_exit_ask',
|
|
5876
6072
|
'long_exit_bid', 'long_exit_ask',
|
|
6073
|
+
# ✅ Iron Condor exit prices (4 legs × 2 prices)
|
|
6074
|
+
'short_call_exit_bid', 'short_call_exit_ask',
|
|
6075
|
+
'long_call_exit_bid', 'long_call_exit_ask',
|
|
6076
|
+
'short_put_exit_bid', 'short_put_exit_ask',
|
|
6077
|
+
'long_put_exit_bid', 'long_put_exit_ask',
|
|
5877
6078
|
|
|
5878
|
-
#
|
|
6079
|
+
# 11. EXIT METRICS (12) - Added Iron Condor IV
|
|
5879
6080
|
'exit_z_score', 'exit_lean', 'iv_lean_exit',
|
|
5880
6081
|
'call_iv_exit', 'put_iv_exit', 'iv_exit',
|
|
5881
6082
|
'iv_rank_exit', 'iv_percentile_exit',
|
|
6083
|
+
# ✅ Iron Condor exit IV (4 legs)
|
|
6084
|
+
'short_call_iv_exit', 'long_call_iv_exit',
|
|
6085
|
+
'short_put_iv_exit', 'long_put_iv_exit',
|
|
5882
6086
|
|
|
5883
|
-
#
|
|
6087
|
+
# 12. EXIT GREEKS (28) - Added Iron Condor Greeks
|
|
5884
6088
|
'call_delta_exit', 'call_gamma_exit', 'call_vega_exit', 'call_theta_exit',
|
|
5885
6089
|
'put_delta_exit', 'put_gamma_exit', 'put_vega_exit', 'put_theta_exit',
|
|
5886
6090
|
'net_delta_exit', 'net_gamma_exit', 'net_vega_exit', 'net_theta_exit',
|
|
6091
|
+
# ✅ Iron Condor exit Greeks (4 legs × 4 greeks)
|
|
6092
|
+
'short_call_delta_exit', 'short_call_gamma_exit', 'short_call_vega_exit', 'short_call_theta_exit',
|
|
6093
|
+
'long_call_delta_exit', 'long_call_gamma_exit', 'long_call_vega_exit', 'long_call_theta_exit',
|
|
6094
|
+
'short_put_delta_exit', 'short_put_gamma_exit', 'short_put_vega_exit', 'short_put_theta_exit',
|
|
6095
|
+
'long_put_delta_exit', 'long_put_gamma_exit', 'long_put_vega_exit', 'long_put_theta_exit',
|
|
5887
6096
|
|
|
5888
|
-
#
|
|
6097
|
+
# 13. EXIT CRITERIA (11)
|
|
5889
6098
|
'target_delta_exit', 'delta_threshold_exit',
|
|
5890
6099
|
'exit_price_pct', 'distance_from_strike_exit',
|
|
5891
6100
|
'dte_exit', 'target_dte_exit',
|
|
5892
6101
|
'volume_exit', 'open_interest_exit', 'volume_ratio_exit',
|
|
5893
6102
|
'exit_criteria', 'exit_signal',
|
|
5894
6103
|
|
|
5895
|
-
#
|
|
6104
|
+
# 14. INTRADAY DATA (18)
|
|
5896
6105
|
'stock_intraday_high', 'stock_intraday_low', 'stock_intraday_close',
|
|
5897
6106
|
'stock_stop_trigger_time', 'stock_stop_trigger_price',
|
|
5898
6107
|
'stock_stop_trigger_bid', 'stock_stop_trigger_ask', 'stock_stop_trigger_last',
|
|
@@ -5901,8 +6110,9 @@ OPTIMAL_COLUMN_ORDER = [
|
|
|
5901
6110
|
'intraday_bar_index', 'intraday_volume',
|
|
5902
6111
|
'intraday_trigger_bid_time', 'intraday_trigger_ask_time',
|
|
5903
6112
|
|
|
5904
|
-
#
|
|
6113
|
+
# 15. ADDITIONAL FIELDS (for compatibility)
|
|
5905
6114
|
'is_short_bias',
|
|
6115
|
+
'underlying_price', # ✅ Final underlying price
|
|
5906
6116
|
]
|
|
5907
6117
|
|
|
5908
6118
|
|
|
@@ -7752,14 +7962,15 @@ def precalculate_indicators_from_config(config, preloaded_data, param_grid=None)
|
|
|
7752
7962
|
|
|
7753
7963
|
def build_indicator_lookup(indicator_cache, config):
|
|
7754
7964
|
"""
|
|
7755
|
-
Creates unified dict for fast access to ALL indicators by date
|
|
7965
|
+
Creates unified dict for fast access to ALL indicators by (symbol, date)
|
|
7966
|
+
Supports multi-symbol data (if 'symbol' column present in indicator DataFrames)
|
|
7756
7967
|
|
|
7757
7968
|
Args:
|
|
7758
7969
|
indicator_cache: Dict from precalculate_indicators_from_config()
|
|
7759
7970
|
config: Strategy config
|
|
7760
7971
|
|
|
7761
7972
|
Returns:
|
|
7762
|
-
dict: {date: {'
|
|
7973
|
+
dict: {(symbol, date): {'iv_rank': 45.2, ...}} or {date: {...}} for single-symbol
|
|
7763
7974
|
"""
|
|
7764
7975
|
strategy_type = config.get('strategy_type')
|
|
7765
7976
|
strategy = STRATEGIES.get(strategy_type)
|
|
@@ -7768,7 +7979,13 @@ def build_indicator_lookup(indicator_cache, config):
|
|
|
7768
7979
|
return {}
|
|
7769
7980
|
|
|
7770
7981
|
# Unified lookup
|
|
7771
|
-
|
|
7982
|
+
by_key = {}
|
|
7983
|
+
|
|
7984
|
+
if config.get('debuginfo', 0) >= 1:
|
|
7985
|
+
print(f"\n[build_indicator_lookup] Building lookup for {len(strategy['indicators'])} indicators...")
|
|
7986
|
+
print(f" Cache contains {len(indicator_cache)} entries:")
|
|
7987
|
+
for cache_key in indicator_cache.keys():
|
|
7988
|
+
print(f" {cache_key}")
|
|
7772
7989
|
|
|
7773
7990
|
for indicator_spec in strategy['indicators']:
|
|
7774
7991
|
indicator_name = indicator_spec['name']
|
|
@@ -7778,6 +7995,9 @@ def build_indicator_lookup(indicator_cache, config):
|
|
|
7778
7995
|
for param_name in indicator_spec.get('params_from_config', []):
|
|
7779
7996
|
if param_name in config:
|
|
7780
7997
|
params[param_name] = config[param_name]
|
|
7998
|
+
elif param_name == 'lookback_period':
|
|
7999
|
+
# Auto-calculate from lookback_ratio
|
|
8000
|
+
params[param_name] = auto_calculate_lookback_period(config, indicator_name)
|
|
7781
8001
|
|
|
7782
8002
|
# Add optional params
|
|
7783
8003
|
registry_entry = INDICATOR_REGISTRY.get(indicator_name, {})
|
|
@@ -7787,23 +8007,35 @@ def build_indicator_lookup(indicator_cache, config):
|
|
|
7787
8007
|
cache_key_params = tuple(params.get(p) for p in registry_entry.get('cache_key_params', []))
|
|
7788
8008
|
cache_key = (indicator_name, cache_key_params)
|
|
7789
8009
|
|
|
8010
|
+
# Debug
|
|
8011
|
+
if config.get('debuginfo', 0) >= 2:
|
|
8012
|
+
print(f"[build_indicator_lookup] Looking for {indicator_name} with key: {cache_key}")
|
|
8013
|
+
print(f" Available keys in cache: {list(indicator_cache.keys())}")
|
|
8014
|
+
|
|
7790
8015
|
# Find in cache
|
|
7791
8016
|
indicator_df = indicator_cache.get(cache_key)
|
|
7792
8017
|
if indicator_df is None or indicator_df.empty:
|
|
8018
|
+
if config.get('debuginfo', 0) >= 1:
|
|
8019
|
+
print(f"⚠️ Indicator '{indicator_name}' not found in cache (key: {cache_key})")
|
|
7793
8020
|
continue
|
|
7794
8021
|
|
|
7795
8022
|
# Add all fields from this indicator
|
|
7796
8023
|
for _, row in indicator_df.iterrows():
|
|
7797
8024
|
date = row['date']
|
|
7798
|
-
|
|
7799
|
-
|
|
8025
|
+
symbol = row.get('symbol', None)
|
|
8026
|
+
|
|
8027
|
+
# Create key: (symbol, date) for multi-symbol, date for single-symbol
|
|
8028
|
+
key = (symbol, date) if symbol is not None else date
|
|
8029
|
+
|
|
8030
|
+
if key not in by_key:
|
|
8031
|
+
by_key[key] = {}
|
|
7800
8032
|
|
|
7801
|
-
# Add all output fields
|
|
8033
|
+
# Add all output fields (exclude 'date' and 'symbol')
|
|
7802
8034
|
for field in registry_entry.get('outputs', []):
|
|
7803
|
-
if field
|
|
7804
|
-
|
|
8035
|
+
if field not in ['date', 'symbol'] and field in row:
|
|
8036
|
+
by_key[key][field] = row[field]
|
|
7805
8037
|
|
|
7806
|
-
return
|
|
8038
|
+
return by_key
|
|
7807
8039
|
|
|
7808
8040
|
|
|
7809
8041
|
# ============================================================
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ivolatility_backtesting
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.30
|
|
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.30"
|
|
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.30}/ivolatility_backtesting/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|