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