Skip to main content

sandbox_quant/backtest_app/
terminal.rs

1use crate::app::bootstrap::BinanceMode;
2use crate::backtest_app::runner::{run_backtest_for_path, BacktestConfig};
3use crate::command::backtest::{
4    backtest_help_text, complete_backtest_input, parse_backtest_shell_input, BacktestCommand,
5    BacktestShellInput,
6};
7use crate::dataset::query::{
8    load_backtest_report, load_backtest_run_summaries, persist_backtest_report,
9};
10use crate::dataset::schema::init_schema_for_path;
11use crate::record::coordination::RecorderCoordination;
12use crate::terminal::app::{TerminalApp, TerminalEvent, TerminalMode};
13use crate::terminal::completion::ShellCompletion;
14use crate::ui::backtest_output::{render_backtest_run, render_backtest_run_list};
15
16pub struct BacktestTerminal {
17    pub mode: BinanceMode,
18    pub base_dir: String,
19}
20
21impl BacktestTerminal {
22    pub fn new(mode: BinanceMode, base_dir: impl Into<String>) -> Self {
23        Self {
24            mode,
25            base_dir: base_dir.into(),
26        }
27    }
28}
29
30impl TerminalApp for BacktestTerminal {
31    fn terminal_mode(&self) -> TerminalMode {
32        TerminalMode::Line
33    }
34
35    fn intro_panel(&self) -> String {
36        format!(
37            "╭──────────────────────────────────────────────╮\n│ >_ Sandbox Quant Backtest (v{})              │\n│                                              │\n│ mode:      {:<18} /mode to change │\n│ base_dir:  {:<28} │\n╰──────────────────────────────────────────────╯",
38            env!("CARGO_PKG_VERSION"),
39            self.mode.as_str(),
40            self.base_dir
41        )
42    }
43
44    fn help_text(&self) -> String {
45        backtest_help_text().to_string()
46    }
47
48    fn prompt(&self) -> String {
49        format!("[backtest:{}] › ", self.mode.as_str())
50    }
51
52    fn complete(&self, line: &str) -> Vec<ShellCompletion> {
53        complete_backtest_input(line)
54    }
55
56    fn execute_line(&mut self, line: &str) -> Result<TerminalEvent, String> {
57        match parse_backtest_shell_input(line) {
58            Ok(BacktestShellInput::Empty) => Ok(TerminalEvent::NoOutput),
59            Ok(BacktestShellInput::Help) => Ok(TerminalEvent::Output(self.help_text())),
60            Ok(BacktestShellInput::Exit) => Ok(TerminalEvent::Exit),
61            Ok(BacktestShellInput::Mode(mode)) => {
62                self.mode = mode;
63                Ok(TerminalEvent::Output(format!(
64                    "mode switched to {}",
65                    self.mode.as_str()
66                )))
67            }
68            Ok(BacktestShellInput::Command(command)) => match command {
69                BacktestCommand::Run {
70                    template,
71                    instrument,
72                    from,
73                    to,
74                } => {
75                    let db_path =
76                        RecorderCoordination::new(self.base_dir.clone()).db_path(self.mode);
77                    init_schema_for_path(&db_path).map_err(|error| error.to_string())?;
78                    let report = run_backtest_for_path(
79                        &db_path,
80                        self.mode,
81                        template,
82                        &instrument,
83                        from,
84                        to,
85                        BacktestConfig::default(),
86                    )
87                    .map_err(|error| error.to_string())?;
88                    let run_id = persist_backtest_report(&db_path, &report)
89                        .map_err(|error| error.to_string())?;
90                    let mut report = report;
91                    report.run_id = Some(run_id);
92                    Ok(TerminalEvent::Output(render_backtest_run(&report)))
93                }
94                BacktestCommand::List => {
95                    let db_path =
96                        RecorderCoordination::new(self.base_dir.clone()).db_path(self.mode);
97                    let runs = load_backtest_run_summaries(&db_path, 20)
98                        .map_err(|error| error.to_string())?;
99                    Ok(TerminalEvent::Output(render_backtest_run_list(&runs)))
100                }
101                BacktestCommand::ReportLatest => {
102                    let db_path =
103                        RecorderCoordination::new(self.base_dir.clone()).db_path(self.mode);
104                    let report =
105                        load_backtest_report(&db_path, None).map_err(|error| error.to_string())?;
106                    if let Some(report) = report {
107                        Ok(TerminalEvent::Output(render_backtest_run(&report)))
108                    } else {
109                        Ok(TerminalEvent::Output(
110                            "backtest report\nstate=missing".to_string(),
111                        ))
112                    }
113                }
114                BacktestCommand::ReportShow { run_id } => {
115                    let db_path =
116                        RecorderCoordination::new(self.base_dir.clone()).db_path(self.mode);
117                    let report = load_backtest_report(&db_path, Some(run_id))
118                        .map_err(|error| error.to_string())?;
119                    if let Some(report) = report {
120                        Ok(TerminalEvent::Output(render_backtest_run(&report)))
121                    } else {
122                        Ok(TerminalEvent::Output(format!(
123                            "backtest report\nrun_id={run_id}\nstate=missing"
124                        )))
125                    }
126                }
127            },
128            Err(error) => Err(error),
129        }
130    }
131}