sandbox-quant 1.0.8

Exchange-truth trading core for Binance Spot and Futures
Documentation
use crate::app::bootstrap::BinanceMode;
use crate::backtest_app::runner::{run_backtest_for_path, BacktestConfig};
use crate::backtest_app::snapshot::maybe_prepare_snapshot_from_postgres;
use crate::command::backtest::{
    backtest_help_text, complete_backtest_input, parse_backtest_shell_input, BacktestCommand,
    BacktestShellInput,
};
use crate::dataset::query::{
    load_backtest_report, load_backtest_run_summaries, persist_backtest_report,
};
use crate::dataset::schema::init_schema_for_path;
use crate::record::coordination::RecorderCoordination;
use crate::terminal::app::{TerminalApp, TerminalEvent, TerminalMode};
use crate::terminal::completion::ShellCompletion;
use crate::ui::backtest_output::{render_backtest_run, render_backtest_run_list};

pub struct BacktestTerminal {
    pub mode: BinanceMode,
    pub base_dir: String,
}

impl BacktestTerminal {
    pub fn new(mode: BinanceMode, base_dir: impl Into<String>) -> Self {
        Self {
            mode,
            base_dir: base_dir.into(),
        }
    }
}

impl TerminalApp for BacktestTerminal {
    fn terminal_mode(&self) -> TerminalMode {
        TerminalMode::Line
    }

    fn intro_panel(&self) -> String {
        format!(
            "╭──────────────────────────────────────────────╮\n│ >_ Sandbox Quant Backtest (v{})              │\n│                                              │\n│ mode:      {:<18} /mode to change │\n│ base_dir:  {:<28} │\n╰──────────────────────────────────────────────╯",
            env!("CARGO_PKG_VERSION"),
            self.mode.as_str(),
            self.base_dir
        )
    }

    fn help_text(&self) -> String {
        backtest_help_text().to_string()
    }

    fn prompt(&self) -> String {
        format!("[backtest:{}] › ", self.mode.as_str())
    }

    fn complete(&self, line: &str) -> Vec<ShellCompletion> {
        complete_backtest_input(line)
    }

    fn execute_line(&mut self, line: &str) -> Result<TerminalEvent, String> {
        match parse_backtest_shell_input(line) {
            Ok(BacktestShellInput::Empty) => Ok(TerminalEvent::NoOutput),
            Ok(BacktestShellInput::Help) => Ok(TerminalEvent::Output(self.help_text())),
            Ok(BacktestShellInput::Exit) => Ok(TerminalEvent::Exit),
            Ok(BacktestShellInput::Mode(mode)) => {
                self.mode = mode;
                Ok(TerminalEvent::Output(format!(
                    "mode switched to {}",
                    self.mode.as_str()
                )))
            }
            Ok(BacktestShellInput::Command(command)) => match command {
                BacktestCommand::Run {
                    template,
                    instrument,
                    from,
                    to,
                } => {
                    let db_path =
                        RecorderCoordination::new(self.base_dir.clone()).db_path(self.mode);
                    if let Some(message) = maybe_prepare_snapshot_from_postgres(
                        self.mode,
                        &self.base_dir,
                        &instrument,
                        from,
                        to,
                    )
                    .map_err(|error| error.to_string())?
                    {
                        eprintln!("{message}");
                    }
                    init_schema_for_path(&db_path).map_err(|error| error.to_string())?;
                    let report = run_backtest_for_path(
                        &db_path,
                        self.mode,
                        template,
                        &instrument,
                        from,
                        to,
                        BacktestConfig::default(),
                    )
                    .map_err(|error| error.to_string())?;
                    let run_id = persist_backtest_report(&db_path, &report)
                        .map_err(|error| error.to_string())?;
                    let mut report = report;
                    report.run_id = Some(run_id);
                    Ok(TerminalEvent::Output(render_backtest_run(&report)))
                }
                BacktestCommand::List => {
                    let db_path =
                        RecorderCoordination::new(self.base_dir.clone()).db_path(self.mode);
                    let runs = load_backtest_run_summaries(&db_path, 20)
                        .map_err(|error| error.to_string())?;
                    Ok(TerminalEvent::Output(render_backtest_run_list(&runs)))
                }
                BacktestCommand::ReportLatest => {
                    let db_path =
                        RecorderCoordination::new(self.base_dir.clone()).db_path(self.mode);
                    let report =
                        load_backtest_report(&db_path, None).map_err(|error| error.to_string())?;
                    if let Some(report) = report {
                        Ok(TerminalEvent::Output(render_backtest_run(&report)))
                    } else {
                        Ok(TerminalEvent::Output(
                            "backtest report\nstate=missing".to_string(),
                        ))
                    }
                }
                BacktestCommand::ReportShow { run_id } => {
                    let db_path =
                        RecorderCoordination::new(self.base_dir.clone()).db_path(self.mode);
                    let report = load_backtest_report(&db_path, Some(run_id))
                        .map_err(|error| error.to_string())?;
                    if let Some(report) = report {
                        Ok(TerminalEvent::Output(render_backtest_run(&report)))
                    } else {
                        Ok(TerminalEvent::Output(format!(
                            "backtest report\nrun_id={run_id}\nstate=missing"
                        )))
                    }
                }
            },
            Err(error) => Err(error),
        }
    }
}