Coverage for src/finbot/cli/backtest.py: 96%

45 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-21 17:12 +0000

1import argparse 

2from pathlib import Path 

3 

4from finbot.logger import get_logger 

5 

6DEFAULT_OUTPUT_DIR = Path(".finbot/experiments") 

7 

8logger = get_logger(__name__) 

9 

10 

11def _format_comparison(rows) -> list[str]: 

12 headers = ("RUN ID", "STRATEGY", "SYMBOLS", "STARTING CASH", "ENDING VALUE", "RETURN", "MAX DD") 

13 

14 values = [ 

15 ( 

16 str(row.run_id), 

17 row.strategy, 

18 ",".join(row.symbols), 

19 f"{row.starting_cash:,.2f}", 

20 f"{row.ending_value:,.2f}", 

21 f"{row.total_return:.2%}", 

22 f"{row.max_drawdown:.2%}", 

23 ) 

24 for row in rows 

25 ] 

26 

27 widths = [ 

28 max(len(headers[index]), *(len(row[index]) for row in values)) 

29 for index in range(len(headers)) 

30 ] 

31 

32 def format_row(row: tuple[str, ...]) -> str: 

33 return " ".join(value.ljust(widths[index]) for index, value in enumerate(row)) 

34 

35 separator = " ".join("-" * width for width in widths) 

36 lines = [format_row(headers), separator, *(format_row(row) for row in values)] 

37 

38 return lines 

39 

40 

41def add_arguments(parser: argparse.ArgumentParser) -> None: 

42 parser.add_argument("--config", type=Path, help="Path to a JSON experiment configuration.") 

43 parser.add_argument("--strategy") 

44 parser.add_argument( 

45 "--strategy-params", 

46 help='Strategy parameters as JSON, e.g. \'{"short_window":10,"long_window":30}\'', 

47 ) 

48 parser.add_argument( 

49 "--symbol", 

50 dest="symbols", 

51 action="append", 

52 help="Symbol to backtest. May be specified multiple times.", 

53 ) 

54 parser.add_argument("--start") 

55 parser.add_argument("--end") 

56 parser.add_argument("--dataset") 

57 parser.add_argument("--initial-cash", type=float) 

58 parser.add_argument("--allocation", type=float) 

59 parser.add_argument("--fee-rate", type=float) 

60 parser.add_argument("--slippage-rate", type=float) 

61 parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR) 

62 

63 

64def add_compare_arguments(parser: argparse.ArgumentParser) -> None: 

65 parser.add_argument("run_ids", nargs="+", help="Experiment run IDs to compare.") 

66 parser.add_argument( 

67 "--output-dir", 

68 type=Path, 

69 default=DEFAULT_OUTPUT_DIR, 

70 help="Directory containing stored experiment runs.", 

71 ) 

72 

73 

74def run(args: argparse.Namespace) -> int: 

75 from finbot.run_backtest import run as run_backtest 

76 

77 return run_backtest(args) 

78 

79 

80def run_compare(args: argparse.Namespace) -> int: 

81 from finbot.backtest.artifacts import ExperimentStore 

82 from finbot.backtest.comparison import compare_experiments 

83 

84 logger.debug("Loading %d experiment runs from %s", len(args.run_ids), args.output_dir) 

85 

86 store = ExperimentStore(args.output_dir) 

87 runs = store.load_many(args.run_ids) 

88 

89 logger.debug("Comparing %d experiment runs", len(runs)) 

90 

91 rows = compare_experiments(runs) 

92 

93 logger.info("Backtest comparison") 

94 

95 for row in _format_comparison(rows): 

96 logger.info("%s", row) 

97 

98 return 0