22import math
33from typing import List
44
5+ from .consistency import (
6+ get_cv_consistency , get_normalized_stability ,
7+ get_consistency_score , get_stability_score ,
8+ )
59from .backtest_metrics import BacktestMetrics
610from .backtest_summary_metrics import BacktestSummaryMetrics
711
@@ -441,55 +445,25 @@ def generate_backtest_summary_metrics(
441445 ) if consecutive_losses else None
442446
443447 # === CONSISTENCY METRICS ===
444- # Two complementary approaches to measure cross-window stability.
445- #
446- # 1) CV-based consistency: 1 - CV (CV = std / |mean|), capped [0, 1].
447- # Standard statistical measure; scale-invariant.
448- # Drawback: undefined when mean ≈ 0.
449- #
450- # 2) Normalized-std stability: 1 - std/max_std, capped [0, 1].
451- # Uses a domain-specific max_std for normalization.
452- # More intuitive for bounded metrics (win rate 0-100,
453- # Sharpe typically -2 to +4).
454-
455448 return_consistency = None
456449 win_rate_consistency = None
457450 sharpe_consistency = None
458- consistency_score = None
451+ consistency_score_val = None
459452 return_stability = None
460453 win_rate_stability = None
461454 sharpe_stability = None
462- stability_score = None
463-
464- def _cv_consistency (values ):
465- """1 - CV capped to [0, 1], or None if insufficient data."""
466- if len (values ) < 2 :
467- return None
468- mean = sum (values ) / len (values )
469- if abs (mean ) < 1e-9 :
470- return 0.0 # mean ≈ 0 → unstable
471- var = sum ((x - mean ) ** 2 for x in values ) / (len (values ) - 1 )
472- cv = math .sqrt (var ) / abs (mean )
473- return max (0.0 , min (1.0 , 1.0 - cv ))
474-
475- def _norm_stability (values , max_std ):
476- """1 - std/max_std capped to [0, 1], or None if insufficient."""
477- if len (values ) < 2 :
478- return None
479- mean = sum (values ) / len (values )
480- var = sum ((x - mean ) ** 2 for x in values ) / (len (values ) - 1 )
481- std = math .sqrt (var )
482- return max (0.0 , min (1.0 , 1.0 - std / max_std ))
455+ stability_score_val = None
483456
484457 if len (valid_metrics ) >= 2 :
485458 # --- Per-window returns ---
486459 per_window_returns = [
487460 b .total_net_gain_percentage for b in valid_metrics
488461 if b .total_net_gain_percentage is not None
489462 ]
490- return_consistency = _cv_consistency (per_window_returns )
491- # max_std = 100: a std of 100% of initial capital → score 0
492- return_stability = _norm_stability (per_window_returns , 100.0 )
463+ return_consistency = get_cv_consistency (per_window_returns )
464+ return_stability = get_normalized_stability (
465+ per_window_returns , 100.0
466+ )
493467
494468 # --- Per-window win rates ---
495469 per_window_win_rates = [
@@ -498,10 +472,10 @@ def _norm_stability(values, max_std):
498472 and b .number_of_trades_closed is not None
499473 and b .number_of_trades_closed > 0
500474 ]
501- return_consistency = _cv_consistency ( per_window_returns )
502- win_rate_consistency = _cv_consistency ( per_window_win_rates )
503- # max_std = 50: theoretical max std for a [0, 100] range
504- win_rate_stability = _norm_stability ( per_window_win_rates , 50.0 )
475+ win_rate_consistency = get_cv_consistency ( per_window_win_rates )
476+ win_rate_stability = get_normalized_stability (
477+ per_window_win_rates , 50.0
478+ )
505479
506480 # --- Per-window Sharpe ratios ---
507481 per_window_sharpe = [
@@ -510,45 +484,19 @@ def _norm_stability(values, max_std):
510484 and not math .isnan (b .sharpe_ratio )
511485 and not math .isinf (b .sharpe_ratio )
512486 ]
513- sharpe_consistency = _cv_consistency (per_window_sharpe )
514- # max_std = 2: Sharpe ratios typically range -2 to +4;
515- # a std of 2 means wildly inconsistent
516- sharpe_stability = _norm_stability ( per_window_sharpe , 2.0 )
487+ sharpe_consistency = get_cv_consistency (per_window_sharpe )
488+ sharpe_stability = get_normalized_stability (
489+ per_window_sharpe , 2.0
490+ )
517491
518492 # --- Composite scores ---
519- # Both use the same weighting scheme:
520- # 35% returns, 25% win rate, 20% Sharpe, 20% profitable
521- # window ratio.
522- def _composite (ret_c , wr_c , sh_c ):
523- components = []
524- weights_c = []
525- if ret_c is not None :
526- components .append (ret_c )
527- weights_c .append (0.35 )
528- if wr_c is not None :
529- components .append (wr_c )
530- weights_c .append (0.25 )
531- if sh_c is not None :
532- components .append (sh_c )
533- weights_c .append (0.20 )
534- if number_of_windows and number_of_windows > 0 :
535- pw_ratio = (
536- number_of_profitable_windows / number_of_windows
537- )
538- components .append (pw_ratio )
539- weights_c .append (0.20 )
540- if not components :
541- return None
542- total_w = sum (weights_c )
543- return sum (
544- c * w for c , w in zip (components , weights_c )
545- ) / total_w
546-
547- consistency_score = _composite (
548- return_consistency , win_rate_consistency , sharpe_consistency
493+ consistency_score_val = get_consistency_score (
494+ return_consistency , win_rate_consistency , sharpe_consistency ,
495+ number_of_profitable_windows , number_of_windows ,
549496 )
550- stability_score = _composite (
551- return_stability , win_rate_stability , sharpe_stability
497+ stability_score_val = get_stability_score (
498+ return_stability , win_rate_stability , sharpe_stability ,
499+ number_of_profitable_windows , number_of_windows ,
552500 )
553501
554502 return BacktestSummaryMetrics (
@@ -602,9 +550,9 @@ def _composite(ret_c, wr_c, sh_c):
602550 return_consistency = return_consistency ,
603551 win_rate_consistency = win_rate_consistency ,
604552 sharpe_consistency = sharpe_consistency ,
605- consistency_score = consistency_score ,
553+ consistency_score = consistency_score_val ,
606554 return_stability = return_stability ,
607555 win_rate_stability = win_rate_stability ,
608556 sharpe_stability = sharpe_stability ,
609- stability_score = stability_score ,
557+ stability_score = stability_score_val ,
610558 )
0 commit comments