ivolatility-backtesting 1.34__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.34 → ivolatility_backtesting-1.36}/PKG-INFO +1 -1
- {ivolatility_backtesting-1.34 → ivolatility_backtesting-1.36}/ivolatility_backtesting/__init__.py +4 -2
- {ivolatility_backtesting-1.34 → ivolatility_backtesting-1.36}/ivolatility_backtesting/ivolatility_backtesting.py +682 -27
- {ivolatility_backtesting-1.34 → ivolatility_backtesting-1.36}/ivolatility_backtesting.egg-info/PKG-INFO +1 -1
- {ivolatility_backtesting-1.34 → ivolatility_backtesting-1.36}/pyproject.toml +1 -1
- {ivolatility_backtesting-1.34 → ivolatility_backtesting-1.36}/LICENSE +0 -0
- {ivolatility_backtesting-1.34 → ivolatility_backtesting-1.36}/README.md +0 -0
- {ivolatility_backtesting-1.34 → ivolatility_backtesting-1.36}/ivolatility_backtesting.egg-info/SOURCES.txt +0 -0
- {ivolatility_backtesting-1.34 → ivolatility_backtesting-1.36}/ivolatility_backtesting.egg-info/dependency_links.txt +0 -0
- {ivolatility_backtesting-1.34 → ivolatility_backtesting-1.36}/ivolatility_backtesting.egg-info/requires.txt +0 -0
- {ivolatility_backtesting-1.34 → ivolatility_backtesting-1.36}/ivolatility_backtesting.egg-info/top_level.txt +0 -0
- {ivolatility_backtesting-1.34 → 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.34 → 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 = {}
|
|
@@ -3656,10 +3659,10 @@ def _api_call_internal(endpoint, cache_config, debug, debug_level, **kwargs):
|
|
|
3656
3659
|
elif debug or cache_config.get('debug', False):
|
|
3657
3660
|
print(f"[CACHE] ✓ Cache hit: {endpoint} ({len(cached_data) if hasattr(cached_data, '__len__') else '?'} records)")
|
|
3658
3661
|
|
|
3659
|
-
# ✅
|
|
3660
|
-
#
|
|
3662
|
+
# ✅ CONSISTENT: Always return list format for backward compatibility
|
|
3663
|
+
# This ensures `if not response.get('data')` works correctly in all strategies
|
|
3661
3664
|
if isinstance(cached_data, pd.DataFrame):
|
|
3662
|
-
return {'data': cached_data, 'status': 'success'
|
|
3665
|
+
return {'data': cached_data.to_dict('records'), 'status': 'success'}
|
|
3663
3666
|
return cached_data
|
|
3664
3667
|
|
|
3665
3668
|
if debug_level >= 3:
|
|
@@ -3773,6 +3776,84 @@ def _api_call_internal(endpoint, cache_config, debug, debug_level, **kwargs):
|
|
|
3773
3776
|
return None
|
|
3774
3777
|
|
|
3775
3778
|
|
|
3779
|
+
# ============================================================
|
|
3780
|
+
# API RESPONSE HELPERS
|
|
3781
|
+
# ============================================================
|
|
3782
|
+
def get_api_data(response, as_dataframe=True):
|
|
3783
|
+
"""
|
|
3784
|
+
Universal helper to extract data from api_call response.
|
|
3785
|
+
Works with both list and DataFrame formats from cache.
|
|
3786
|
+
|
|
3787
|
+
Args:
|
|
3788
|
+
response: Response dict from api_call()
|
|
3789
|
+
as_dataframe: If True (default), always return DataFrame.
|
|
3790
|
+
If False, return list of dicts.
|
|
3791
|
+
|
|
3792
|
+
Returns:
|
|
3793
|
+
DataFrame/list or None if no valid data
|
|
3794
|
+
|
|
3795
|
+
Example:
|
|
3796
|
+
# Get stock data as DataFrame
|
|
3797
|
+
stock_df = get_api_data(api_call('/equities/eod/stock-prices', ...))
|
|
3798
|
+
if stock_df is None:
|
|
3799
|
+
print("No data!")
|
|
3800
|
+
|
|
3801
|
+
# Get as list of dicts
|
|
3802
|
+
records = get_api_data(response, as_dataframe=False)
|
|
3803
|
+
"""
|
|
3804
|
+
if response is None:
|
|
3805
|
+
return None
|
|
3806
|
+
|
|
3807
|
+
data = response.get('data')
|
|
3808
|
+
if data is None:
|
|
3809
|
+
return None
|
|
3810
|
+
|
|
3811
|
+
# Handle DataFrame (from cache)
|
|
3812
|
+
if isinstance(data, pd.DataFrame):
|
|
3813
|
+
if data.empty:
|
|
3814
|
+
return None
|
|
3815
|
+
return data if as_dataframe else data.to_dict('records')
|
|
3816
|
+
|
|
3817
|
+
# Handle list (from API)
|
|
3818
|
+
if not data: # empty list
|
|
3819
|
+
return None
|
|
3820
|
+
|
|
3821
|
+
return pd.DataFrame(data) if as_dataframe else data
|
|
3822
|
+
|
|
3823
|
+
|
|
3824
|
+
def is_api_response_valid(response):
|
|
3825
|
+
"""
|
|
3826
|
+
Check if api_call response contains valid non-empty data.
|
|
3827
|
+
Works with both list and DataFrame formats.
|
|
3828
|
+
|
|
3829
|
+
Args:
|
|
3830
|
+
response: Response dict from api_call()
|
|
3831
|
+
|
|
3832
|
+
Returns:
|
|
3833
|
+
bool: True if response contains valid non-empty data
|
|
3834
|
+
|
|
3835
|
+
Example:
|
|
3836
|
+
response = api_call('/equities/eod/stock-prices', ...)
|
|
3837
|
+
if not is_api_response_valid(response):
|
|
3838
|
+
print("No data available!")
|
|
3839
|
+
return
|
|
3840
|
+
|
|
3841
|
+
# Safe to use response['data'] now
|
|
3842
|
+
df = pd.DataFrame(response['data'])
|
|
3843
|
+
"""
|
|
3844
|
+
if response is None:
|
|
3845
|
+
return False
|
|
3846
|
+
|
|
3847
|
+
data = response.get('data')
|
|
3848
|
+
if data is None:
|
|
3849
|
+
return False
|
|
3850
|
+
|
|
3851
|
+
if isinstance(data, pd.DataFrame):
|
|
3852
|
+
return not data.empty
|
|
3853
|
+
|
|
3854
|
+
return bool(data)
|
|
3855
|
+
|
|
3856
|
+
|
|
3776
3857
|
# ============================================================
|
|
3777
3858
|
# OPTIONS DATA HELPERS
|
|
3778
3859
|
# ============================================================
|
|
@@ -4764,6 +4845,39 @@ class PositionManager:
|
|
|
4764
4845
|
self.sl_config = None
|
|
4765
4846
|
self.sl_manager = None
|
|
4766
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
|
+
|
|
4767
4881
|
# ================================================================
|
|
4768
4882
|
# UNIVERSAL INTRINSIC VALUE CALCULATION METHOD
|
|
4769
4883
|
# ================================================================
|
|
@@ -5422,16 +5536,26 @@ class PositionManager:
|
|
|
5422
5536
|
# Determine stop_type: 'expiration' (market data) or 'expiration_intrinsic' (no data)
|
|
5423
5537
|
stop_type = 'expiration_intrinsic' if used_intrinsic else 'expiration'
|
|
5424
5538
|
|
|
5425
|
-
|
|
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 = {
|
|
5426
5545
|
'position_id': position_id,
|
|
5427
5546
|
'symbol': position['symbol'],
|
|
5428
5547
|
'stop_type': stop_type,
|
|
5548
|
+
'close_reason': stop_type, # ✅ For strategy stats
|
|
5549
|
+
'stat_key': 'expiration_exits', # ✅ For strategy stats
|
|
5429
5550
|
'stop_level': None,
|
|
5430
5551
|
'current_price': current_price,
|
|
5431
5552
|
'pnl': current_pnl,
|
|
5432
5553
|
'pnl_pct': current_pnl_pct,
|
|
5433
|
-
'settlement_type': 'intrinsic' if used_intrinsic else 'market'
|
|
5434
|
-
|
|
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)
|
|
5435
5559
|
|
|
5436
5560
|
if self.debug:
|
|
5437
5561
|
print(f"[PositionManager] 📅 EXPIRATION: {position_id} expired on {expiration}")
|
|
@@ -5628,6 +5752,10 @@ class PositionManager:
|
|
|
5628
5752
|
# For other stop types: generic reason
|
|
5629
5753
|
stop_info['exit_reason'] = f"stop_loss_{stop_type}"
|
|
5630
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
|
+
|
|
5631
5759
|
# ✅ ADD stop-loss metadata for CSV export (stop_threshold, actual_value)
|
|
5632
5760
|
# These fields are needed for detailed analysis of stop-loss triggers
|
|
5633
5761
|
if stop_type == 'pl_loss':
|
|
@@ -5653,6 +5781,12 @@ class PositionManager:
|
|
|
5653
5781
|
# Just merge them into stop_info (no need for second mapping!)
|
|
5654
5782
|
stop_info.update(intraday_data)
|
|
5655
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
|
+
|
|
5656
5790
|
to_close.append(stop_info)
|
|
5657
5791
|
|
|
5658
5792
|
if self.debug:
|
|
@@ -5661,6 +5795,148 @@ class PositionManager:
|
|
|
5661
5795
|
|
|
5662
5796
|
return to_close
|
|
5663
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
|
+
|
|
5664
5940
|
def check_profit_target(self, current_date, stock_price, options_df, get_option_func):
|
|
5665
5941
|
"""
|
|
5666
5942
|
Check all positions for profit target achievement.
|
|
@@ -5754,6 +6030,8 @@ class PositionManager:
|
|
|
5754
6030
|
to_close.append({
|
|
5755
6031
|
'position_id': position_id,
|
|
5756
6032
|
'symbol': position['symbol'],
|
|
6033
|
+
'close_reason': 'profit_target', # ✅ For strategy stats
|
|
6034
|
+
'stat_key': 'profit_target_exits', # ✅ For strategy stats
|
|
5757
6035
|
'pnl': current_pnl,
|
|
5758
6036
|
'pnl_pct': pnl_pct,
|
|
5759
6037
|
'target_pct': target_pct,
|
|
@@ -7404,6 +7682,138 @@ class ChartGenerator:
|
|
|
7404
7682
|
print(f"Chart saved: {filename}")
|
|
7405
7683
|
|
|
7406
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
|
|
7407
7817
|
|
|
7408
7818
|
|
|
7409
7819
|
def create_stoploss_charts(analyzer, filename='stoploss_analysis.png', show_plots=True):
|
|
@@ -7707,8 +8117,13 @@ def format_params_string(config):
|
|
|
7707
8117
|
# ═══════════════════════════════════════════════════════════════════════
|
|
7708
8118
|
# 2. AUTOMATIC STRATEGY DETECTION (from STRATEGIES registry)
|
|
7709
8119
|
# ═══════════════════════════════════════════════════════════════════════
|
|
8120
|
+
# First try signature-based detection
|
|
7710
8121
|
strategy_type_lower = detect_strategy_type(config)
|
|
7711
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
|
+
|
|
7712
8127
|
# Debug: Show detection result
|
|
7713
8128
|
if config.get('debuginfo', 0) >= 2:
|
|
7714
8129
|
print(f"[DEBUG format_params_string] Detected strategy: {strategy_type_lower}")
|
|
@@ -7729,15 +8144,32 @@ def format_params_string(config):
|
|
|
7729
8144
|
except Exception as e:
|
|
7730
8145
|
pass # Will be handled below
|
|
7731
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
|
+
|
|
7732
8164
|
# Format strategy-specific parameters
|
|
7733
8165
|
for param_def in strategy['file_naming']['format']:
|
|
7734
8166
|
key = param_def['key']
|
|
7735
8167
|
code = param_def['code']
|
|
7736
8168
|
format_func = param_def['formatter']
|
|
7737
8169
|
|
|
7738
|
-
|
|
8170
|
+
value = get_param_value(key)
|
|
8171
|
+
if value is not None:
|
|
7739
8172
|
try:
|
|
7740
|
-
value = config[key]
|
|
7741
8173
|
formatted = format_func(value)
|
|
7742
8174
|
if code:
|
|
7743
8175
|
parts.append(f"{code}{formatted}")
|
|
@@ -7748,6 +8180,28 @@ def format_params_string(config):
|
|
|
7748
8180
|
# Debug: Show missing keys
|
|
7749
8181
|
elif config.get('debuginfo', 0) >= 2:
|
|
7750
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
|
|
7751
8205
|
|
|
7752
8206
|
# Add stop-loss (if enabled)
|
|
7753
8207
|
if config.get('stop_loss_enabled') and 'stop_loss_config' in config:
|
|
@@ -8512,6 +8966,80 @@ class StopLossConfig:
|
|
|
8512
8966
|
return merged
|
|
8513
8967
|
|
|
8514
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
|
+
|
|
8515
9043
|
def create_stoploss_comparison_chart(results, filename='stoploss_comparison.png', show_plots=True):
|
|
8516
9044
|
"""Create comparison chart"""
|
|
8517
9045
|
try:
|
|
@@ -8918,11 +9446,7 @@ def preload_data_universal(config, data_requests=None, debug=False):
|
|
|
8918
9446
|
|
|
8919
9447
|
response = api_call(endpoint, cache_config, debug=debuginfo, **params)
|
|
8920
9448
|
if response and 'data' in response:
|
|
8921
|
-
|
|
8922
|
-
if response.get('_is_dataframe'):
|
|
8923
|
-
df = response['data']
|
|
8924
|
-
else:
|
|
8925
|
-
df = pd.DataFrame(response['data'])
|
|
9449
|
+
df = pd.DataFrame(response['data'])
|
|
8926
9450
|
if len(df) > 0:
|
|
8927
9451
|
all_data.append(df)
|
|
8928
9452
|
|
|
@@ -8983,11 +9507,7 @@ def preload_data_universal(config, data_requests=None, debug=False):
|
|
|
8983
9507
|
|
|
8984
9508
|
response = api_call(endpoint, cache_config, debug=debuginfo, **params)
|
|
8985
9509
|
if response and 'data' in response:
|
|
8986
|
-
|
|
8987
|
-
if response.get('_is_dataframe'):
|
|
8988
|
-
df = response['data'] # Already a DataFrame!
|
|
8989
|
-
else:
|
|
8990
|
-
df = pd.DataFrame(response['data'])
|
|
9510
|
+
df = pd.DataFrame(response['data'])
|
|
8991
9511
|
if len(df) > 0:
|
|
8992
9512
|
# Add cp type if not in response
|
|
8993
9513
|
if cp_type and 'type' not in df.columns and 'optionType' not in df.columns:
|
|
@@ -9009,11 +9529,7 @@ def preload_data_universal(config, data_requests=None, debug=False):
|
|
|
9009
9529
|
params = base_params.copy()
|
|
9010
9530
|
response = api_call(endpoint, cache_config, debug=debuginfo, **params)
|
|
9011
9531
|
if response and 'data' in response:
|
|
9012
|
-
|
|
9013
|
-
if response.get('_is_dataframe'):
|
|
9014
|
-
df = response['data']
|
|
9015
|
-
else:
|
|
9016
|
-
df = pd.DataFrame(response['data'])
|
|
9532
|
+
df = pd.DataFrame(response['data'])
|
|
9017
9533
|
if len(df) > 0:
|
|
9018
9534
|
all_data.append(df)
|
|
9019
9535
|
|
|
@@ -9259,6 +9775,27 @@ def _auto_detect_requests(config):
|
|
|
9259
9775
|
}
|
|
9260
9776
|
})
|
|
9261
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
|
+
|
|
9262
9799
|
return requests
|
|
9263
9800
|
|
|
9264
9801
|
|
|
@@ -9579,6 +10116,82 @@ def _auto_process_dates(df):
|
|
|
9579
10116
|
return df
|
|
9580
10117
|
|
|
9581
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
|
+
|
|
9582
10195
|
# ============================================================================
|
|
9583
10196
|
# INDICATOR PRE-CALCULATION SYSTEM - Universal helpers
|
|
9584
10197
|
# ============================================================================
|
|
@@ -10052,6 +10665,10 @@ def optimize_parameters(base_config, param_grid, strategy_function,
|
|
|
10052
10665
|
optimization_start_time = datetime.now()
|
|
10053
10666
|
start_time_str = optimization_start_time.strftime('%Y-%m-%d %H:%M:%S')
|
|
10054
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
|
+
|
|
10055
10672
|
print("\n" + "="*80)
|
|
10056
10673
|
print(" "*20 + "PARAMETER OPTIMIZATION")
|
|
10057
10674
|
print("="*80)
|
|
@@ -10059,9 +10676,11 @@ def optimize_parameters(base_config, param_grid, strategy_function,
|
|
|
10059
10676
|
print(f"Period: {base_config.get('start_date')} to {base_config.get('end_date')}")
|
|
10060
10677
|
print(f"Optimization Metric: {optimization_metric}")
|
|
10061
10678
|
print(f"Min Trades: {min_trades}")
|
|
10062
|
-
print(f"🕐 Started: {start_time_str}")
|
|
10063
10679
|
if max_drawdown_limit:
|
|
10064
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}")
|
|
10065
10684
|
print("="*80 + "\n")
|
|
10066
10685
|
|
|
10067
10686
|
# Generate all combinations
|
|
@@ -10201,6 +10820,42 @@ def optimize_parameters(base_config, param_grid, strategy_function,
|
|
|
10201
10820
|
# ✨ CRITICAL: Enable stop-loss when combined_* params are used
|
|
10202
10821
|
test_config['stop_loss_enabled'] = True
|
|
10203
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
|
+
|
|
10204
10859
|
# Update name
|
|
10205
10860
|
param_str = "_".join([f"{k}={v}" for k, v in zip(param_names, param_combo)])
|
|
10206
10861
|
test_config['strategy_name'] = f"{base_config.get('strategy_name', 'Strategy')} [{param_str}]"
|
|
@@ -11217,7 +11872,7 @@ class UniversalCacheManager:
|
|
|
11217
11872
|
__all__ = [
|
|
11218
11873
|
'BacktestResults', 'BacktestAnalyzer', 'ResultsReporter',
|
|
11219
11874
|
'ChartGenerator', 'ResultsExporter', 'run_backtest', 'run_backtest_with_stoploss',
|
|
11220
|
-
'init_api', 'api_call', 'APIHelper', 'APIManager',
|
|
11875
|
+
'init_api', 'api_call', 'get_api_data', 'is_api_response_valid', 'APIHelper', 'APIManager',
|
|
11221
11876
|
'ResourceMonitor', 'create_progress_bar', 'update_progress', 'format_time',
|
|
11222
11877
|
'StopLossManager', 'PositionManager', 'StopLossConfig',
|
|
11223
11878
|
'calculate_stoploss_metrics', 'print_stoploss_section', 'create_stoploss_charts',
|
|
@@ -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
|