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
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 17:12 +0000
1import argparse
2from pathlib import Path
4from finbot.logger import get_logger
6DEFAULT_OUTPUT_DIR = Path(".finbot/experiments")
8logger = get_logger(__name__)
11def _format_comparison(rows) -> list[str]:
12 headers = ("RUN ID", "STRATEGY", "SYMBOLS", "STARTING CASH", "ENDING VALUE", "RETURN", "MAX DD")
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 ]
27 widths = [
28 max(len(headers[index]), *(len(row[index]) for row in values))
29 for index in range(len(headers))
30 ]
32 def format_row(row: tuple[str, ...]) -> str:
33 return " ".join(value.ljust(widths[index]) for index, value in enumerate(row))
35 separator = " ".join("-" * width for width in widths)
36 lines = [format_row(headers), separator, *(format_row(row) for row in values)]
38 return lines
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)
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 )
74def run(args: argparse.Namespace) -> int:
75 from finbot.run_backtest import run as run_backtest
77 return run_backtest(args)
80def run_compare(args: argparse.Namespace) -> int:
81 from finbot.backtest.artifacts import ExperimentStore
82 from finbot.backtest.comparison import compare_experiments
84 logger.debug("Loading %d experiment runs from %s", len(args.run_ids), args.output_dir)
86 store = ExperimentStore(args.output_dir)
87 runs = store.load_many(args.run_ids)
89 logger.debug("Comparing %d experiment runs", len(runs))
91 rows = compare_experiments(runs)
93 logger.info("Backtest comparison")
95 for row in _format_comparison(rows):
96 logger.info("%s", row)
98 return 0