ivolatility-backtesting 2.144__tar.gz → 2.145__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ivolatility_backtesting
3
- Version: 2.144
3
+ Version: 2.145
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
@@ -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 = None
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 = None
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,
@@ -13715,15 +13732,12 @@ class PositionManager:
13715
13732
  if stock_row is not None:
13716
13733
  # runtime_open = split-aligned open (REFERENCE §4a) — the
13717
13734
  # AM-settlement reference must not use the adjusted series
13718
- _stock_open = None
13719
- for _oc in ('runtime_open', 'open'):
13720
- try:
13721
- _ov = stock_row.get(_oc) if hasattr(stock_row, 'get') else stock_row[_oc]
13722
- except (KeyError, IndexError, TypeError):
13723
- _ov = None
13724
- if _ov is not None and not pd.isna(_ov) and float(_ov) > 0:
13725
- _stock_open = float(_ov)
13726
- break
13735
+ # stock_row may arrive as a single-row slice (stock_df[mask]) —
13736
+ # take the row itself, otherwise .get() yields a Series and any
13737
+ # boolean/float use of it raises ValueError.
13738
+ _stock_open = _first_valid_numeric_value(stock_row, ('runtime_open', 'open'))
13739
+ if _stock_open is not None and _stock_open <= 0:
13740
+ _stock_open = None
13727
13741
  price_data[pos_id].update({
13728
13742
  'underlying_high': stock_high,
13729
13743
  'underlying_low': stock_low,
@@ -15708,6 +15722,14 @@ class ResultsReporter:
15708
15722
  print("TRADING STATISTICS")
15709
15723
  print("-"*80)
15710
15724
  print(f"Total Trades: {m['total_trades']:>15}")
15725
+ if not m.get('total_trades'):
15726
+ # 0 trades is almost never a real result — it usually means the entry
15727
+ # signal never fired (unread config key, indicators not computed).
15728
+ print("\u26a0\ufe0f 0 trades: the entry signal never fired — this is not a "
15729
+ "result, it is a run that never entered. Check that indicators were "
15730
+ "computed (look for 'Processing N cached indicators' — 0 means the "
15731
+ "signal could not be evaluated) and that the data loaded (a locked "
15732
+ "DuckDB cache or a failed fetch also ends in 0 trades).")
15711
15733
  print(f"Winning Trades: {m['winning_trades']:>15}")
15712
15734
  print(f"Losing Trades: {m['losing_trades']:>15}")
15713
15735
  print(f"Win Rate: {m['win_rate']:>15.2f}% (% profitable trades)")
@@ -25550,7 +25572,7 @@ def print_signals_table(config, indicator_cache, stock_df, save_path=None, silen
25550
25572
  expected_ivx_key = ('iv_lean_zscore_ivx', (_cfg_dte, int(_cfg_lb))) if _cfg_lb is not None else None
25551
25573
 
25552
25574
  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)})
25575
+ 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
25576
  print(f"⚠️ print_signals_table: no iv_lean_zscore cache for lookback={_cfg_lb} "
25555
25577
  f"(cache has: {present_lbs}). Table will be empty for active config.")
25556
25578
 
@@ -26803,6 +26825,7 @@ def run_optimization(base_config, param_grid, strategy_function,
26803
26825
  (combined_results_df, baselines_dict, results_folder)
26804
26826
  baselines_dict: {symbol: metrics_dict}
26805
26827
  """
26828
+
26806
26829
  import copy as _copy
26807
26830
 
26808
26831
  if symbols is None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ivolatility_backtesting
3
- Version: 2.144
3
+ Version: 2.145
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 = "2.144"
7
+ version = "2.145"
8
8
  description = "A universal backtesting framework for financial strategies using the IVolatility API."
9
9
  readme = "README.md"
10
10
  authors = [