ivolatility-backtesting 2.144__tar.gz → 2.146__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-2.144 → ivolatility_backtesting-2.146}/PKG-INFO +1 -1
- {ivolatility_backtesting-2.144 → ivolatility_backtesting-2.146}/ivolatility_backtesting/__init__.py +2 -2
- {ivolatility_backtesting-2.144 → ivolatility_backtesting-2.146}/ivolatility_backtesting/ivolatility_backtesting.py +222 -31
- {ivolatility_backtesting-2.144 → ivolatility_backtesting-2.146}/ivolatility_backtesting.egg-info/PKG-INFO +1 -1
- {ivolatility_backtesting-2.144 → ivolatility_backtesting-2.146}/ivolatility_backtesting.egg-info/SOURCES.txt +2 -1
- {ivolatility_backtesting-2.144 → ivolatility_backtesting-2.146}/pyproject.toml +1 -1
- ivolatility_backtesting-2.146/tests/test_2146_vix_vro.py +205 -0
- {ivolatility_backtesting-2.144 → ivolatility_backtesting-2.146}/README.md +0 -0
- {ivolatility_backtesting-2.144 → ivolatility_backtesting-2.146}/ivolatility_backtesting.egg-info/dependency_links.txt +0 -0
- {ivolatility_backtesting-2.144 → ivolatility_backtesting-2.146}/ivolatility_backtesting.egg-info/requires.txt +0 -0
- {ivolatility_backtesting-2.144 → ivolatility_backtesting-2.146}/ivolatility_backtesting.egg-info/top_level.txt +0 -0
- {ivolatility_backtesting-2.144 → ivolatility_backtesting-2.146}/setup.cfg +0 -0
- {ivolatility_backtesting-2.144 → ivolatility_backtesting-2.146}/tests/test_2142_fixes.py +0 -0
- {ivolatility_backtesting-2.144 → ivolatility_backtesting-2.146}/tests/test_2144_duckdb_dedup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ivolatility_backtesting
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.146
|
|
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-2.144 → ivolatility_backtesting-2.146}/ivolatility_backtesting/__init__.py
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from .ivolatility_backtesting import (
|
|
2
2
|
BacktestResults, BacktestAnalyzer, ResultsReporter,
|
|
3
3
|
ChartGenerator, ResultsExporter, run_backtest, run_backtest_with_stoploss,
|
|
4
|
-
run_backtest_notebook, print_signals_table,
|
|
4
|
+
run_backtest_notebook, print_signals_table, enable_output_logging,
|
|
5
5
|
init_api, api_call, APIHelper, APIManager,
|
|
6
6
|
ResourceMonitor, create_progress_bar, update_progress, format_time,
|
|
7
7
|
StopLossManager, PositionManager, StopLossConfig,
|
|
@@ -33,7 +33,7 @@ from .ivolatility_backtesting import (
|
|
|
33
33
|
__all__ = [
|
|
34
34
|
'BacktestResults', 'BacktestAnalyzer', 'ResultsReporter',
|
|
35
35
|
'ChartGenerator', 'ResultsExporter', 'run_backtest', 'run_backtest_with_stoploss',
|
|
36
|
-
'run_backtest_notebook', 'print_signals_table',
|
|
36
|
+
'run_backtest_notebook', 'print_signals_table', 'enable_output_logging',
|
|
37
37
|
'init_api', 'api_call', 'APIHelper', 'APIManager',
|
|
38
38
|
'ResourceMonitor', 'create_progress_bar', 'update_progress', 'format_time',
|
|
39
39
|
'StopLossManager', 'PositionManager', 'StopLossConfig',
|
|
@@ -179,8 +179,29 @@ _API_LOG_FILE = None # Track API log file path for summary printing
|
|
|
179
179
|
# ============================================================
|
|
180
180
|
from typing import Optional, Dict, Any, Tuple
|
|
181
181
|
|
|
182
|
+
_NUMERIC_COERCE_WARNED = False
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _as_data_row(source):
|
|
186
|
+
"""Normalize a stock/data row to a single record.
|
|
187
|
+
|
|
188
|
+
Callers legitimately pass either a row (``df.iloc[0]``) or a one-row slice
|
|
189
|
+
(``df[df['date'] == d]``). Only a 2-D object needs ``.iloc[0]``: applying it
|
|
190
|
+
to a Series would collapse it to its first cell.
|
|
191
|
+
"""
|
|
192
|
+
if source is None:
|
|
193
|
+
return None
|
|
194
|
+
try:
|
|
195
|
+
if getattr(source, 'ndim', 1) > 1:
|
|
196
|
+
return source.iloc[0] if len(source) > 0 else None
|
|
197
|
+
except Exception:
|
|
198
|
+
return source
|
|
199
|
+
return source
|
|
200
|
+
|
|
201
|
+
|
|
182
202
|
def _first_valid_numeric_value(source, columns) -> Optional[float]:
|
|
183
203
|
"""Return the first usable numeric value from source[column] in priority order."""
|
|
204
|
+
source = _as_data_row(source)
|
|
184
205
|
if source is None:
|
|
185
206
|
return None
|
|
186
207
|
for col in columns:
|
|
@@ -196,7 +217,16 @@ def _first_valid_numeric_value(source, columns) -> Optional[float]:
|
|
|
196
217
|
if value is None or pd.isna(value):
|
|
197
218
|
continue
|
|
198
219
|
return float(value)
|
|
199
|
-
except Exception:
|
|
220
|
+
except Exception as exc:
|
|
221
|
+
# Shape errors here used to vanish silently and surface later as a
|
|
222
|
+
# wrong number (fallback instead of the real price). Warn once per
|
|
223
|
+
# run so the cause is visible without flooding the notebook output.
|
|
224
|
+
global _NUMERIC_COERCE_WARNED
|
|
225
|
+
if not _NUMERIC_COERCE_WARNED:
|
|
226
|
+
_NUMERIC_COERCE_WARNED = True
|
|
227
|
+
print(f"\u26a0\ufe0f Could not read numeric field '{col}' from the data row "
|
|
228
|
+
f"({type(exc).__name__}: {exc}). Falling back to defaults — "
|
|
229
|
+
f"check that stock_row is a row or a one-row slice.")
|
|
200
230
|
continue
|
|
201
231
|
return None
|
|
202
232
|
|
|
@@ -266,14 +296,7 @@ def _resolve_runtime_underlying_price(stock_price=None, stock_row=None, options_
|
|
|
266
296
|
Resolve the best option-aligned underlying price for runtime P&L / SL logic.
|
|
267
297
|
Priority: stock_row runtime fields -> options underlying_price -> passed scalar.
|
|
268
298
|
"""
|
|
269
|
-
row_obj =
|
|
270
|
-
try:
|
|
271
|
-
if stock_row is not None and hasattr(stock_row, 'iloc') and len(stock_row) > 0:
|
|
272
|
-
row_obj = stock_row.iloc[0]
|
|
273
|
-
elif stock_row is not None:
|
|
274
|
-
row_obj = stock_row
|
|
275
|
-
except Exception:
|
|
276
|
-
row_obj = stock_row
|
|
299
|
+
row_obj = _as_data_row(stock_row)
|
|
277
300
|
|
|
278
301
|
value = _first_valid_numeric_value(
|
|
279
302
|
row_obj,
|
|
@@ -297,14 +320,7 @@ def _resolve_runtime_underlying_price(stock_price=None, stock_row=None, options_
|
|
|
297
320
|
|
|
298
321
|
def _resolve_runtime_stock_range(stock_row=None, fallback_price=None) -> Tuple[Optional[float], Optional[float]]:
|
|
299
322
|
"""Resolve runtime high/low in the same coordinate system as runtime price."""
|
|
300
|
-
row_obj =
|
|
301
|
-
try:
|
|
302
|
-
if stock_row is not None and hasattr(stock_row, 'iloc') and len(stock_row) > 0:
|
|
303
|
-
row_obj = stock_row.iloc[0]
|
|
304
|
-
elif stock_row is not None:
|
|
305
|
-
row_obj = stock_row
|
|
306
|
-
except Exception:
|
|
307
|
-
row_obj = stock_row
|
|
323
|
+
row_obj = _as_data_row(stock_row)
|
|
308
324
|
|
|
309
325
|
high = _first_valid_numeric_value(row_obj, ('runtime_high', 'high'))
|
|
310
326
|
low = _first_valid_numeric_value(row_obj, ('runtime_low', 'low'))
|
|
@@ -1515,6 +1531,7 @@ def report_results_folder(
|
|
|
1515
1531
|
return {"folder": folder, "strategies": strategies}
|
|
1516
1532
|
|
|
1517
1533
|
|
|
1534
|
+
|
|
1518
1535
|
def run_backtest_notebook(
|
|
1519
1536
|
base_config: dict,
|
|
1520
1537
|
strategy_fn,
|
|
@@ -12125,12 +12142,59 @@ def _is_am_settled_position(position):
|
|
|
12125
12142
|
if style == 'PM':
|
|
12126
12143
|
return False
|
|
12127
12144
|
root = str(position.get('opt_root') or position.get('symbol') or '').upper()
|
|
12145
|
+
if root == 'VIX':
|
|
12146
|
+
# Every VIX expiration (Wednesday, or Tue/Thu when shifted) is cash-settled
|
|
12147
|
+
# on the morning VRO print; the open is only the fallback proxy.
|
|
12148
|
+
return True
|
|
12128
12149
|
if root not in _AM_SETTLED_INDEX_ROOTS:
|
|
12129
12150
|
return False
|
|
12130
12151
|
exp = _as_date_obj(position.get('expiration'))
|
|
12131
12152
|
return exp is not None and exp.weekday() == 4 and 15 <= exp.day <= 21
|
|
12132
12153
|
|
|
12133
12154
|
|
|
12155
|
+
# Max calendar gap between a VIX expiration date and the VRO print that settles it.
|
|
12156
|
+
# Normal case: same day; an unscheduled exchange closure moves the print to the next trading day.
|
|
12157
|
+
_VRO_MAX_SHIFT_DAYS = 5
|
|
12158
|
+
|
|
12159
|
+
|
|
12160
|
+
def _vro_settlement_for(config, expiration):
|
|
12161
|
+
"""VRO settlement value for a VIX expiration, or None when unavailable.
|
|
12162
|
+
|
|
12163
|
+
The settling print is the first VRO row dated >= expiration (VIX expiry
|
|
12164
|
+
dates in the option data already carry the Tue/Wed shift; only unscheduled
|
|
12165
|
+
closures move the print to a later day). Returns None when config has no
|
|
12166
|
+
'_preloaded_vro' frame, the date is unparsable, or the nearest print is
|
|
12167
|
+
more than _VRO_MAX_SHIFT_DAYS away.
|
|
12168
|
+
"""
|
|
12169
|
+
exp = _as_date_obj(expiration)
|
|
12170
|
+
if exp is None or config is None:
|
|
12171
|
+
return None
|
|
12172
|
+
vro = config.get('_preloaded_vro')
|
|
12173
|
+
if vro is None:
|
|
12174
|
+
# Safety net for strategies that never went through preload_data_universal:
|
|
12175
|
+
# fetch once and keep it on the shared config (empty frame on failure).
|
|
12176
|
+
try:
|
|
12177
|
+
vro = _load_vro_series(config.get('start_date'), config.get('end_date'), config)
|
|
12178
|
+
except Exception:
|
|
12179
|
+
vro = pd.DataFrame(columns=['date', 'vro'])
|
|
12180
|
+
config['_preloaded_vro'] = vro
|
|
12181
|
+
if getattr(vro, 'empty', True):
|
|
12182
|
+
return None
|
|
12183
|
+
try:
|
|
12184
|
+
dates = pd.to_datetime(vro['date']).dt.date
|
|
12185
|
+
mask = dates >= exp
|
|
12186
|
+
if not mask.any():
|
|
12187
|
+
return None
|
|
12188
|
+
first_idx = dates[mask].idxmin() # earliest print on/after expiration
|
|
12189
|
+
first_date = dates.loc[first_idx]
|
|
12190
|
+
if (first_date - exp).days > _VRO_MAX_SHIFT_DAYS:
|
|
12191
|
+
return None
|
|
12192
|
+
value = float(vro.loc[first_idx, 'vro'])
|
|
12193
|
+
return value if value > 0 else None
|
|
12194
|
+
except Exception:
|
|
12195
|
+
return None
|
|
12196
|
+
|
|
12197
|
+
|
|
12134
12198
|
class PositionManager:
|
|
12135
12199
|
"""Universal Position Manager with automatic mode detection"""
|
|
12136
12200
|
|
|
@@ -12139,6 +12203,7 @@ class PositionManager:
|
|
|
12139
12203
|
self.closed_trades = []
|
|
12140
12204
|
self.config = config
|
|
12141
12205
|
self.debug = debug
|
|
12206
|
+
self._vro_missing_warned = set() # VIX expirations already reported as settled without VRO
|
|
12142
12207
|
self.debuginfo = config.get('debuginfo', 0) # Add debuginfo level
|
|
12143
12208
|
|
|
12144
12209
|
# AUTO-DETECT strategy_type if missing
|
|
@@ -12936,10 +13001,29 @@ class PositionManager:
|
|
|
12936
13001
|
position.get('underlying_entry_price', 0)
|
|
12937
13002
|
)
|
|
12938
13003
|
|
|
12939
|
-
#
|
|
12940
|
-
#
|
|
12941
|
-
|
|
13004
|
+
# Settlement reference, in priority order:
|
|
13005
|
+
# 1. exact settlement print (VIX -> VRO), set by build_price_data
|
|
13006
|
+
# 2. AM-settled index (SPX/NDX/... 3rd Friday, VIX any date):
|
|
13007
|
+
# expiration-day OPEN as the SOQ proxy
|
|
13008
|
+
# 3. close (everything else)
|
|
13009
|
+
settlement_source = 'close'
|
|
13010
|
+
_settle_px = None
|
|
13011
|
+
if position_id in price_data and isinstance(price_data[position_id], dict):
|
|
13012
|
+
_settle_px = price_data[position_id].get('settlement_price')
|
|
13013
|
+
if _settle_px and _settle_px > 0 and self.config.get('am_settlement_open', True):
|
|
13014
|
+
if self.debug and underlying_price and abs(_settle_px - underlying_price) > 1e-9:
|
|
13015
|
+
print(f"[SETTLEMENT] {position_id}: using settlement print "
|
|
13016
|
+
f"{_settle_px:.2f} (VRO) instead of close {underlying_price:.2f}")
|
|
13017
|
+
underlying_price = _settle_px
|
|
13018
|
+
settlement_source = 'vro'
|
|
13019
|
+
elif (underlying_price and self.config.get('am_settlement_open', True)
|
|
12942
13020
|
and _is_am_settled_position(position)):
|
|
13021
|
+
if str(position.get('opt_root') or position.get('symbol') or '').upper() == 'VIX':
|
|
13022
|
+
_exp_key = str(position.get('expiration'))[:10]
|
|
13023
|
+
if _exp_key not in self._vro_missing_warned:
|
|
13024
|
+
self._vro_missing_warned.add(_exp_key)
|
|
13025
|
+
print(f"WARNING: no VRO settlement print for VIX expiration {_exp_key} - "
|
|
13026
|
+
f"settling on the expiration-day open (SOQ proxy)")
|
|
12943
13027
|
_open_px = None
|
|
12944
13028
|
if position_id in price_data and isinstance(price_data[position_id], dict):
|
|
12945
13029
|
_open_px = price_data[position_id].get('underlying_open')
|
|
@@ -12948,6 +13032,7 @@ class PositionManager:
|
|
|
12948
13032
|
print(f"[AM SETTLEMENT] {position_id}: using expiration-day "
|
|
12949
13033
|
f"OPEN {_open_px:.2f} (SOQ proxy) instead of close {underlying_price:.2f}")
|
|
12950
13034
|
underlying_price = _open_px
|
|
13035
|
+
settlement_source = 'open'
|
|
12951
13036
|
|
|
12952
13037
|
strike = position.get('strike', 0)
|
|
12953
13038
|
strategy_type = position.get('strategy_type', '')
|
|
@@ -13049,7 +13134,11 @@ class PositionManager:
|
|
|
13049
13134
|
'pnl': current_pnl,
|
|
13050
13135
|
'pnl_pct': current_pnl_pct,
|
|
13051
13136
|
'settlement_type': 'intrinsic' if used_intrinsic else 'market',
|
|
13052
|
-
**close_kwargs # Include leg exit data automatically
|
|
13137
|
+
**close_kwargs, # Include leg exit data automatically
|
|
13138
|
+
# after close_kwargs: a custom strategy's generic kwargs must not
|
|
13139
|
+
# overwrite what the settlement actually used
|
|
13140
|
+
'settlement_source': settlement_source,
|
|
13141
|
+
'settlement_price': underlying_price if used_intrinsic else None,
|
|
13053
13142
|
}
|
|
13054
13143
|
|
|
13055
13144
|
to_close.append(stop_info)
|
|
@@ -13715,19 +13804,23 @@ class PositionManager:
|
|
|
13715
13804
|
if stock_row is not None:
|
|
13716
13805
|
# runtime_open = split-aligned open (REFERENCE §4a) — the
|
|
13717
13806
|
# AM-settlement reference must not use the adjusted series
|
|
13718
|
-
|
|
13719
|
-
|
|
13720
|
-
|
|
13721
|
-
|
|
13722
|
-
|
|
13723
|
-
|
|
13724
|
-
|
|
13725
|
-
|
|
13726
|
-
|
|
13807
|
+
# stock_row may arrive as a single-row slice (stock_df[mask]) —
|
|
13808
|
+
# take the row itself, otherwise .get() yields a Series and any
|
|
13809
|
+
# boolean/float use of it raises ValueError.
|
|
13810
|
+
_stock_open = _first_valid_numeric_value(stock_row, ('runtime_open', 'open'))
|
|
13811
|
+
if _stock_open is not None and _stock_open <= 0:
|
|
13812
|
+
_stock_open = None
|
|
13813
|
+
_settle_px = None
|
|
13814
|
+
if str(position.get('opt_root') or position.get('symbol') or '').upper() == 'VIX':
|
|
13815
|
+
_exp_d = _as_date_obj(position.get('expiration'))
|
|
13816
|
+
_cur_d = _as_date_obj(current_date)
|
|
13817
|
+
if _exp_d is not None and _cur_d is not None and _cur_d >= _exp_d:
|
|
13818
|
+
_settle_px = _vro_settlement_for(self.config, position.get('expiration'))
|
|
13727
13819
|
price_data[pos_id].update({
|
|
13728
13820
|
'underlying_high': stock_high,
|
|
13729
13821
|
'underlying_low': stock_low,
|
|
13730
13822
|
'underlying_open': _stock_open,
|
|
13823
|
+
'settlement_price': _settle_px,
|
|
13731
13824
|
'underlying_entry_price': position.get('underlying_entry_price', stock_price),
|
|
13732
13825
|
'current_date': current_date,
|
|
13733
13826
|
'symbol': position.get('symbol')
|
|
@@ -15265,6 +15358,24 @@ class BacktestAnalyzer:
|
|
|
15265
15358
|
self.metrics['avg_loss'] = losing['pnl'].mean() if len(losing) > 0 else 0
|
|
15266
15359
|
self.metrics['best_trade'] = trades_df['pnl'].max()
|
|
15267
15360
|
self.metrics['worst_trade'] = trades_df['pnl'].min()
|
|
15361
|
+
|
|
15362
|
+
# Requested vs realized tenor. A wide dte_tolerance (or a symbol with only
|
|
15363
|
+
# weekly expirations) can silently turn "1 DTE" into a ~6 DTE test.
|
|
15364
|
+
_dte_target = (getattr(getattr(self, 'results', None), 'config', None) or {}).get('dte_target')
|
|
15365
|
+
if 'entry_dte' in trades_df.columns:
|
|
15366
|
+
_dte = pd.to_numeric(trades_df['entry_dte'], errors='coerce').dropna()
|
|
15367
|
+
if len(_dte) > 0:
|
|
15368
|
+
self.metrics['dte_target'] = _dte_target
|
|
15369
|
+
self.metrics['dte_realized_median'] = float(_dte.median())
|
|
15370
|
+
self.metrics['dte_realized_min'] = int(_dte.min())
|
|
15371
|
+
self.metrics['dte_realized_max'] = int(_dte.max())
|
|
15372
|
+
try:
|
|
15373
|
+
_t = float(_dte_target)
|
|
15374
|
+
# dte_target 0 means "nearest expiration after the exit" (earnings
|
|
15375
|
+
# pattern) - realized DTE is expected to float there, no check.
|
|
15376
|
+
self.metrics['dte_mismatch'] = _t > 0 and abs(float(_dte.median()) - _t) > max(1.0, _t)
|
|
15377
|
+
except (TypeError, ValueError):
|
|
15378
|
+
self.metrics['dte_mismatch'] = False
|
|
15268
15379
|
|
|
15269
15380
|
if len(winning) > 0 and len(losing) > 0 and self.metrics['avg_loss'] != 0:
|
|
15270
15381
|
self.metrics['avg_win_loss_ratio'] = abs(self.metrics['avg_win'] / self.metrics['avg_loss'])
|
|
@@ -15708,6 +15819,25 @@ class ResultsReporter:
|
|
|
15708
15819
|
print("TRADING STATISTICS")
|
|
15709
15820
|
print("-"*80)
|
|
15710
15821
|
print(f"Total Trades: {m['total_trades']:>15}")
|
|
15822
|
+
if m.get('dte_realized_median') is not None:
|
|
15823
|
+
_tgt = m.get('dte_target')
|
|
15824
|
+
try:
|
|
15825
|
+
_tgt_s = f"{int(float(_tgt))}"
|
|
15826
|
+
except (TypeError, ValueError):
|
|
15827
|
+
_tgt_s = "n/a"
|
|
15828
|
+
print(f"DTE Target / Realized: {_tgt_s:>6} / median {m['dte_realized_median']:.0f}"
|
|
15829
|
+
f" (range {m['dte_realized_min']}-{m['dte_realized_max']} days at entry)")
|
|
15830
|
+
if m.get('dte_mismatch'):
|
|
15831
|
+
print("WARNING: realized DTE differs from the requested dte_target - the tenor actually")
|
|
15832
|
+
print(" traded is not the one asked for (check dte_tolerance / expiration calendar)")
|
|
15833
|
+
if not m.get('total_trades'):
|
|
15834
|
+
# 0 trades is almost never a real result — it usually means the entry
|
|
15835
|
+
# signal never fired (unread config key, indicators not computed).
|
|
15836
|
+
print("\u26a0\ufe0f 0 trades: the entry signal never fired — this is not a "
|
|
15837
|
+
"result, it is a run that never entered. Check that indicators were "
|
|
15838
|
+
"computed (look for 'Processing N cached indicators' — 0 means the "
|
|
15839
|
+
"signal could not be evaluated) and that the data loaded (a locked "
|
|
15840
|
+
"DuckDB cache or a failed fetch also ends in 0 trades).")
|
|
15711
15841
|
print(f"Winning Trades: {m['winning_trades']:>15}")
|
|
15712
15842
|
print(f"Losing Trades: {m['losing_trades']:>15}")
|
|
15713
15843
|
print(f"Win Rate: {m['win_rate']:>15.2f}% (% profitable trades)")
|
|
@@ -16755,6 +16885,17 @@ class ResultsExporter:
|
|
|
16755
16885
|
f.write(f"Sharpe: {m['sharpe']:.2f}\n")
|
|
16756
16886
|
f.write(f"Max DD: {m['max_drawdown']:.2f}%\n")
|
|
16757
16887
|
f.write(f"Trades: {m['total_trades']}\n")
|
|
16888
|
+
if m.get('dte_realized_median') is not None:
|
|
16889
|
+
_tgt = m.get('dte_target')
|
|
16890
|
+
try:
|
|
16891
|
+
_tgt_s = f"{int(float(_tgt))}"
|
|
16892
|
+
except (TypeError, ValueError):
|
|
16893
|
+
_tgt_s = "n/a"
|
|
16894
|
+
f.write(f"DTE Target / Realized: {_tgt_s} / median {m['dte_realized_median']:.0f}"
|
|
16895
|
+
f" (range {m['dte_realized_min']}-{m['dte_realized_max']} days at entry)\n")
|
|
16896
|
+
if m.get('dte_mismatch'):
|
|
16897
|
+
f.write("WARNING: realized DTE differs from the requested dte_target - "
|
|
16898
|
+
"the tenor actually traded is not the one asked for\n")
|
|
16758
16899
|
|
|
16759
16900
|
exported_files.append((f'{prefix}_summary.txt', ""))
|
|
16760
16901
|
if not silent:
|
|
@@ -18932,6 +19073,7 @@ def _try_read_from_duckdb_storage(config, cache_config, symbol, extended_start,
|
|
|
18932
19073
|
}
|
|
18933
19074
|
|
|
18934
19075
|
_load_futures_for_hedge(config, preloaded)
|
|
19076
|
+
_attach_vro_settlement(config, preloaded)
|
|
18935
19077
|
return preloaded
|
|
18936
19078
|
else:
|
|
18937
19079
|
_rich_print(f" ℹ️ Legacy mode — running full gap detection")
|
|
@@ -19473,6 +19615,7 @@ def _try_read_from_duckdb_storage(config, cache_config, symbol, extended_start,
|
|
|
19473
19615
|
|
|
19474
19616
|
# DON'T close conn - ChunkManager needs it!
|
|
19475
19617
|
_load_futures_for_hedge(config, preloaded)
|
|
19618
|
+
_attach_vro_settlement(config, preloaded)
|
|
19476
19619
|
return preloaded
|
|
19477
19620
|
|
|
19478
19621
|
else:
|
|
@@ -19615,6 +19758,7 @@ def _try_read_from_duckdb_storage(config, cache_config, symbol, extended_start,
|
|
|
19615
19758
|
}
|
|
19616
19759
|
|
|
19617
19760
|
_load_futures_for_hedge(config, preloaded)
|
|
19761
|
+
_attach_vro_settlement(config, preloaded)
|
|
19618
19762
|
return preloaded
|
|
19619
19763
|
|
|
19620
19764
|
except Exception as e:
|
|
@@ -19993,6 +20137,7 @@ def _preload_duckdb_SSD_storage(config, cache_config):
|
|
|
19993
20137
|
}
|
|
19994
20138
|
|
|
19995
20139
|
_load_futures_for_hedge(config, preloaded)
|
|
20140
|
+
_attach_vro_settlement(config, preloaded)
|
|
19996
20141
|
return preloaded
|
|
19997
20142
|
|
|
19998
20143
|
|
|
@@ -20887,6 +21032,7 @@ def _try_duckdb_preload(config, cache_config, debug=False):
|
|
|
20887
21032
|
}
|
|
20888
21033
|
|
|
20889
21034
|
_load_futures_for_hedge(config, preloaded)
|
|
21035
|
+
_attach_vro_settlement(config, preloaded)
|
|
20890
21036
|
return preloaded
|
|
20891
21037
|
|
|
20892
21038
|
|
|
@@ -23098,6 +23244,48 @@ def _preload_futures_to_duckdb(config, cache_config):
|
|
|
23098
23244
|
# ============================================================
|
|
23099
23245
|
# UNIVERSAL DATA PRELOADER V2 (NEW!)
|
|
23100
23246
|
# ============================================================
|
|
23247
|
+
def _vro_window_end(end_date):
|
|
23248
|
+
"""end_date + _VRO_MAX_SHIFT_DAYS as 'YYYY-MM-DD' (a print shifted past the last backtest day must still load)."""
|
|
23249
|
+
d = _as_date_obj(end_date)
|
|
23250
|
+
if d is None:
|
|
23251
|
+
return str(end_date)[:10]
|
|
23252
|
+
import datetime as _d
|
|
23253
|
+
return (d + _d.timedelta(days=_VRO_MAX_SHIFT_DAYS)).strftime('%Y-%m-%d')
|
|
23254
|
+
|
|
23255
|
+
|
|
23256
|
+
def _load_vro_series(start_date, end_date, config=None):
|
|
23257
|
+
"""VRO (VIX settlement print) as a DataFrame[date, vro]; empty frame on any failure."""
|
|
23258
|
+
if not start_date or not end_date:
|
|
23259
|
+
return pd.DataFrame(columns=['date', 'vro'])
|
|
23260
|
+
try:
|
|
23261
|
+
raw = get_api_data(api_call(MarketDataManager.ENDPOINT_STOCK_EOD,
|
|
23262
|
+
cache_config=(config or {}).get('cache_config'),
|
|
23263
|
+
symbol='VRO', from_=str(start_date)[:10], to=_vro_window_end(end_date)))
|
|
23264
|
+
if raw is None or raw.empty or 'date' not in raw.columns or 'close' not in raw.columns:
|
|
23265
|
+
print("WARNING: VRO settlement series unavailable - VIX expirations will settle on the open (SOQ proxy)")
|
|
23266
|
+
return pd.DataFrame(columns=['date', 'vro'])
|
|
23267
|
+
out = raw[['date', 'close']].rename(columns={'close': 'vro'}).copy()
|
|
23268
|
+
out['date'] = pd.to_datetime(out['date'])
|
|
23269
|
+
out = out.sort_values('date').drop_duplicates('date').reset_index(drop=True)
|
|
23270
|
+
print(f"VRO settlement series loaded: {len(out)} prints ({out['date'].min().date()} .. {out['date'].max().date()})")
|
|
23271
|
+
return out
|
|
23272
|
+
except Exception as e:
|
|
23273
|
+
print(f"WARNING: VRO settlement series failed to load ({e}) - VIX expirations will settle on the open (SOQ proxy)")
|
|
23274
|
+
return pd.DataFrame(columns=['date', 'vro'])
|
|
23275
|
+
|
|
23276
|
+
|
|
23277
|
+
def _attach_vro_settlement(config, preloaded):
|
|
23278
|
+
"""VIX only: put the VRO settlement series into preloaded['_preloaded_vro'] (idempotent)."""
|
|
23279
|
+
try:
|
|
23280
|
+
if str((config or {}).get('symbol', '')).upper() != 'VIX':
|
|
23281
|
+
return
|
|
23282
|
+
if '_preloaded_vro' in preloaded or '_preloaded_vro' in config:
|
|
23283
|
+
return
|
|
23284
|
+
preloaded['_preloaded_vro'] = _load_vro_series(config.get('start_date'), config.get('end_date'), config)
|
|
23285
|
+
except Exception as e:
|
|
23286
|
+
print(f"WARNING: VRO settlement series not attached ({e})")
|
|
23287
|
+
|
|
23288
|
+
|
|
23101
23289
|
def preload_data_universal(config, data_requests=None, debug=False):
|
|
23102
23290
|
"""
|
|
23103
23291
|
🚀 TRULY UNIVERSAL DATA PRELOADER - Works with ANY API endpoint!
|
|
@@ -23279,6 +23467,7 @@ def preload_data_universal(config, data_requests=None, debug=False):
|
|
|
23279
23467
|
set_option_patch_context(OptionPatchContext(symbol, _fetch_opt_intraday))
|
|
23280
23468
|
|
|
23281
23469
|
_activate_runtime_stock_shim(config, config)
|
|
23470
|
+
_attach_vro_settlement(config, config)
|
|
23282
23471
|
return config
|
|
23283
23472
|
|
|
23284
23473
|
# Start timing for data loading
|
|
@@ -24113,6 +24302,7 @@ def preload_data_universal(config, data_requests=None, debug=False):
|
|
|
24113
24302
|
|
|
24114
24303
|
_activate_runtime_stock_shim(config, preloaded)
|
|
24115
24304
|
_load_futures_for_hedge(config, preloaded)
|
|
24305
|
+
_attach_vro_settlement(config, preloaded)
|
|
24116
24306
|
return preloaded
|
|
24117
24307
|
|
|
24118
24308
|
|
|
@@ -25550,7 +25740,7 @@ def print_signals_table(config, indicator_cache, stock_df, save_path=None, silen
|
|
|
25550
25740
|
expected_ivx_key = ('iv_lean_zscore_ivx', (_cfg_dte, int(_cfg_lb))) if _cfg_lb is not None else None
|
|
25551
25741
|
|
|
25552
25742
|
if expected_raw_key is not None and expected_raw_key not in indicator_cache and should_print:
|
|
25553
|
-
present_lbs = sorted({k[1][0] for k in indicator_cache if k[0] == 'iv_lean_zscore' and isinstance(k[1], tuple)})
|
|
25743
|
+
present_lbs = sorted({k[1][0] for k in indicator_cache if k[0] == 'iv_lean_zscore' and isinstance(k[1], tuple) and k[1]})
|
|
25554
25744
|
print(f"⚠️ print_signals_table: no iv_lean_zscore cache for lookback={_cfg_lb} "
|
|
25555
25745
|
f"(cache has: {present_lbs}). Table will be empty for active config.")
|
|
25556
25746
|
|
|
@@ -26803,6 +26993,7 @@ def run_optimization(base_config, param_grid, strategy_function,
|
|
|
26803
26993
|
(combined_results_df, baselines_dict, results_folder)
|
|
26804
26994
|
baselines_dict: {symbol: metrics_dict}
|
|
26805
26995
|
"""
|
|
26996
|
+
|
|
26806
26997
|
import copy as _copy
|
|
26807
26998
|
|
|
26808
26999
|
if symbols is None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ivolatility_backtesting
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.146
|
|
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
|
|
@@ -9,4 +9,5 @@ ivolatility_backtesting.egg-info/dependency_links.txt
|
|
|
9
9
|
ivolatility_backtesting.egg-info/requires.txt
|
|
10
10
|
ivolatility_backtesting.egg-info/top_level.txt
|
|
11
11
|
tests/test_2142_fixes.py
|
|
12
|
-
tests/test_2144_duckdb_dedup.py
|
|
12
|
+
tests/test_2144_duckdb_dedup.py
|
|
13
|
+
tests/test_2146_vix_vro.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ivolatility_backtesting"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.146"
|
|
8
8
|
description = "A universal backtesting framework for financial strategies using the IVolatility API."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Tests for 2.146: VIX options settle against VRO (Cboe settlement print),
|
|
2
|
+
# not the VIX close; report shows requested vs realized DTE.
|
|
3
|
+
#
|
|
4
|
+
# Runs standalone (no pytest needed): python3 tests/test_2146_vix_vro.py
|
|
5
|
+
# Also pytest-compatible for CI: pytest tests/test_2146_vix_vro.py
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import traceback
|
|
9
|
+
from datetime import date
|
|
10
|
+
|
|
11
|
+
import matplotlib
|
|
12
|
+
matplotlib.use('Agg')
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
16
|
+
sys.path.insert(0, os.path.join(_REPO, 'ivolatility_backtesting'))
|
|
17
|
+
import ivolatility_backtesting as lib # noqa: E402
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _vro_frame():
|
|
21
|
+
# Real VRO prints (restapi stock-prices symbol=VRO): Wed 2024-12-18, the
|
|
22
|
+
# holiday-shifted Thu 2018-12-06 (expiration was Wed 2018-12-05).
|
|
23
|
+
return pd.DataFrame({
|
|
24
|
+
'date': pd.to_datetime(['2018-12-06', '2024-12-11', '2024-12-18', '2024-12-24']),
|
|
25
|
+
'vro': [24.60, 13.20, 15.66, 14.10],
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _open_short_vix_call(pm, pid='p1', strike=17.0, credit=6.0, exp=date(2024, 12, 18)):
|
|
30
|
+
# 1 contract sold at $0.06 -> total_cost 6.0 (credit); entry_price 0 marks SHORT
|
|
31
|
+
pm.open_position(position_id=pid, strategy_type='CALL', symbol='VIX',
|
|
32
|
+
entry_date=date(2024, 12, 16), entry_price=0.0,
|
|
33
|
+
quantity=100, contracts=1, total_cost=credit,
|
|
34
|
+
strike=strike, expiration=exp,
|
|
35
|
+
underlying_entry_price=14.69, opt_type='call')
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_vix_is_am_settled_any_date():
|
|
39
|
+
assert lib._is_am_settled_position({'symbol': 'VIX', 'expiration': date(2024, 12, 18)}) is True # Wed
|
|
40
|
+
assert lib._is_am_settled_position({'symbol': 'VIX', 'expiration': date(2024, 12, 24)}) is True # Tue
|
|
41
|
+
assert lib._is_am_settled_position({'opt_root': 'VIX', 'expiration': date(2018, 12, 5)}) is True
|
|
42
|
+
assert lib._is_am_settled_position({'symbol': 'VIX', 'expiration': date(2024, 12, 18),
|
|
43
|
+
'settlement_style': 'PM'}) is False
|
|
44
|
+
# unchanged for the existing roots
|
|
45
|
+
assert lib._is_am_settled_position({'symbol': 'SPX', 'expiration': date(2025, 7, 25)}) is False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_vro_lookup_same_day_and_holiday_shift():
|
|
49
|
+
cfg = {'_preloaded_vro': _vro_frame()}
|
|
50
|
+
assert lib._vro_settlement_for(cfg, date(2024, 12, 18)) == 15.66
|
|
51
|
+
assert lib._vro_settlement_for(cfg, '2024-12-18') == 15.66
|
|
52
|
+
assert lib._vro_settlement_for(cfg, pd.Timestamp('2024-12-24')) == 14.10
|
|
53
|
+
# expiration on an unscheduled closure -> next print
|
|
54
|
+
assert lib._vro_settlement_for(cfg, date(2018, 12, 5)) == 24.60
|
|
55
|
+
# too far from any print -> None (no silent fallback to a wrong week)
|
|
56
|
+
assert lib._vro_settlement_for(cfg, date(2024, 11, 1)) is None
|
|
57
|
+
assert lib._vro_settlement_for(cfg, date(2025, 1, 1)) is None
|
|
58
|
+
assert lib._vro_settlement_for({}, date(2024, 12, 18)) is None
|
|
59
|
+
assert lib._vro_settlement_for({'_preloaded_vro': pd.DataFrame(columns=['date', 'vro'])},
|
|
60
|
+
date(2024, 12, 18)) is None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_vro_lookup_unsorted_frame():
|
|
64
|
+
vro = pd.DataFrame({'date': pd.to_datetime(['2024-12-24', '2024-12-18', '2024-12-11']),
|
|
65
|
+
'vro': [14.10, 15.66, 13.20]})
|
|
66
|
+
assert lib._vro_settlement_for({'_preloaded_vro': vro}, date(2024, 12, 18)) == 15.66
|
|
67
|
+
assert lib._vro_settlement_for({'_preloaded_vro': vro}, date(2024, 12, 13)) == 15.66 # 5-day shift allowed
|
|
68
|
+
assert lib._vro_settlement_for({'_preloaded_vro': vro}, date(2024, 12, 12)) is None # 6 days: too far
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_am_settlement_open_false_disables_vro_too():
|
|
72
|
+
pm = lib.PositionManager({'strategy_type': 'CALL', 'symbol': 'VIX', 'am_settlement_open': False}, debug=False)
|
|
73
|
+
_open_short_vix_call(pm)
|
|
74
|
+
pd_data = {'p1': {'underlying_price': 27.62, 'underlying_open': 15.57, 'settlement_price': 15.66}}
|
|
75
|
+
si = pm.check_positions(date(2024, 12, 18), pd_data)[0]
|
|
76
|
+
assert abs(si['pnl'] - (6.0 - (27.62 - 17.0) * 100)) < 1e-6, si['pnl']
|
|
77
|
+
assert si['settlement_source'] == 'close'
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_settlement_uses_vro_over_close_and_open():
|
|
81
|
+
pm = lib.PositionManager({'strategy_type': 'CALL', 'symbol': 'VIX'}, debug=False)
|
|
82
|
+
_open_short_vix_call(pm)
|
|
83
|
+
# 2024-12-18: VIX open 15.57, close 27.62, VRO 15.66 -> strike 17 expires worthless
|
|
84
|
+
pd_data = {'p1': {'underlying_price': 27.62, 'underlying_open': 15.57, 'settlement_price': 15.66}}
|
|
85
|
+
si = pm.check_positions(date(2024, 12, 18), pd_data)[0]
|
|
86
|
+
assert abs(si['pnl'] - 6.0) < 1e-6, si['pnl']
|
|
87
|
+
assert si['settlement_source'] == 'vro', si.get('settlement_source')
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_settlement_vro_in_the_money():
|
|
91
|
+
pm = lib.PositionManager({'strategy_type': 'CALL', 'symbol': 'VIX'}, debug=False)
|
|
92
|
+
_open_short_vix_call(pm, strike=19.0, credit=10.0, exp=date(2018, 12, 5))
|
|
93
|
+
# settled Thu 2018-12-06 at VRO 24.60 (close was 21.19): loss = 10 - (24.60-19)*100 = -550
|
|
94
|
+
pd_data = {'p1': {'underlying_price': 21.19, 'underlying_open': 23.53, 'settlement_price': 24.60}}
|
|
95
|
+
si = pm.check_positions(date(2018, 12, 6), pd_data)[0]
|
|
96
|
+
assert abs(si['pnl'] - (-550.0)) < 1e-6, si['pnl']
|
|
97
|
+
assert si['settlement_source'] == 'vro'
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_settlement_falls_back_to_open_without_vro():
|
|
101
|
+
import io, contextlib
|
|
102
|
+
pm = lib.PositionManager({'strategy_type': 'CALL', 'symbol': 'VIX'}, debug=False)
|
|
103
|
+
_open_short_vix_call(pm)
|
|
104
|
+
pd_data = {'p1': {'underlying_price': 27.62, 'underlying_open': 15.57, 'settlement_price': None}}
|
|
105
|
+
buf = io.StringIO()
|
|
106
|
+
with contextlib.redirect_stdout(buf):
|
|
107
|
+
si = pm.check_positions(date(2024, 12, 18), pd_data)[0]
|
|
108
|
+
assert abs(si['pnl'] - 6.0) < 1e-6, si['pnl']
|
|
109
|
+
assert si['settlement_source'] == 'open'
|
|
110
|
+
assert 'no VRO settlement print for VIX expiration 2024-12-18' in buf.getvalue(), buf.getvalue()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_settlement_close_for_equity_unchanged():
|
|
114
|
+
pm = lib.PositionManager({'strategy_type': 'CALL', 'symbol': 'AAPL'}, debug=False)
|
|
115
|
+
pm.open_position(position_id='p1', strategy_type='CALL', symbol='AAPL',
|
|
116
|
+
entry_date=date(2025, 5, 2), entry_price=1000.0,
|
|
117
|
+
quantity=100, contracts=1, total_cost=1000.0,
|
|
118
|
+
strike=200.0, expiration=date(2025, 7, 18),
|
|
119
|
+
underlying_entry_price=201.0, opt_type='call')
|
|
120
|
+
pd_data = {'p1': {'underlying_price': 230.0, 'underlying_open': 225.0}}
|
|
121
|
+
si = pm.check_positions(date(2025, 7, 18), pd_data)[0]
|
|
122
|
+
assert abs(si['pnl'] - ((230.0 - 200.0) * 100 - 1000.0)) < 1.0, si['pnl']
|
|
123
|
+
assert si['settlement_source'] == 'close'
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_build_price_data_attaches_vro_for_vix():
|
|
127
|
+
cfg = {'strategy_type': 'CALL', 'symbol': 'VIX', '_preloaded_vro': _vro_frame()}
|
|
128
|
+
pm = lib.PositionManager(cfg, debug=False)
|
|
129
|
+
_open_short_vix_call(pm)
|
|
130
|
+
stock_row = pd.Series({'date': pd.Timestamp('2024-12-18'), 'open': 15.57, 'high': 28.32,
|
|
131
|
+
'low': 15.0, 'close': 27.62})
|
|
132
|
+
options_df = pd.DataFrame({'strike': [17.0], 'expiration': [pd.Timestamp('2024-12-18')],
|
|
133
|
+
'opt_type': ['C'], 'bid': [10.5], 'ask': [10.7], 'price': [10.6]})
|
|
134
|
+
|
|
135
|
+
def _get_opt(*a, **k):
|
|
136
|
+
return {'bid': 10.5, 'ask': 10.7, 'price': 10.6, 'mid': 10.6}
|
|
137
|
+
|
|
138
|
+
pdata = pm.build_price_data(date(2024, 12, 18), 27.62, options_df, _get_opt, stock_row=stock_row)
|
|
139
|
+
assert pdata['p1'].get('settlement_price') == 15.66, pdata['p1'].get('settlement_price')
|
|
140
|
+
assert pdata['p1'].get('underlying_open') == 15.57
|
|
141
|
+
# before expiration the print is not exposed (no look-ahead field on live bars)
|
|
142
|
+
pre = pm.build_price_data(date(2024, 12, 17), 14.7, options_df, _get_opt, stock_row=stock_row)
|
|
143
|
+
assert pre['p1'].get('settlement_price') is None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_enable_output_logging_is_exported():
|
|
147
|
+
# the master prompt tells generated scripts to `from ivolatility_backtesting import enable_output_logging`
|
|
148
|
+
from ivolatility_backtesting import enable_output_logging # noqa: F401
|
|
149
|
+
assert callable(enable_output_logging)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_vro_window_end_extends_past_end_date():
|
|
153
|
+
assert lib._vro_window_end(date(2018, 12, 5)) == '2018-12-10'
|
|
154
|
+
assert lib._vro_window_end('2018-12-05') == '2018-12-10'
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_dte_realized_metrics_and_warning():
|
|
158
|
+
trades = [
|
|
159
|
+
{'pnl': 10.0, 'entry_dte': 6, 'entry_date': date(2024, 1, 4), 'exit_date': date(2024, 1, 10)},
|
|
160
|
+
{'pnl': -5.0, 'entry_dte': 6, 'entry_date': date(2024, 1, 11), 'exit_date': date(2024, 1, 17)},
|
|
161
|
+
{'pnl': 8.0, 'entry_dte': 1, 'entry_date': date(2024, 1, 23), 'exit_date': date(2024, 1, 24)},
|
|
162
|
+
]
|
|
163
|
+
res = lib.BacktestResults(equity_curve=[100000, 100010, 100005, 100013],
|
|
164
|
+
equity_dates=[date(2024, 1, 2), date(2024, 1, 10), date(2024, 1, 17), date(2024, 1, 24)],
|
|
165
|
+
trades=trades, initial_capital=100000,
|
|
166
|
+
config={'dte_target': 1, 'dte_tolerance': 7})
|
|
167
|
+
an = lib.BacktestAnalyzer(res)
|
|
168
|
+
an.calculate_all_metrics()
|
|
169
|
+
m = an.metrics
|
|
170
|
+
assert m['dte_target'] == 1 and m['dte_realized_median'] == 6 and m['dte_realized_min'] == 1 \
|
|
171
|
+
and m['dte_realized_max'] == 6, m
|
|
172
|
+
assert m['dte_mismatch'] is True
|
|
173
|
+
|
|
174
|
+
res2 = lib.BacktestResults(equity_curve=[100000, 100010], equity_dates=[date(2024, 1, 2), date(2024, 1, 10)],
|
|
175
|
+
trades=[{'pnl': 1.0, 'entry_dte': 30, 'entry_date': date(2024, 1, 2), 'exit_date': date(2024, 1, 10)}],
|
|
176
|
+
initial_capital=100000, config={'dte_target': 30})
|
|
177
|
+
an2 = lib.BacktestAnalyzer(res2)
|
|
178
|
+
an2.calculate_all_metrics()
|
|
179
|
+
assert an2.metrics['dte_mismatch'] is False
|
|
180
|
+
|
|
181
|
+
# earnings pattern: dte_target 0 = nearest expiration after exit -> never a mismatch
|
|
182
|
+
res3 = lib.BacktestResults(equity_curve=[100000, 100010], equity_dates=[date(2024, 1, 2), date(2024, 1, 10)],
|
|
183
|
+
trades=[{'pnl': 1.0, 'entry_dte': 12, 'entry_date': date(2024, 1, 2), 'exit_date': date(2024, 1, 10)}],
|
|
184
|
+
initial_capital=100000, config={'dte_target': 0, 'dte_tolerance': 29})
|
|
185
|
+
an3 = lib.BacktestAnalyzer(res3)
|
|
186
|
+
an3.calculate_all_metrics()
|
|
187
|
+
assert an3.metrics['dte_mismatch'] is False and an3.metrics['dte_realized_median'] == 12
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ---------------------------------------------------------------- runner
|
|
191
|
+
|
|
192
|
+
if __name__ == '__main__':
|
|
193
|
+
tests = [(n, f) for n, f in sorted(globals().items())
|
|
194
|
+
if n.startswith('test_') and callable(f)]
|
|
195
|
+
failed = []
|
|
196
|
+
for name, fn in tests:
|
|
197
|
+
try:
|
|
198
|
+
fn()
|
|
199
|
+
print(f"PASS {name}")
|
|
200
|
+
except Exception:
|
|
201
|
+
failed.append(name)
|
|
202
|
+
print(f"FAIL {name}")
|
|
203
|
+
traceback.print_exc()
|
|
204
|
+
print(f"\n{len(tests) - len(failed)}/{len(tests)} passed")
|
|
205
|
+
sys.exit(1 if failed else 0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ivolatility_backtesting-2.144 → ivolatility_backtesting-2.146}/tests/test_2144_duckdb_dedup.py
RENAMED
|
File without changes
|