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