ivolatility-backtesting 1.35__tar.gz → 1.36__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.35 → ivolatility_backtesting-1.36}/PKG-INFO +1 -1
- {ivolatility_backtesting-1.35 → ivolatility_backtesting-1.36}/ivolatility_backtesting/__init__.py +4 -2
- {ivolatility_backtesting-1.35 → ivolatility_backtesting-1.36}/ivolatility_backtesting/ivolatility_backtesting.py +597 -8
- {ivolatility_backtesting-1.35 → ivolatility_backtesting-1.36}/ivolatility_backtesting.egg-info/PKG-INFO +1 -1
- {ivolatility_backtesting-1.35 → ivolatility_backtesting-1.36}/pyproject.toml +1 -1
- {ivolatility_backtesting-1.35 → ivolatility_backtesting-1.36}/LICENSE +0 -0
- {ivolatility_backtesting-1.35 → ivolatility_backtesting-1.36}/README.md +0 -0
- {ivolatility_backtesting-1.35 → ivolatility_backtesting-1.36}/ivolatility_backtesting.egg-info/SOURCES.txt +0 -0
- {ivolatility_backtesting-1.35 → ivolatility_backtesting-1.36}/ivolatility_backtesting.egg-info/dependency_links.txt +0 -0
- {ivolatility_backtesting-1.35 → ivolatility_backtesting-1.36}/ivolatility_backtesting.egg-info/requires.txt +0 -0
- {ivolatility_backtesting-1.35 → ivolatility_backtesting-1.36}/ivolatility_backtesting.egg-info/top_level.txt +0 -0
- {ivolatility_backtesting-1.35 → ivolatility_backtesting-1.36}/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.36
|
|
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
|
{ivolatility_backtesting-1.35 → ivolatility_backtesting-1.36}/ivolatility_backtesting/__init__.py
RENAMED
|
@@ -19,7 +19,8 @@ from .ivolatility_backtesting import (
|
|
|
19
19
|
_process_options_df,
|
|
20
20
|
precalculate_indicators_from_config, build_indicator_lookup,
|
|
21
21
|
INDICATOR_REGISTRY, auto_calculate_lookback_period,
|
|
22
|
-
calculate_iv_lean_from_ivx, preload_ivx_zscore_cache
|
|
22
|
+
calculate_iv_lean_from_ivx, preload_ivx_zscore_cache,
|
|
23
|
+
calculate_iv_percentile_from_ivx # For earnings strategies
|
|
23
24
|
)
|
|
24
25
|
|
|
25
26
|
__all__ = [
|
|
@@ -45,5 +46,6 @@ __all__ = [
|
|
|
45
46
|
'precalculate_indicators_from_config', 'build_indicator_lookup',
|
|
46
47
|
'INDICATOR_REGISTRY', 'auto_calculate_lookback_period',
|
|
47
48
|
# IV Lean / Z-Score functions
|
|
48
|
-
'calculate_iv_lean_from_ivx', 'preload_ivx_zscore_cache'
|
|
49
|
+
'calculate_iv_lean_from_ivx', 'preload_ivx_zscore_cache',
|
|
50
|
+
'calculate_iv_percentile_from_ivx' # For earnings strategies
|
|
49
51
|
]
|
|
@@ -2365,7 +2365,9 @@ class StrategyRegistry:
|
|
|
2365
2365
|
strategy = cls.get(strategy_type)
|
|
2366
2366
|
|
|
2367
2367
|
if not strategy or 'legs' not in strategy:
|
|
2368
|
-
|
|
2368
|
+
# Fallback: filter out fields passed explicitly to close_position()
|
|
2369
|
+
excluded = ['pnl', 'pnl_pct', 'price', 'exit_reason', 'close_reason', 'position_id']
|
|
2370
|
+
return {k: v for k, v in pos_data.items() if k not in excluded}
|
|
2369
2371
|
|
|
2370
2372
|
kwargs = {}
|
|
2371
2373
|
|
|
@@ -3023,7 +3025,8 @@ class StrategyRegistry:
|
|
|
3023
3025
|
if not strategy or 'legs' not in strategy:
|
|
3024
3026
|
# Fallback: return all pos_data (for backward compatibility)
|
|
3025
3027
|
# BUT exclude fields that are ALWAYS passed explicitly to close_position()
|
|
3026
|
-
|
|
3028
|
+
excluded_fields = ['pnl', 'pnl_pct', 'price', 'exit_reason', 'close_reason', 'position_id']
|
|
3029
|
+
kwargs = {k: v for k, v in pos_data.items() if k not in excluded_fields}
|
|
3027
3030
|
return kwargs
|
|
3028
3031
|
|
|
3029
3032
|
kwargs = {}
|
|
@@ -4842,6 +4845,39 @@ class PositionManager:
|
|
|
4842
4845
|
self.sl_config = None
|
|
4843
4846
|
self.sl_manager = None
|
|
4844
4847
|
|
|
4848
|
+
# ================================================================
|
|
4849
|
+
# ENTRY COST CALCULATION (wrapper for StrategyRegistry)
|
|
4850
|
+
# ================================================================
|
|
4851
|
+
def calculate_entry_cost(self, leg_data, contracts=1, is_short=None):
|
|
4852
|
+
"""
|
|
4853
|
+
Calculate entry cost for current strategy (uses strategy_type from config).
|
|
4854
|
+
|
|
4855
|
+
This is a convenience wrapper around StrategyRegistry.calculate_entry_cost()
|
|
4856
|
+
that automatically uses strategy_type from self.config.
|
|
4857
|
+
|
|
4858
|
+
Args:
|
|
4859
|
+
leg_data: Dict with option data at entry
|
|
4860
|
+
contracts: Number of contracts (default=1 for per-contract calculation)
|
|
4861
|
+
is_short: Override for SHORT detection (True = sell to open, False = buy to open)
|
|
4862
|
+
Required for strategies like STRADDLE/IV_LEAN where leg names don't have 'short_' prefix
|
|
4863
|
+
|
|
4864
|
+
Returns:
|
|
4865
|
+
dict: {
|
|
4866
|
+
'total': Total cost (positive for DEBIT, negative for CREDIT),
|
|
4867
|
+
'per_leg': {leg_name: cost, ...},
|
|
4868
|
+
'net_credit': bool,
|
|
4869
|
+
}
|
|
4870
|
+
|
|
4871
|
+
Example:
|
|
4872
|
+
entry_cost = position_mgr.calculate_entry_cost(
|
|
4873
|
+
{'call': call_data, 'put': put_data},
|
|
4874
|
+
contracts=2,
|
|
4875
|
+
is_short=True
|
|
4876
|
+
)
|
|
4877
|
+
"""
|
|
4878
|
+
strategy_type = self.config.get('strategy_type', 'STRADDLE')
|
|
4879
|
+
return StrategyRegistry.calculate_entry_cost(strategy_type, leg_data, contracts, is_short)
|
|
4880
|
+
|
|
4845
4881
|
# ================================================================
|
|
4846
4882
|
# UNIVERSAL INTRINSIC VALUE CALCULATION METHOD
|
|
4847
4883
|
# ================================================================
|
|
@@ -5500,16 +5536,26 @@ class PositionManager:
|
|
|
5500
5536
|
# Determine stop_type: 'expiration' (market data) or 'expiration_intrinsic' (no data)
|
|
5501
5537
|
stop_type = 'expiration_intrinsic' if used_intrinsic else 'expiration'
|
|
5502
5538
|
|
|
5503
|
-
|
|
5539
|
+
# ✅ AUTO-GENERATE close kwargs (so strategy code doesn't need to call generate_close_position_kwargs)
|
|
5540
|
+
strategy_type = position.get('strategy_type', 'STRADDLE')
|
|
5541
|
+
pos_data = price_data.get(position_id, {})
|
|
5542
|
+
close_kwargs = StrategyRegistry.generate_close_position_kwargs(strategy_type, pos_data)
|
|
5543
|
+
|
|
5544
|
+
stop_info = {
|
|
5504
5545
|
'position_id': position_id,
|
|
5505
5546
|
'symbol': position['symbol'],
|
|
5506
5547
|
'stop_type': stop_type,
|
|
5548
|
+
'close_reason': stop_type, # ✅ For strategy stats
|
|
5549
|
+
'stat_key': 'expiration_exits', # ✅ For strategy stats
|
|
5507
5550
|
'stop_level': None,
|
|
5508
5551
|
'current_price': current_price,
|
|
5509
5552
|
'pnl': current_pnl,
|
|
5510
5553
|
'pnl_pct': current_pnl_pct,
|
|
5511
|
-
'settlement_type': 'intrinsic' if used_intrinsic else 'market'
|
|
5512
|
-
|
|
5554
|
+
'settlement_type': 'intrinsic' if used_intrinsic else 'market',
|
|
5555
|
+
**close_kwargs # ✅ Include leg exit data automatically
|
|
5556
|
+
}
|
|
5557
|
+
|
|
5558
|
+
to_close.append(stop_info)
|
|
5513
5559
|
|
|
5514
5560
|
if self.debug:
|
|
5515
5561
|
print(f"[PositionManager] 📅 EXPIRATION: {position_id} expired on {expiration}")
|
|
@@ -5706,6 +5752,10 @@ class PositionManager:
|
|
|
5706
5752
|
# For other stop types: generic reason
|
|
5707
5753
|
stop_info['exit_reason'] = f"stop_loss_{stop_type}"
|
|
5708
5754
|
|
|
5755
|
+
# ✅ ADD close_reason and stat_key for strategy stats
|
|
5756
|
+
stop_info['close_reason'] = stop_info['exit_reason']
|
|
5757
|
+
stop_info['stat_key'] = 'stoploss_triggered'
|
|
5758
|
+
|
|
5709
5759
|
# ✅ ADD stop-loss metadata for CSV export (stop_threshold, actual_value)
|
|
5710
5760
|
# These fields are needed for detailed analysis of stop-loss triggers
|
|
5711
5761
|
if stop_type == 'pl_loss':
|
|
@@ -5731,6 +5781,12 @@ class PositionManager:
|
|
|
5731
5781
|
# Just merge them into stop_info (no need for second mapping!)
|
|
5732
5782
|
stop_info.update(intraday_data)
|
|
5733
5783
|
|
|
5784
|
+
# ✅ AUTO-GENERATE close kwargs (so strategy code doesn't need to call generate_close_position_kwargs)
|
|
5785
|
+
strategy_type = position.get('strategy_type', 'STRADDLE')
|
|
5786
|
+
pos_data = price_data.get(position_id, {})
|
|
5787
|
+
close_kwargs = StrategyRegistry.generate_close_position_kwargs(strategy_type, pos_data)
|
|
5788
|
+
stop_info.update(close_kwargs)
|
|
5789
|
+
|
|
5734
5790
|
to_close.append(stop_info)
|
|
5735
5791
|
|
|
5736
5792
|
if self.debug:
|
|
@@ -5739,6 +5795,148 @@ class PositionManager:
|
|
|
5739
5795
|
|
|
5740
5796
|
return to_close
|
|
5741
5797
|
|
|
5798
|
+
def build_price_data(self, current_date, stock_price, options_df, get_option_func,
|
|
5799
|
+
indicators=None, extra_fields_callback=None):
|
|
5800
|
+
"""
|
|
5801
|
+
Build price_data dict for all open positions.
|
|
5802
|
+
|
|
5803
|
+
Args:
|
|
5804
|
+
current_date: Current backtest date
|
|
5805
|
+
stock_price: Current underlying price
|
|
5806
|
+
options_df: Options data for current date
|
|
5807
|
+
get_option_func: Function to get option by (strike, exp, type)
|
|
5808
|
+
indicators: Optional dict of indicators for current date
|
|
5809
|
+
All fields will be added to price_data with 'exit_' prefix
|
|
5810
|
+
(e.g., z_score → exit_z_score, iv_percentile → exit_iv_percentile)
|
|
5811
|
+
extra_fields_callback: Optional function(position, price_data) to add custom fields
|
|
5812
|
+
(e.g., directional stop fields, earnings data)
|
|
5813
|
+
|
|
5814
|
+
Returns:
|
|
5815
|
+
Dict[position_id, price_data] for all open positions
|
|
5816
|
+
"""
|
|
5817
|
+
price_data = {}
|
|
5818
|
+
|
|
5819
|
+
for position in self.get_open_positions():
|
|
5820
|
+
# Get leg data from current market prices
|
|
5821
|
+
leg_data = StrategyRegistry.get_leg_data_for_position(
|
|
5822
|
+
position, options_df, get_option_func
|
|
5823
|
+
)
|
|
5824
|
+
|
|
5825
|
+
if leg_data is not None:
|
|
5826
|
+
# Get strategy type
|
|
5827
|
+
strategy_type = position.get('strategy_type', self.config.get('strategy_type', 'STRADDLE'))
|
|
5828
|
+
|
|
5829
|
+
# Auto-generate price_data (framework calculates P&L automatically!)
|
|
5830
|
+
price_data[position['id']] = StrategyRegistry.generate_price_data(
|
|
5831
|
+
strategy_type=strategy_type,
|
|
5832
|
+
leg_data=leg_data,
|
|
5833
|
+
underlying_price=stock_price,
|
|
5834
|
+
position=position
|
|
5835
|
+
)
|
|
5836
|
+
|
|
5837
|
+
# Add ALL indicators with 'exit_' prefix (universal for any strategy)
|
|
5838
|
+
if indicators:
|
|
5839
|
+
for indicator_name, indicator_value in indicators.items():
|
|
5840
|
+
if indicator_value is not None:
|
|
5841
|
+
# Add with exit_ prefix if not already prefixed
|
|
5842
|
+
field_name = f'exit_{indicator_name}' if not indicator_name.startswith('exit_') else indicator_name
|
|
5843
|
+
price_data[position['id']][field_name] = indicator_value
|
|
5844
|
+
|
|
5845
|
+
# Add custom fields via callback (e.g., directional stop fields)
|
|
5846
|
+
if extra_fields_callback:
|
|
5847
|
+
extra_fields = extra_fields_callback(position, price_data[position['id']])
|
|
5848
|
+
if extra_fields:
|
|
5849
|
+
price_data[position['id']].update(extra_fields)
|
|
5850
|
+
|
|
5851
|
+
return price_data
|
|
5852
|
+
|
|
5853
|
+
def close_remaining_positions(self, final_date, stock_price, options_df, get_option_func,
|
|
5854
|
+
indicators=None):
|
|
5855
|
+
"""
|
|
5856
|
+
Close all remaining open positions at end of backtest.
|
|
5857
|
+
|
|
5858
|
+
Args:
|
|
5859
|
+
final_date: Final backtest date
|
|
5860
|
+
stock_price: Final underlying price
|
|
5861
|
+
options_df: Options data for final date
|
|
5862
|
+
get_option_func: Function to get option by (strike, exp, type)
|
|
5863
|
+
indicators: Optional dict of indicators for final date
|
|
5864
|
+
|
|
5865
|
+
Returns:
|
|
5866
|
+
Total P&L from closing positions
|
|
5867
|
+
"""
|
|
5868
|
+
if len(self.get_open_positions()) == 0:
|
|
5869
|
+
return 0
|
|
5870
|
+
|
|
5871
|
+
if self.debug:
|
|
5872
|
+
print(f"\n⚠️ Closing {len(self.get_open_positions())} remaining open positions...")
|
|
5873
|
+
|
|
5874
|
+
# Build price_data for all open positions
|
|
5875
|
+
price_data = self.build_price_data(
|
|
5876
|
+
final_date, stock_price, options_df, get_option_func, indicators
|
|
5877
|
+
)
|
|
5878
|
+
|
|
5879
|
+
# Close all positions
|
|
5880
|
+
self.close_all_positions(final_date, price_data, 'end_of_backtest')
|
|
5881
|
+
|
|
5882
|
+
# Calculate total P&L
|
|
5883
|
+
total_pnl = sum(data.get('pnl', 0) for data in price_data.values())
|
|
5884
|
+
|
|
5885
|
+
if self.debug:
|
|
5886
|
+
print(f" ✓ Closed {len(price_data)} positions at end of period")
|
|
5887
|
+
|
|
5888
|
+
return total_pnl
|
|
5889
|
+
|
|
5890
|
+
def close_position_by_signal(self, position_id, exit_date, price_data, close_reason='signal_exit'):
|
|
5891
|
+
"""
|
|
5892
|
+
Close position manually by signal (z-score exit, earnings exit, etc.).
|
|
5893
|
+
|
|
5894
|
+
This is a convenience method that handles all the boilerplate:
|
|
5895
|
+
- Gets position data from price_data
|
|
5896
|
+
- Generates close kwargs automatically
|
|
5897
|
+
- Closes the position
|
|
5898
|
+
- Returns pnl for capital update
|
|
5899
|
+
|
|
5900
|
+
Args:
|
|
5901
|
+
position_id: ID of position to close
|
|
5902
|
+
exit_date: Current date
|
|
5903
|
+
price_data: Dict from build_price_data() or check_positions() context
|
|
5904
|
+
close_reason: Reason for closing (e.g., 'z_score_exit', 'earnings_exit')
|
|
5905
|
+
|
|
5906
|
+
Returns:
|
|
5907
|
+
float: P&L of the closed position
|
|
5908
|
+
|
|
5909
|
+
Example:
|
|
5910
|
+
if abs(z_score) <= config['z_score_exit']:
|
|
5911
|
+
pnl = position_mgr.close_position_by_signal(
|
|
5912
|
+
position['id'], current_date, price_data, 'z_score_exit'
|
|
5913
|
+
)
|
|
5914
|
+
stats['signal_exits'] += 1
|
|
5915
|
+
capital += pnl
|
|
5916
|
+
"""
|
|
5917
|
+
# Get position data
|
|
5918
|
+
pos_data = price_data.get(position_id, {})
|
|
5919
|
+
pnl = pos_data.get('pnl', 0)
|
|
5920
|
+
pnl_pct = pos_data.get('pnl_pct', 0)
|
|
5921
|
+
|
|
5922
|
+
# Generate close kwargs automatically
|
|
5923
|
+
strategy_type = self.config.get('strategy_type', 'STRADDLE')
|
|
5924
|
+
kwargs = StrategyRegistry.generate_close_position_kwargs(strategy_type, pos_data)
|
|
5925
|
+
|
|
5926
|
+
# Close position
|
|
5927
|
+
self.close_position(
|
|
5928
|
+
position_id=position_id,
|
|
5929
|
+
exit_date=exit_date,
|
|
5930
|
+
exit_price=0.0,
|
|
5931
|
+
close_reason=close_reason,
|
|
5932
|
+
pnl=pnl,
|
|
5933
|
+
pnl_pct=pnl_pct,
|
|
5934
|
+
stat_key='signal_exits', # For consistency with other exits
|
|
5935
|
+
**kwargs
|
|
5936
|
+
)
|
|
5937
|
+
|
|
5938
|
+
return pnl
|
|
5939
|
+
|
|
5742
5940
|
def check_profit_target(self, current_date, stock_price, options_df, get_option_func):
|
|
5743
5941
|
"""
|
|
5744
5942
|
Check all positions for profit target achievement.
|
|
@@ -5832,6 +6030,8 @@ class PositionManager:
|
|
|
5832
6030
|
to_close.append({
|
|
5833
6031
|
'position_id': position_id,
|
|
5834
6032
|
'symbol': position['symbol'],
|
|
6033
|
+
'close_reason': 'profit_target', # ✅ For strategy stats
|
|
6034
|
+
'stat_key': 'profit_target_exits', # ✅ For strategy stats
|
|
5835
6035
|
'pnl': current_pnl,
|
|
5836
6036
|
'pnl_pct': pnl_pct,
|
|
5837
6037
|
'target_pct': target_pct,
|
|
@@ -7482,6 +7682,138 @@ class ChartGenerator:
|
|
|
7482
7682
|
print(f"Chart saved: {filename}")
|
|
7483
7683
|
|
|
7484
7684
|
return filename
|
|
7685
|
+
|
|
7686
|
+
@staticmethod
|
|
7687
|
+
def create_optimization_summary(results_df, metric='sharpe', filename='optimization_summary.png',
|
|
7688
|
+
show_plots=True, silent=False):
|
|
7689
|
+
"""
|
|
7690
|
+
Create 6-panel optimization summary chart.
|
|
7691
|
+
|
|
7692
|
+
Panels:
|
|
7693
|
+
1. Sharpe vs Return (colored by drawdown, sized by trades)
|
|
7694
|
+
2. Win Rate vs Profit Factor (colored by Sharpe)
|
|
7695
|
+
3. Sharpe distribution histogram
|
|
7696
|
+
4. Trade counts distribution
|
|
7697
|
+
5. Top 10 combinations by metric
|
|
7698
|
+
6. Parameter heatmap (if exactly 2 params)
|
|
7699
|
+
|
|
7700
|
+
Args:
|
|
7701
|
+
results_df: DataFrame with optimization results
|
|
7702
|
+
metric: Optimization metric ('sharpe', 'total_return', etc.)
|
|
7703
|
+
filename: Output filename
|
|
7704
|
+
show_plots: If True, display chart
|
|
7705
|
+
silent: If True, suppress print output
|
|
7706
|
+
|
|
7707
|
+
Returns:
|
|
7708
|
+
str: Path to saved chart, or None if failed
|
|
7709
|
+
|
|
7710
|
+
Example:
|
|
7711
|
+
chart_path = ChartGenerator.create_optimization_summary(
|
|
7712
|
+
results_df, metric='sharpe',
|
|
7713
|
+
filename=os.path.join(folder, 'optimization_summary.png')
|
|
7714
|
+
)
|
|
7715
|
+
"""
|
|
7716
|
+
try:
|
|
7717
|
+
import matplotlib.pyplot as plt
|
|
7718
|
+
import seaborn as sns
|
|
7719
|
+
sns.set_style("whitegrid")
|
|
7720
|
+
|
|
7721
|
+
# Create figure with 6 subplots
|
|
7722
|
+
fig = plt.figure(figsize=(20, 12))
|
|
7723
|
+
fig.suptitle('Optimization Results Summary', fontsize=16, fontweight='bold', y=0.995)
|
|
7724
|
+
|
|
7725
|
+
# 1. Sharpe vs Return (colored by drawdown, sized by trades)
|
|
7726
|
+
ax1 = plt.subplot(2, 3, 1)
|
|
7727
|
+
scatter1 = ax1.scatter(results_df['total_return'], results_df['sharpe'],
|
|
7728
|
+
c=results_df['max_drawdown'], s=results_df['total_trades']*5,
|
|
7729
|
+
cmap='RdYlGn_r', alpha=0.6, edgecolors='black', linewidth=0.5)
|
|
7730
|
+
ax1.set_xlabel('Total Return (%)')
|
|
7731
|
+
ax1.set_ylabel('Sharpe Ratio')
|
|
7732
|
+
ax1.set_title('Sharpe vs Return (size=trades, color=drawdown)')
|
|
7733
|
+
plt.colorbar(scatter1, ax=ax1, label='Max Drawdown (%)')
|
|
7734
|
+
ax1.grid(True, alpha=0.3)
|
|
7735
|
+
|
|
7736
|
+
# 2. Win Rate vs Profit Factor (colored by Sharpe)
|
|
7737
|
+
ax2 = plt.subplot(2, 3, 2)
|
|
7738
|
+
scatter2 = ax2.scatter(results_df['win_rate'], results_df['profit_factor'],
|
|
7739
|
+
c=results_df['sharpe'], s=100, cmap='viridis',
|
|
7740
|
+
alpha=0.6, edgecolors='black', linewidth=0.5)
|
|
7741
|
+
ax2.set_xlabel('Win Rate (%)')
|
|
7742
|
+
ax2.set_ylabel('Profit Factor')
|
|
7743
|
+
ax2.set_title('Win Rate vs Profit Factor (color=Sharpe)')
|
|
7744
|
+
plt.colorbar(scatter2, ax=ax2, label='Sharpe Ratio')
|
|
7745
|
+
ax2.grid(True, alpha=0.3)
|
|
7746
|
+
|
|
7747
|
+
# 3. Distribution of Sharpe Ratios
|
|
7748
|
+
ax3 = plt.subplot(2, 3, 3)
|
|
7749
|
+
ax3.hist(results_df['sharpe'], bins=20, color='steelblue', alpha=0.7, edgecolor='black')
|
|
7750
|
+
ax3.axvline(results_df['sharpe'].mean(), color='red', linestyle='--',
|
|
7751
|
+
label=f'Mean: {results_df["sharpe"].mean():.2f}')
|
|
7752
|
+
ax3.axvline(results_df['sharpe'].median(), color='green', linestyle='--',
|
|
7753
|
+
label=f'Median: {results_df["sharpe"].median():.2f}')
|
|
7754
|
+
ax3.set_xlabel('Sharpe Ratio')
|
|
7755
|
+
ax3.set_ylabel('Frequency')
|
|
7756
|
+
ax3.set_title('Distribution of Sharpe Ratios')
|
|
7757
|
+
ax3.legend()
|
|
7758
|
+
ax3.grid(True, alpha=0.3)
|
|
7759
|
+
|
|
7760
|
+
# 4. Distribution of Trade Counts
|
|
7761
|
+
ax4 = plt.subplot(2, 3, 4)
|
|
7762
|
+
ax4.hist(results_df['total_trades'], bins=20, color='coral', alpha=0.7, edgecolor='black')
|
|
7763
|
+
ax4.set_xlabel('Total Trades')
|
|
7764
|
+
ax4.set_ylabel('Frequency')
|
|
7765
|
+
ax4.set_title('Distribution of Trade Counts')
|
|
7766
|
+
ax4.grid(True, alpha=0.3)
|
|
7767
|
+
|
|
7768
|
+
# 5. Top 10 Combinations by metric
|
|
7769
|
+
ax5 = plt.subplot(2, 3, 5)
|
|
7770
|
+
top10 = results_df.nlargest(10, metric)
|
|
7771
|
+
top10_labels = [f"#{i+1}" for i in range(len(top10))]
|
|
7772
|
+
bars = ax5.barh(top10_labels, top10[metric], color='green', alpha=0.7, edgecolor='black')
|
|
7773
|
+
ax5.set_xlabel(metric.replace('_', ' ').title())
|
|
7774
|
+
ax5.set_title(f'Top 10 Combinations by {metric.replace("_", " ").title()}')
|
|
7775
|
+
ax5.invert_yaxis()
|
|
7776
|
+
ax5.grid(True, alpha=0.3, axis='x')
|
|
7777
|
+
|
|
7778
|
+
# 6. Heatmap of parameter combinations (if exactly 2 params)
|
|
7779
|
+
ax6 = plt.subplot(2, 3, 6)
|
|
7780
|
+
# Identify parameter columns (exclude metric columns)
|
|
7781
|
+
metric_cols = ['combination_id', 'is_valid', 'invalid_reason', 'total_return',
|
|
7782
|
+
'sharpe', 'sortino', 'calmar', 'max_drawdown', 'win_rate',
|
|
7783
|
+
'profit_factor', 'total_trades', 'avg_win', 'avg_loss',
|
|
7784
|
+
'volatility', 'stop_loss_type', 'stop_loss_value']
|
|
7785
|
+
param_cols = [col for col in results_df.columns if col not in metric_cols]
|
|
7786
|
+
|
|
7787
|
+
if len(param_cols) == 2:
|
|
7788
|
+
# Create pivot table for heatmap
|
|
7789
|
+
pivot = results_df.pivot_table(values=metric, index=param_cols[0],
|
|
7790
|
+
columns=param_cols[1], aggfunc='mean')
|
|
7791
|
+
sns.heatmap(pivot, annot=True, fmt='.3f', cmap='RdYlGn', ax=ax6,
|
|
7792
|
+
cbar_kws={'label': metric.replace('_', ' ').title()})
|
|
7793
|
+
ax6.set_title(f'{metric.replace("_", " ").title()} Heatmap')
|
|
7794
|
+
else:
|
|
7795
|
+
ax6.text(0.5, 0.5, f'Heatmap requires\nexactly 2 parameters\n(found {len(param_cols)})',
|
|
7796
|
+
ha='center', va='center', fontsize=14, transform=ax6.transAxes)
|
|
7797
|
+
ax6.set_title(f'{metric.replace("_", " ").title()} Heatmap')
|
|
7798
|
+
ax6.axis('off')
|
|
7799
|
+
|
|
7800
|
+
plt.tight_layout()
|
|
7801
|
+
plt.savefig(filename, dpi=150, bbox_inches='tight')
|
|
7802
|
+
|
|
7803
|
+
if show_plots:
|
|
7804
|
+
plt.show()
|
|
7805
|
+
else:
|
|
7806
|
+
plt.close()
|
|
7807
|
+
|
|
7808
|
+
if not silent:
|
|
7809
|
+
print(f"✓ Optimization chart saved: {filename}")
|
|
7810
|
+
|
|
7811
|
+
return filename
|
|
7812
|
+
|
|
7813
|
+
except Exception as e:
|
|
7814
|
+
if not silent:
|
|
7815
|
+
print(f"⚠️ Could not create optimization charts: {e}")
|
|
7816
|
+
return None
|
|
7485
7817
|
|
|
7486
7818
|
|
|
7487
7819
|
def create_stoploss_charts(analyzer, filename='stoploss_analysis.png', show_plots=True):
|
|
@@ -7785,8 +8117,13 @@ def format_params_string(config):
|
|
|
7785
8117
|
# ═══════════════════════════════════════════════════════════════════════
|
|
7786
8118
|
# 2. AUTOMATIC STRATEGY DETECTION (from STRATEGIES registry)
|
|
7787
8119
|
# ═══════════════════════════════════════════════════════════════════════
|
|
8120
|
+
# First try signature-based detection
|
|
7788
8121
|
strategy_type_lower = detect_strategy_type(config)
|
|
7789
8122
|
|
|
8123
|
+
# ✅ Fallback: Use explicit strategy_type from config if detection failed
|
|
8124
|
+
if not strategy_type_lower and config.get('strategy_type'):
|
|
8125
|
+
strategy_type_lower = config['strategy_type'].lower()
|
|
8126
|
+
|
|
7790
8127
|
# Debug: Show detection result
|
|
7791
8128
|
if config.get('debuginfo', 0) >= 2:
|
|
7792
8129
|
print(f"[DEBUG format_params_string] Detected strategy: {strategy_type_lower}")
|
|
@@ -7807,15 +8144,32 @@ def format_params_string(config):
|
|
|
7807
8144
|
except Exception as e:
|
|
7808
8145
|
pass # Will be handled below
|
|
7809
8146
|
|
|
8147
|
+
# ✅ Helper: Get value from config OR nested configs (earnings_config, stop_loss_config)
|
|
8148
|
+
def get_param_value(key):
|
|
8149
|
+
"""Get parameter value from config, earnings_config, or stop_loss_config"""
|
|
8150
|
+
if key in config:
|
|
8151
|
+
return config[key]
|
|
8152
|
+
# Check earnings_config
|
|
8153
|
+
earnings_cfg = config.get('earnings_config', {})
|
|
8154
|
+
if key in earnings_cfg:
|
|
8155
|
+
return earnings_cfg[key]
|
|
8156
|
+
# Check stop_loss_config
|
|
8157
|
+
sl_cfg = config.get('stop_loss_config', {})
|
|
8158
|
+
if key in sl_cfg:
|
|
8159
|
+
return sl_cfg[key]
|
|
8160
|
+
if key == 'stop_loss_pct' and 'value' in sl_cfg:
|
|
8161
|
+
return sl_cfg['value']
|
|
8162
|
+
return None
|
|
8163
|
+
|
|
7810
8164
|
# Format strategy-specific parameters
|
|
7811
8165
|
for param_def in strategy['file_naming']['format']:
|
|
7812
8166
|
key = param_def['key']
|
|
7813
8167
|
code = param_def['code']
|
|
7814
8168
|
format_func = param_def['formatter']
|
|
7815
8169
|
|
|
7816
|
-
|
|
8170
|
+
value = get_param_value(key)
|
|
8171
|
+
if value is not None:
|
|
7817
8172
|
try:
|
|
7818
|
-
value = config[key]
|
|
7819
8173
|
formatted = format_func(value)
|
|
7820
8174
|
if code:
|
|
7821
8175
|
parts.append(f"{code}{formatted}")
|
|
@@ -7826,6 +8180,28 @@ def format_params_string(config):
|
|
|
7826
8180
|
# Debug: Show missing keys
|
|
7827
8181
|
elif config.get('debuginfo', 0) >= 2:
|
|
7828
8182
|
print(f"[DEBUG format_params_string] Missing key '{key}' in config for strategy {strategy_type_upper}")
|
|
8183
|
+
|
|
8184
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
8185
|
+
# AUTO-FORMAT EARNINGS PARAMS (if earnings_config exists)
|
|
8186
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
8187
|
+
earnings_cfg = config.get('earnings_config', {})
|
|
8188
|
+
if earnings_cfg.get('mode') in ['trade_around', 'avoid']:
|
|
8189
|
+
earnings_formats = [
|
|
8190
|
+
('entry_days_before', 'ED', lambda x: f"{int(x)}"),
|
|
8191
|
+
('exit_days_after', 'XD', lambda x: f"{int(x)}"),
|
|
8192
|
+
('iv_percentile_min', 'IV', lambda x: f"{int(x)}" if x else None),
|
|
8193
|
+
('min_implied_move', 'IM', lambda x: f"{int(x*100)}" if x else None),
|
|
8194
|
+
]
|
|
8195
|
+
for key, code, fmt in earnings_formats:
|
|
8196
|
+
# Check both root config and earnings_config
|
|
8197
|
+
value = config.get(key) or earnings_cfg.get(key)
|
|
8198
|
+
if value is not None:
|
|
8199
|
+
try:
|
|
8200
|
+
formatted = fmt(value)
|
|
8201
|
+
if formatted and code:
|
|
8202
|
+
parts.append(f"{code}{formatted}")
|
|
8203
|
+
except:
|
|
8204
|
+
pass
|
|
7829
8205
|
|
|
7830
8206
|
# Add stop-loss (if enabled)
|
|
7831
8207
|
if config.get('stop_loss_enabled') and 'stop_loss_config' in config:
|
|
@@ -8590,6 +8966,80 @@ class StopLossConfig:
|
|
|
8590
8966
|
return merged
|
|
8591
8967
|
|
|
8592
8968
|
|
|
8969
|
+
# ============================================================
|
|
8970
|
+
# BASELINE STOP-LOSS CONFIGURATION HELPER
|
|
8971
|
+
# ============================================================
|
|
8972
|
+
def configure_baseline_stop_loss(base_config, stop_loss_config, optimization_config, verbose=True):
|
|
8973
|
+
"""
|
|
8974
|
+
Configure stop-loss for baseline test with pre-defined variables.
|
|
8975
|
+
|
|
8976
|
+
Simplifies baseline configuration by:
|
|
8977
|
+
1. Pre-defining ALL stop-loss variables (avoids NameError)
|
|
8978
|
+
2. Using flat if/elif/else structure (avoids indentation errors)
|
|
8979
|
+
3. Printing stop-loss info (if verbose)
|
|
8980
|
+
4. Returning properly configured config
|
|
8981
|
+
|
|
8982
|
+
Args:
|
|
8983
|
+
base_config: Base configuration dict (e.g., optimized_config)
|
|
8984
|
+
stop_loss_config: STOP_LOSS_CONFIG dict
|
|
8985
|
+
optimization_config: OPTIMIZATION_CONFIG dict with param_grid
|
|
8986
|
+
verbose: If True, print stop-loss configuration info
|
|
8987
|
+
|
|
8988
|
+
Returns:
|
|
8989
|
+
tuple: (configured_config, sl_values)
|
|
8990
|
+
- configured_config: Config with proper stop_loss_config
|
|
8991
|
+
- sl_values: Dict with extracted SL values for reference
|
|
8992
|
+
|
|
8993
|
+
Example:
|
|
8994
|
+
baseline_config, sl_values = configure_baseline_stop_loss(
|
|
8995
|
+
optimized_config, STOP_LOSS_CONFIG, OPTIMIZATION_CONFIG
|
|
8996
|
+
)
|
|
8997
|
+
"""
|
|
8998
|
+
param_grid = optimization_config.get('param_grid', {})
|
|
8999
|
+
sl_enabled = stop_loss_config.get('enable_in', {}).get('baseline', False)
|
|
9000
|
+
|
|
9001
|
+
# Pre-define ALL stop-loss variables (avoids NameError in any branch)
|
|
9002
|
+
sl_values = {
|
|
9003
|
+
'simple': param_grid.get('stop_loss_values', [0.04])[0],
|
|
9004
|
+
'pl_loss': param_grid.get('combined_pl_loss', [0.10])[0],
|
|
9005
|
+
'directional': param_grid.get('combined_directional', [0.04])[0],
|
|
9006
|
+
'logic': stop_loss_config.get('combined_settings', {}).get('logic', 'and'),
|
|
9007
|
+
'enabled': sl_enabled,
|
|
9008
|
+
'type': stop_loss_config.get('type', 'none')
|
|
9009
|
+
}
|
|
9010
|
+
|
|
9011
|
+
# Print info (if verbose)
|
|
9012
|
+
if verbose:
|
|
9013
|
+
if not sl_enabled:
|
|
9014
|
+
print("Stop-Loss: DISABLED")
|
|
9015
|
+
elif stop_loss_config.get('type') == 'combined':
|
|
9016
|
+
logic_upper = sl_values['logic'].upper()
|
|
9017
|
+
print(f"Stop-Loss: Combined {logic_upper} (P&L {sl_values['pl_loss']*100:.0f}% {logic_upper} Directional {sl_values['directional']*100:.0f}%) - ENABLED")
|
|
9018
|
+
else:
|
|
9019
|
+
sl_type = stop_loss_config.get('type', 'directional')
|
|
9020
|
+
print(f"Stop-Loss: {sl_values['simple']*100:.0f}% {sl_type} - ENABLED")
|
|
9021
|
+
|
|
9022
|
+
# Create configured config
|
|
9023
|
+
configured_config = base_config.copy()
|
|
9024
|
+
configured_config['stop_loss_enabled'] = sl_enabled
|
|
9025
|
+
|
|
9026
|
+
# Configure stop_loss_config based on type (flat structure)
|
|
9027
|
+
if not sl_enabled:
|
|
9028
|
+
configured_config['stop_loss_config'] = {'enabled': False, 'type': 'none', 'value': 0}
|
|
9029
|
+
elif stop_loss_config.get('type') == 'combined':
|
|
9030
|
+
configured_config['stop_loss_config'] = stop_loss_config.copy()
|
|
9031
|
+
configured_config['stop_loss_config']['combined_settings'] = {
|
|
9032
|
+
'pl_loss': sl_values['pl_loss'],
|
|
9033
|
+
'directional': sl_values['directional'],
|
|
9034
|
+
'logic': sl_values['logic']
|
|
9035
|
+
}
|
|
9036
|
+
else:
|
|
9037
|
+
configured_config['stop_loss_config'] = stop_loss_config.copy()
|
|
9038
|
+
configured_config['stop_loss_config']['value'] = sl_values['simple']
|
|
9039
|
+
|
|
9040
|
+
return configured_config, sl_values
|
|
9041
|
+
|
|
9042
|
+
|
|
8593
9043
|
def create_stoploss_comparison_chart(results, filename='stoploss_comparison.png', show_plots=True):
|
|
8594
9044
|
"""Create comparison chart"""
|
|
8595
9045
|
try:
|
|
@@ -9325,6 +9775,27 @@ def _auto_detect_requests(config):
|
|
|
9325
9775
|
}
|
|
9326
9776
|
})
|
|
9327
9777
|
|
|
9778
|
+
# ========================================================
|
|
9779
|
+
# AUTO-DETECT EARNINGS CALENDAR
|
|
9780
|
+
# ========================================================
|
|
9781
|
+
# Load earnings calendar if earnings_config is specified
|
|
9782
|
+
# Works with ANY strategy type (STRADDLE, IRON_CONDOR, etc.)
|
|
9783
|
+
earnings_config = config.get('earnings_config', {})
|
|
9784
|
+
earnings_mode = earnings_config.get('mode')
|
|
9785
|
+
|
|
9786
|
+
if earnings_mode in ['trade_around', 'avoid']:
|
|
9787
|
+
print(f" 📅 Earnings mode: {earnings_mode} - loading earnings calendar")
|
|
9788
|
+
requests.append({
|
|
9789
|
+
'name': 'earnings',
|
|
9790
|
+
'endpoint': '/equities/eod/history-earnings-calendar',
|
|
9791
|
+
'params': {
|
|
9792
|
+
'symbols': config['symbol'],
|
|
9793
|
+
'startDate': config['start_date'],
|
|
9794
|
+
'endDate': config['end_date']
|
|
9795
|
+
},
|
|
9796
|
+
'chunking': {'enabled': False} # Single request (lightweight)
|
|
9797
|
+
})
|
|
9798
|
+
|
|
9328
9799
|
return requests
|
|
9329
9800
|
|
|
9330
9801
|
|
|
@@ -9645,6 +10116,82 @@ def _auto_process_dates(df):
|
|
|
9645
10116
|
return df
|
|
9646
10117
|
|
|
9647
10118
|
|
|
10119
|
+
# ============================================================================
|
|
10120
|
+
# EARNINGS STRATEGY HELPERS
|
|
10121
|
+
# ============================================================================
|
|
10122
|
+
|
|
10123
|
+
def parse_earnings_calendar(earnings_df):
|
|
10124
|
+
"""
|
|
10125
|
+
Parse preloaded earnings DataFrame to list of dicts for strategy use.
|
|
10126
|
+
|
|
10127
|
+
Framework loads earnings via _auto_detect_requests() when earnings_config['mode']
|
|
10128
|
+
is set. This helper converts the DataFrame to a sorted list for iteration.
|
|
10129
|
+
|
|
10130
|
+
Args:
|
|
10131
|
+
earnings_df: Preloaded DataFrame from config.get('_preloaded_earnings')
|
|
10132
|
+
|
|
10133
|
+
Returns:
|
|
10134
|
+
list: [{'date': datetime.date, 'estimate': float, 'reported': float,
|
|
10135
|
+
'time_of_day': str}, ...]
|
|
10136
|
+
Sorted by date ascending.
|
|
10137
|
+
|
|
10138
|
+
Usage in strategy:
|
|
10139
|
+
earnings_df = config.get('_preloaded_earnings', pd.DataFrame())
|
|
10140
|
+
earnings_events = parse_earnings_calendar(earnings_df)
|
|
10141
|
+
|
|
10142
|
+
for event in earnings_events:
|
|
10143
|
+
earning_date = event['date']
|
|
10144
|
+
# ... process event
|
|
10145
|
+
"""
|
|
10146
|
+
from datetime import datetime
|
|
10147
|
+
|
|
10148
|
+
if earnings_df is None or earnings_df.empty:
|
|
10149
|
+
return []
|
|
10150
|
+
|
|
10151
|
+
earnings_events = []
|
|
10152
|
+
for _, record in earnings_df.iterrows():
|
|
10153
|
+
earning_date = record.get('earning_date')
|
|
10154
|
+
if earning_date:
|
|
10155
|
+
# Handle both string and date formats
|
|
10156
|
+
if isinstance(earning_date, str):
|
|
10157
|
+
date_obj = datetime.strptime(earning_date, '%Y-%m-%d').date()
|
|
10158
|
+
elif hasattr(earning_date, 'date'):
|
|
10159
|
+
date_obj = earning_date.date()
|
|
10160
|
+
else:
|
|
10161
|
+
date_obj = earning_date
|
|
10162
|
+
|
|
10163
|
+
earnings_events.append({
|
|
10164
|
+
'date': date_obj,
|
|
10165
|
+
'estimate': record.get('estimate'),
|
|
10166
|
+
'reported': record.get('reported_earning'),
|
|
10167
|
+
'time_of_day': record.get('time_of_day_code', 'UNK')
|
|
10168
|
+
})
|
|
10169
|
+
|
|
10170
|
+
return sorted(earnings_events, key=lambda x: x['date'])
|
|
10171
|
+
|
|
10172
|
+
|
|
10173
|
+
def find_next_trading_day(trading_days, target_date):
|
|
10174
|
+
"""
|
|
10175
|
+
Find the next available trading day on or after target_date.
|
|
10176
|
+
|
|
10177
|
+
Args:
|
|
10178
|
+
trading_days: Sorted list of trading days (datetime.date objects)
|
|
10179
|
+
target_date: Target date to find trading day for
|
|
10180
|
+
|
|
10181
|
+
Returns:
|
|
10182
|
+
datetime.date: Next trading day, or None if not found
|
|
10183
|
+
|
|
10184
|
+
Usage:
|
|
10185
|
+
trading_days = sorted(stock_df['date'].unique())
|
|
10186
|
+
entry_day = find_next_trading_day(trading_days, earning_date - timedelta(days=1))
|
|
10187
|
+
exit_day = find_next_trading_day(trading_days, earning_date + timedelta(days=1))
|
|
10188
|
+
"""
|
|
10189
|
+
for day in trading_days:
|
|
10190
|
+
if day >= target_date:
|
|
10191
|
+
return day
|
|
10192
|
+
return None
|
|
10193
|
+
|
|
10194
|
+
|
|
9648
10195
|
# ============================================================================
|
|
9649
10196
|
# INDICATOR PRE-CALCULATION SYSTEM - Universal helpers
|
|
9650
10197
|
# ============================================================================
|
|
@@ -10118,6 +10665,10 @@ def optimize_parameters(base_config, param_grid, strategy_function,
|
|
|
10118
10665
|
optimization_start_time = datetime.now()
|
|
10119
10666
|
start_time_str = optimization_start_time.strftime('%Y-%m-%d %H:%M:%S')
|
|
10120
10667
|
|
|
10668
|
+
# Detect preloaded data
|
|
10669
|
+
preloaded_keys = [k for k in base_config.keys() if k.startswith('_preloaded_')]
|
|
10670
|
+
has_preloaded_data = len(preloaded_keys) > 0
|
|
10671
|
+
|
|
10121
10672
|
print("\n" + "="*80)
|
|
10122
10673
|
print(" "*20 + "PARAMETER OPTIMIZATION")
|
|
10123
10674
|
print("="*80)
|
|
@@ -10125,9 +10676,11 @@ def optimize_parameters(base_config, param_grid, strategy_function,
|
|
|
10125
10676
|
print(f"Period: {base_config.get('start_date')} to {base_config.get('end_date')}")
|
|
10126
10677
|
print(f"Optimization Metric: {optimization_metric}")
|
|
10127
10678
|
print(f"Min Trades: {min_trades}")
|
|
10128
|
-
print(f"🕐 Started: {start_time_str}")
|
|
10129
10679
|
if max_drawdown_limit:
|
|
10130
10680
|
print(f"Max Drawdown Limit: {max_drawdown_limit*100:.0f}%")
|
|
10681
|
+
if has_preloaded_data:
|
|
10682
|
+
print(f"✅ Preloaded data: {', '.join([k.replace('_preloaded_', '') for k in preloaded_keys])}")
|
|
10683
|
+
print(f"🕐 Started: {start_time_str}")
|
|
10131
10684
|
print("="*80 + "\n")
|
|
10132
10685
|
|
|
10133
10686
|
# Generate all combinations
|
|
@@ -10267,6 +10820,42 @@ def optimize_parameters(base_config, param_grid, strategy_function,
|
|
|
10267
10820
|
# ✨ CRITICAL: Enable stop-loss when combined_* params are used
|
|
10268
10821
|
test_config['stop_loss_enabled'] = True
|
|
10269
10822
|
|
|
10823
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
10824
|
+
# EARNINGS CONFIG SUPPORT (v2.22+)
|
|
10825
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
10826
|
+
# If param_grid contains earnings-related parameters, automatically
|
|
10827
|
+
# update earnings_config with them
|
|
10828
|
+
# ✅ Use .get() NOT .pop() - keep params in test_config for format_params_string!
|
|
10829
|
+
earnings_params = {}
|
|
10830
|
+
if 'entry_days_before' in test_config:
|
|
10831
|
+
earnings_params['entry_days_before'] = test_config['entry_days_before']
|
|
10832
|
+
if 'exit_days_after' in test_config:
|
|
10833
|
+
earnings_params['exit_days_after'] = test_config['exit_days_after']
|
|
10834
|
+
if 'min_implied_move' in test_config:
|
|
10835
|
+
earnings_params['min_implied_move'] = test_config['min_implied_move']
|
|
10836
|
+
if 'iv_percentile_min' in test_config:
|
|
10837
|
+
earnings_params['iv_percentile_min'] = test_config['iv_percentile_min']
|
|
10838
|
+
if 'earnings_buffer_days' in test_config:
|
|
10839
|
+
earnings_params['buffer_days'] = test_config['earnings_buffer_days']
|
|
10840
|
+
|
|
10841
|
+
# If any earnings params exist, update earnings_config
|
|
10842
|
+
if earnings_params:
|
|
10843
|
+
if 'earnings_config' not in test_config:
|
|
10844
|
+
test_config['earnings_config'] = {}
|
|
10845
|
+
|
|
10846
|
+
# Merge earnings_params into earnings_config
|
|
10847
|
+
test_config['earnings_config'].update(earnings_params)
|
|
10848
|
+
|
|
10849
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
10850
|
+
# STOP-LOSS CONFIG SUPPORT
|
|
10851
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
10852
|
+
# If param_grid contains stop_loss_pct, update stop_loss_config['value']
|
|
10853
|
+
if 'stop_loss_pct' in test_config:
|
|
10854
|
+
if 'stop_loss_config' not in test_config:
|
|
10855
|
+
test_config['stop_loss_config'] = {}
|
|
10856
|
+
test_config['stop_loss_config']['value'] = test_config['stop_loss_pct']
|
|
10857
|
+
test_config['stop_loss_enabled'] = True
|
|
10858
|
+
|
|
10270
10859
|
# Update name
|
|
10271
10860
|
param_str = "_".join([f"{k}={v}" for k, v in zip(param_names, param_combo)])
|
|
10272
10861
|
test_config['strategy_name'] = f"{base_config.get('strategy_name', 'Strategy')} [{param_str}]"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ivolatility_backtesting
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.36
|
|
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.36"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|