Coverage for src/finbot/backtest/comparison.py: 100%

48 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-04 12:05 +0000

1import math 

2from collections.abc import Iterable 

3from dataclasses import dataclass 

4from datetime import datetime 

5from uuid import UUID 

6 

7from finbot.backtest.experiment import ExperimentRun, WalkForwardExperimentRun 

8 

9 

10@dataclass(frozen=True) 

11class ExperimentComparisonRow: 

12 run_id: UUID 

13 strategy: str 

14 symbols: tuple[str, ...] 

15 start: datetime 

16 end: datetime 

17 

18 starting_cash: float 

19 ending_value: float 

20 total_return: float 

21 max_drawdown: float 

22 total_fees: float 

23 total_slippage: float 

24 

25 

26def compare_experiments(runs: Iterable[ExperimentRun]) -> list[ExperimentComparisonRow]: 

27 return [ 

28 ExperimentComparisonRow( 

29 run_id=run.metadata.run_id, 

30 strategy=run.metadata.config.strategy, 

31 symbols=run.metadata.config.symbols, 

32 start=run.metadata.config.start, 

33 end=run.metadata.config.end, 

34 starting_cash=run.result.starting_cash, 

35 ending_value=run.result.ending_value, 

36 total_return=run.result.total_return, 

37 max_drawdown=run.result.max_drawdown, 

38 total_fees=run.result.total_fees, 

39 total_slippage=run.result.total_slippage, 

40 ) 

41 for run in runs 

42 ] 

43 

44 

45@dataclass(frozen=True) 

46class WalkForwardComparisonRow: 

47 run_id: UUID 

48 strategy: str 

49 symbols: tuple[str, ...] 

50 

51 test_start: datetime 

52 test_end: datetime 

53 fold_count: int 

54 

55 compounded_return: float 

56 average_fold_return: float 

57 worst_fold_return: float 

58 worst_fold_drawdown: float 

59 

60 total_fees: float 

61 total_slippage: float 

62 

63 

64def compare_walk_forward_experiments( 

65 runs: Iterable[WalkForwardExperimentRun], 

66) -> list[WalkForwardComparisonRow]: 

67 out: list[WalkForwardComparisonRow] = [] 

68 

69 for run in runs: 

70 folds = run.result.folds 

71 config = run.metadata.config 

72 

73 compounded_return = math.prod(1.0 + fold.test_result.total_return for fold in folds) - 1.0 

74 average_fold_return = sum(fold.test_result.total_return for fold in folds) / len(folds) 

75 worst_fold_result = min(fold.test_result.total_return for fold in folds) 

76 worst_fold_drawdown = max(fold.test_result.max_drawdown for fold in folds) 

77 

78 total_fees = sum(fold.test_result.total_fees for fold in folds) 

79 total_slippage = sum(fold.test_result.total_slippage for fold in folds) 

80 

81 out.append( 

82 WalkForwardComparisonRow( 

83 run_id=run.metadata.run_id, 

84 strategy=config.strategy, 

85 symbols=config.symbols, 

86 

87 test_start=folds[0].split.test.start, 

88 test_end=folds[-1].split.test.end, 

89 fold_count=len(folds), 

90 

91 compounded_return=compounded_return, 

92 average_fold_return=average_fold_return, 

93 worst_fold_return=worst_fold_result, 

94 worst_fold_drawdown=worst_fold_drawdown, 

95 

96 total_fees=total_fees, 

97 total_slippage=total_slippage, 

98 ), 

99 ) 

100 

101 return out