ivolatility-backtesting 1.14.0__tar.gz → 1.18.0__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.

Potentially problematic release.


This version of ivolatility-backtesting might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ivolatility_backtesting
3
- Version: 1.14.0
3
+ Version: 1.18.0
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
@@ -2234,6 +2234,10 @@ class StopLossConfig:
2234
2234
 
2235
2235
  NEW METHOD:
2236
2236
  - combined(): Requires BOTH pl_loss AND directional conditions
2237
+
2238
+ IMPORTANT:
2239
+ - directional(): Creates EOD directional stop (checked once per day)
2240
+ - For INTRADAY directional stops, use INTRADAY_STOPS_CONFIG (separate system)
2237
2241
  """
2238
2242
 
2239
2243
  @staticmethod
@@ -2333,7 +2337,7 @@ class StopLossConfig:
2333
2337
 
2334
2338
  @staticmethod
2335
2339
  def directional(pct):
2336
- """Directional stop based on underlying movement"""
2340
+ """EOD directional stop based on underlying movement (checked once per day)"""
2337
2341
  decimal = StopLossConfig._normalize_pct(pct)
2338
2342
  display = StopLossConfig._format_pct(pct)
2339
2343
 
@@ -2341,8 +2345,8 @@ class StopLossConfig:
2341
2345
  'enabled': True,
2342
2346
  'type': 'directional',
2343
2347
  'value': decimal,
2344
- 'name': f'Directional {display}',
2345
- 'description': f'Stop when underlying moves {display}'
2348
+ 'name': f'EOD Directional {display}',
2349
+ 'description': f'Stop when underlying moves {display} (checked at EOD)'
2346
2350
  }
2347
2351
 
2348
2352
  # ========================================================
@@ -3288,29 +3292,19 @@ def optimize_parameters(base_config, param_grid, strategy_function,
3288
3292
  test_config['_preloaded_options_cache'] = preloaded_options_df
3289
3293
  # Otherwise, data is already in base_config from preload_data_universal
3290
3294
 
3291
- # Update progress
3292
- if has_widgets:
3293
- # Use update_progress for full display with ETA, CPU, RAM
3294
- update_progress(
3295
- progress_bar, status_label, monitor,
3296
- current=idx,
3297
- total=total_combinations,
3298
- start_time=start_time,
3299
- message=f"Testing: {param_str}"
3300
- )
3301
- else:
3302
- if idx % max(1, total_combinations // 10) == 0:
3303
- print(f"[{idx}/{total_combinations}] {param_str}")
3304
-
3305
- # ═══ MODIFY run_backtest CALL (lines ~2240-2248) ═══
3295
+ # ═══ CREATE COMPACT PARAMETER STRING EARLY (for progress display) ═══
3306
3296
  try:
3307
- # Create compact parameter string (e.g., Z1.0_E0.1_PT20)
3297
+ # Create compact parameter string (e.g., Z1.0_E0.1_L60_DT45)
3308
3298
  param_parts = []
3309
3299
  for name, value in zip(param_names, param_combo):
3310
3300
  if 'z_score_entry' in name:
3311
3301
  param_parts.append(f"Z{value}")
3312
3302
  elif 'z_score_exit' in name:
3313
3303
  param_parts.append(f"E{value}")
3304
+ elif 'lookback' in name:
3305
+ param_parts.append(f"L{value}")
3306
+ elif 'dte' in name:
3307
+ param_parts.append(f"DT{value}")
3314
3308
  elif 'profit_target' in name:
3315
3309
  if value is None:
3316
3310
  param_parts.append("PTNo")
@@ -3325,13 +3319,70 @@ def optimize_parameters(base_config, param_grid, strategy_function,
3325
3319
 
3326
3320
  compact_params = "_".join(param_parts)
3327
3321
 
3328
- # Create combo folder: c01_Z1.0_E0.1_PT20
3329
- combo_folder = os.path.join(results_folder, f'c{idx:02d}_{compact_params}')
3330
- os.makedirs(combo_folder, exist_ok=True)
3322
+ # Create friendly string for display/logging
3323
+ param_display = ", ".join([f"{name}={value}" for name, value in zip(param_names, param_combo)])
3324
+
3325
+ # Add SL prefix if provided (from notebook loop)
3326
+ sl_prefix = test_config.get('_sl_prefix', '')
3327
+ if sl_prefix:
3328
+ # Format: SL3_cb1_Z1_E0.1_L60_DT45
3329
+ combo_name = f"{sl_prefix}_cb{idx}_{compact_params}"
3330
+ display_name = f"{sl_prefix}_{compact_params}"
3331
+ else:
3332
+ # Fallback: Add stop-loss to filename if enabled
3333
+ if test_config.get('stop_loss_enabled') and 'stop_loss_config' in test_config:
3334
+ sl_value = test_config['stop_loss_config'].get('value', 0)
3335
+ combo_name = f"cb{idx}_{compact_params}_SL{int(sl_value*100)}"
3336
+ display_name = f"{compact_params}_SL{int(sl_value*100)}"
3337
+ else:
3338
+ combo_name = f"cb{idx}_{compact_params}"
3339
+ display_name = compact_params
3331
3340
 
3332
- # File prefix: c01_Z1.0_E0.1_PT20
3333
- combo_prefix = f"c{idx:02d}_{compact_params}"
3341
+ # -----------------------------
3342
+ # Print combo header BEFORE running backtest (so user sees params)
3343
+ # -----------------------------
3344
+ print("\n" + "="*80)
3345
+ print(f"[{idx}/{total_combinations}] {combo_name}")
3346
+ print("="*80)
3347
+ print(f"• Parameters : {param_display}")
3348
+ if test_config.get('stop_loss_enabled') and 'stop_loss_config' in test_config:
3349
+ sl_cfg = test_config['stop_loss_config']
3350
+ sl_type = sl_cfg.get('type', 'unknown')
3351
+ sl_value = sl_cfg.get('value')
3352
+ if isinstance(sl_value, (int, float)):
3353
+ sl_value_display = f"{sl_value*100:.2f}%" if sl_type in ('pl_loss', 'fixed_pct', 'trailing', 'directional') else sl_value
3354
+ else:
3355
+ sl_value_display = sl_value
3356
+ print(f"• Stop-loss : {sl_type} -> {sl_value_display}")
3357
+ else:
3358
+ print("• Stop-loss : disabled")
3359
+
3360
+ intraday_cfg = test_config.get('intraday_stops', {})
3361
+ if intraday_cfg.get('enabled', False):
3362
+ intraday_pct = intraday_cfg.get('stop_pct')
3363
+ pct_text = f"{intraday_pct*100:.2f}%" if isinstance(intraday_pct, (int, float)) else intraday_pct
3364
+ print(f"• Intraday SL: enabled ({pct_text}, min_days={intraday_cfg.get('min_days_before_intraday', 'n/a')})")
3365
+ else:
3366
+ print("• Intraday SL: disabled")
3367
+ print("-"*80)
3368
+
3369
+ # Update progress with compact name (after printing parameters)
3370
+ if has_widgets:
3371
+ update_progress(
3372
+ progress_bar, status_label, monitor,
3373
+ current=idx,
3374
+ total=total_combinations,
3375
+ start_time=start_time,
3376
+ message=f"Testing: {display_name}"
3377
+ )
3378
+
3379
+ # Create combo folder: SL3_c01_Z1.0_E0.1_PT20
3380
+ combo_folder = os.path.join(results_folder, combo_name)
3381
+ os.makedirs(combo_folder, exist_ok=True)
3334
3382
 
3383
+ # File prefix: SL3_c01_Z1.0_E0.1_PT20
3384
+ combo_prefix = combo_name
3385
+
3335
3386
  # Run backtest WITH EXPORT AND CHARTS (saved but not displayed)
3336
3387
  analyzer = run_backtest(
3337
3388
  strategy_function,
@@ -3360,10 +3411,6 @@ def optimize_parameters(base_config, param_grid, strategy_function,
3360
3411
  status_symbol = "✓" if is_valid else "✗"
3361
3412
  status_color = "#00cc00" if is_valid else "#ff6666"
3362
3413
 
3363
- # Print combination header
3364
- print(f"[{idx}/{total_combinations}] {param_str}")
3365
- print("-" * 100)
3366
-
3367
3414
  # Print chart file if created
3368
3415
  if hasattr(analyzer, 'chart_file') and analyzer.chart_file:
3369
3416
  print(f"Chart saved: {analyzer.chart_file}")
@@ -3400,7 +3447,7 @@ def optimize_parameters(base_config, param_grid, strategy_function,
3400
3447
  resource_text = f"CPU: {cpu_pct:.0f}% | RAM: {ram_mb:.0f}MB"
3401
3448
 
3402
3449
  status_label.value = (
3403
- f"<b style='color:{status_color}'>[{idx}/{total_combinations}] {param_str}</b><br>"
3450
+ f"<b style='color:{status_color}'>[{idx}/{total_combinations}] {combo_name}</b><br>"
3404
3451
  f"<span style='color:#666'>{result_text}</span><br>"
3405
3452
  f"<span style='color:#999;font-size:10px'>{resource_text}</span>"
3406
3453
  )
@@ -3422,12 +3469,28 @@ def optimize_parameters(base_config, param_grid, strategy_function,
3422
3469
  'avg_win': analyzer.metrics['avg_win'],
3423
3470
  'avg_loss': analyzer.metrics['avg_loss'],
3424
3471
  'volatility': analyzer.metrics['volatility'],
3472
+ 'combo_name': combo_name,
3473
+ 'combo_folder': combo_folder if export_each_combo else "",
3425
3474
  }
3426
3475
 
3427
3476
  results.append(result)
3428
3477
 
3429
3478
  # ═══ MEMORY CLEANUP AFTER EACH TEST ═══
3430
3479
  # Delete large objects to free RAM for next iteration
3480
+
3481
+ # Clear references to preloaded data (prevents memory leaks)
3482
+ if use_legacy_preload:
3483
+ # Legacy preload method
3484
+ if '_preloaded_lean_df' in test_config:
3485
+ del test_config['_preloaded_lean_df']
3486
+ if '_preloaded_options_cache' in test_config:
3487
+ del test_config['_preloaded_options_cache']
3488
+ else:
3489
+ # Universal preloader - clear all preloaded keys
3490
+ for key in list(test_config.keys()):
3491
+ if key.startswith('_preloaded_'):
3492
+ del test_config[key]
3493
+
3431
3494
  del analyzer, test_config
3432
3495
  gc.collect()
3433
3496
 
@@ -3623,59 +3686,132 @@ def optimize_parameters(base_config, param_grid, strategy_function,
3623
3686
  # ═══════════════════════════════════════════════════════════════════════════
3624
3687
  # NEW! FULL BACKTEST OF BEST COMBINATION WITH ALL CHARTS
3625
3688
  # ═══════════════════════════════════════════════════════════════════════════
3626
- print("\n" + "="*80)
3627
- print(" "*15 + "RUNNING FULL BACKTEST FOR BEST COMBINATION")
3628
- print("="*80)
3629
- print("\n📊 Creating detailed report for best combination...")
3630
- print(f"Parameters: {', '.join([f'{k}={v}' for k, v in best_params.items()])}\n")
3631
-
3632
- # Create config for best combination
3633
- best_config = base_config.copy()
3634
- best_config.update(best_params)
3635
- if use_legacy_preload:
3636
- best_config['_preloaded_lean_df'] = preloaded_lean_df
3637
- best_config['_preloaded_options_cache'] = preloaded_options_df
3638
-
3639
- # Create folder for best combination
3640
- best_combo_folder = os.path.join(results_folder, 'best_combination')
3641
- os.makedirs(best_combo_folder, exist_ok=True)
3642
-
3643
- # Run FULL backtest with ALL charts and exports
3644
- # Note: progress_context=None, so plt.show() will be called but fail due to renderer
3645
- # We'll display charts explicitly afterwards using IPython.display.Image
3646
- best_analyzer = run_backtest(
3647
- strategy_function,
3648
- best_config,
3649
- print_report=True, # ← SHOW FULL REPORT
3650
- create_charts=True, # ← CREATE ALL CHARTS
3651
- export_results=True, # ← EXPORT ALL FILES
3652
- progress_context=None, # ← Normal mode
3653
- chart_filename=os.path.join(best_combo_folder, 'equity_curve.png'),
3654
- export_prefix=os.path.join(best_combo_folder, 'best')
3655
- )
3656
-
3657
- # Save detailed metrics to optimization_metrics.csv
3658
- metrics_data = {
3659
- 'metric': list(best_analyzer.metrics.keys()),
3660
- 'value': list(best_analyzer.metrics.values())
3661
- }
3662
- metrics_df = pd.DataFrame(metrics_data)
3663
- metrics_path = os.path.join(results_folder, 'optimization_metrics.csv')
3664
- metrics_df.to_csv(metrics_path, index=False)
3665
3689
 
3666
- print(f"\n✓ Detailed metrics saved: {metrics_path}")
3667
- print(f"✓ Best combination results saved to: {best_combo_folder}/")
3690
+ # Create compact parameter string for best combination
3691
+ param_parts = []
3692
+ for name, value in best_params.items():
3693
+ if 'z_score_entry' in name:
3694
+ param_parts.append(f"Z{value}")
3695
+ elif 'z_score_exit' in name:
3696
+ param_parts.append(f"E{value}")
3697
+ elif 'lookback' in name:
3698
+ param_parts.append(f"L{value}")
3699
+ elif 'dte' in name:
3700
+ param_parts.append(f"DT{value}")
3701
+ elif 'stop_loss' in name:
3702
+ param_parts.append(f"SL{int(value*100)}")
3703
+
3704
+ best_params_str = "_".join(param_parts) if param_parts else "best"
3705
+
3706
+ # Add SL prefix if provided (from notebook loop)
3707
+ sl_prefix = base_config.get('_sl_prefix', '')
3708
+ if sl_prefix:
3709
+ best_params_str_with_prefix = f"{sl_prefix}_{best_params_str}"
3710
+ else:
3711
+ best_params_str_with_prefix = best_params_str
3668
3712
 
3669
- # ═══════════════════════════════════════════════════════════════════════════
3670
- # DISPLAY CHARTS FOR BEST COMBINATION IN NOTEBOOK
3671
- # ═══════════════════════════════════════════════════════════════════════════
3672
- try:
3673
- # Charts are displayed in the notebook, not here
3674
- chart_file = os.path.join(best_combo_folder, 'equity_curve.png')
3713
+ metrics_path = os.path.join(results_folder, 'optimization_metrics.csv')
3714
+
3715
+ best_combo_name = best_result.get('combo_name', '')
3716
+ best_combo_folder_recorded = best_result.get('combo_folder', '')
3717
+
3718
+ if export_each_combo and best_combo_name and best_combo_folder_recorded and os.path.exists(best_combo_folder_recorded):
3719
+ print("\n" + "="*80)
3720
+ print(" "*15 + "USING EXISTING ARTIFACTS FOR BEST COMBINATION")
3721
+ print("="*80)
3722
+ print("\n⚡ export_each_combo=True → пропускаем повторный запуск")
3723
+ print("⚡ Используем ранее сохранённые файлы комбинации")
3724
+ print(f"Комбинация: {best_combo_name}")
3725
+ print(f"Путь: {best_combo_folder_recorded}")
3726
+
3727
+ metrics_dict = None
3728
+ metrics_json_path = os.path.join(best_combo_folder_recorded, f"{best_combo_name}_metrics.json")
3729
+ if os.path.exists(metrics_json_path):
3730
+ try:
3731
+ import json
3732
+ with open(metrics_json_path, 'r') as mj:
3733
+ metrics_dict = json.load(mj)
3734
+ except Exception as e:
3735
+ print(f"⚠ Не удалось загрузить {metrics_json_path}: {e}")
3736
+
3737
+ if metrics_dict is None:
3738
+ metrics_dict = {
3739
+ 'total_return': best_result['total_return'],
3740
+ 'sharpe': best_result['sharpe'],
3741
+ 'sortino': best_result.get('sortino'),
3742
+ 'calmar': best_result.get('calmar'),
3743
+ 'max_drawdown': best_result['max_drawdown'],
3744
+ 'win_rate': best_result['win_rate'],
3745
+ 'profit_factor': best_result['profit_factor'],
3746
+ 'total_trades': best_result['total_trades'],
3747
+ 'avg_win': best_result['avg_win'],
3748
+ 'avg_loss': best_result['avg_loss'],
3749
+ 'volatility': best_result['volatility'],
3750
+ }
3751
+
3752
+ metrics_df = pd.DataFrame({
3753
+ 'metric': list(metrics_dict.keys()),
3754
+ 'value': list(metrics_dict.values())
3755
+ })
3756
+ metrics_df.to_csv(metrics_path, index=False)
3757
+ print(f"\n✓ Метрики сохранены: {metrics_path} (использованы существующие данные)")
3758
+ print(f"✓ Артефакты лучшей комбинации: {best_combo_folder_recorded}/")
3759
+
3760
+ chart_file = os.path.join(best_combo_folder_recorded, 'equity_curve.png')
3675
3761
  if os.path.exists(chart_file):
3676
- print(f"\n📈 Best combination charts saved to: {chart_file}")
3677
- except Exception as e:
3678
- print(f"\n⚠ Could not display charts (saved to {best_combo_folder}/): {e}")
3762
+ print(f"📈 График equity: {chart_file}")
3763
+ summary_file = os.path.join(best_combo_folder_recorded, f"{best_combo_name}_summary.txt")
3764
+ if os.path.exists(summary_file):
3765
+ print(f"📄 Summary: {summary_file}")
3766
+
3767
+ else:
3768
+ print("\n" + "="*80)
3769
+ print(" "*15 + "RUNNING FULL BACKTEST FOR BEST COMBINATION")
3770
+ print("="*80)
3771
+ print("\n📊 Creating detailed report for best combination...")
3772
+ print(f"Parameters: {', '.join([f'{k}={v}' for k, v in best_params.items()])}")
3773
+ print(f"Files will be saved with prefix: BST_{best_params_str_with_prefix}_*\n")
3774
+
3775
+ # Create config for best combination
3776
+ best_config = base_config.copy()
3777
+ best_config.update(best_params)
3778
+ if use_legacy_preload:
3779
+ best_config['_preloaded_lean_df'] = preloaded_lean_df
3780
+ best_config['_preloaded_options_cache'] = preloaded_options_df
3781
+
3782
+ # Create folder for best combination with parameters in name
3783
+ best_combo_folder = os.path.join(results_folder, f'best_{best_params_str_with_prefix}')
3784
+ os.makedirs(best_combo_folder, exist_ok=True)
3785
+
3786
+ # Run FULL backtest with ALL charts and exports
3787
+ best_analyzer = run_backtest(
3788
+ strategy_function,
3789
+ best_config,
3790
+ print_report=True,
3791
+ create_charts=True,
3792
+ export_results=True,
3793
+ progress_context=None,
3794
+ chart_filename=os.path.join(best_combo_folder, f'BST_{best_params_str_with_prefix}_chart.png'),
3795
+ export_prefix=os.path.join(best_combo_folder, f'BST_{best_params_str_with_prefix}')
3796
+ )
3797
+
3798
+ metrics_data = {
3799
+ 'metric': list(best_analyzer.metrics.keys()),
3800
+ 'value': list(best_analyzer.metrics.values())
3801
+ }
3802
+ metrics_df = pd.DataFrame(metrics_data)
3803
+ metrics_df.to_csv(metrics_path, index=False)
3804
+
3805
+ print(f"\n✓ Detailed metrics saved: {metrics_path}")
3806
+ print(f"✓ Best combination results saved to: {best_combo_folder}/")
3807
+ print(f" Files: BST_{best_params_str_with_prefix}_*.csv, BST_{best_params_str_with_prefix}_chart.png")
3808
+
3809
+ try:
3810
+ chart_file = os.path.join(best_combo_folder, f'BST_{best_params_str_with_prefix}_chart.png')
3811
+ if os.path.exists(chart_file):
3812
+ print(f"\n📈 Best combination charts saved to: {chart_file}")
3813
+ except Exception as e:
3814
+ print(f"\n⚠ Could not display charts (saved to {best_combo_folder}/): {e}")
3679
3815
 
3680
3816
  # ═══════════════════════════════════════════════════════════════════════════
3681
3817
  # CREATE OPTIMIZATION COMPARISON CHARTS (save only, display in notebook manually)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ivolatility_backtesting
3
- Version: 1.14.0
3
+ Version: 1.18.0
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.14.0"
7
+ version = "1.18.0"
8
8
  description = "A universal backtesting framework for financial strategies using the IVolatility API."
9
9
  readme = "README.md"
10
10
  authors = [