sandbox_quant/backtest_app/
terminal.rs1use 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}