sandbox-quant 1.0.7

Exchange-truth trading core for Binance Spot and Futures
Documentation
use crate::app::bootstrap::BinanceMode;
use crate::app::cli::normalize_instrument_symbol;
use crate::terminal::completion::ShellCompletion;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RecorderCommand {
    Start { symbols: Vec<String> },
    Status,
    Stop,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RecorderShellInput {
    Empty,
    Help,
    Exit,
    Mode(BinanceMode),
    Command(RecorderCommand),
}

pub fn recorder_help_text() -> &'static str {
    "/start [symbols...]\n/status\n/stop\n/mode <real|demo>\n/help\n/exit"
}

pub fn parse_recorder_shell_input(line: &str) -> Result<RecorderShellInput, String> {
    let trimmed = line.trim();
    if trimmed.is_empty() {
        return Ok(RecorderShellInput::Empty);
    }

    let without_prefix = trimmed.strip_prefix('/').unwrap_or(trimmed);
    match without_prefix {
        "help" => return Ok(RecorderShellInput::Help),
        "exit" | "quit" => return Ok(RecorderShellInput::Exit),
        _ => {}
    }

    let args: Vec<String> = without_prefix
        .split_whitespace()
        .map(str::to_string)
        .collect();
    if args.first().map(String::as_str) == Some("mode") {
        let raw_mode = args.get(1).ok_or("usage: /mode <real|demo>")?;
        let mode = match raw_mode.as_str() {
            "real" => BinanceMode::Real,
            "demo" => BinanceMode::Demo,
            _ => return Err(format!("unsupported mode: {raw_mode}")),
        };
        return Ok(RecorderShellInput::Mode(mode));
    }

    parse_recorder_command(&args).map(RecorderShellInput::Command)
}

pub fn parse_recorder_command(args: &[String]) -> Result<RecorderCommand, String> {
    match args.first().map(String::as_str) {
        Some("start") => Ok(RecorderCommand::Start {
            symbols: args[1..]
                .iter()
                .map(|raw| normalize_instrument_symbol(raw))
                .collect(),
        }),
        Some("status") => {
            if args.len() > 1 {
                Err("usage: /status".to_string())
            } else {
                Ok(RecorderCommand::Status)
            }
        }
        Some("stop") => {
            if args.len() > 1 {
                Err("usage: /stop".to_string())
            } else {
                Ok(RecorderCommand::Stop)
            }
        }
        Some(other) => Err(format!("unsupported command: {other}")),
        None => Err("missing recorder command".to_string()),
    }
}

pub fn complete_recorder_input(line: &str) -> Vec<ShellCompletion> {
    let trimmed = line.trim_start();
    let without_prefix = trimmed.strip_prefix('/').unwrap_or(trimmed);
    let trailing_space = without_prefix.ends_with(' ');
    let parts: Vec<&str> = without_prefix.split_whitespace().collect();

    if parts.is_empty() {
        return vec![
            completion("/start", "start recorder with optional symbols"),
            completion("/status", "show recorder status"),
            completion("/stop", "stop recorder"),
            completion("/mode", "switch mode"),
            completion("/help", "show help"),
            completion("/exit", "exit"),
        ];
    }

    if parts.len() == 1 && !trailing_space {
        return ["/start", "/status", "/stop", "/mode", "/help", "/exit"]
            .into_iter()
            .filter(|item| item.trim_start_matches('/').starts_with(parts[0]))
            .map(|item| completion(item, ""))
            .collect();
    }

    match parts.first().copied() {
        Some("mode") => ["real", "demo"]
            .into_iter()
            .filter(|item| item.starts_with(parts.last().copied().unwrap_or_default()))
            .map(|item| completion(&format!("/mode {item}"), "switch recorder mode"))
            .collect(),
        _ => Vec::new(),
    }
}

fn completion(value: &str, description: &str) -> ShellCompletion {
    ShellCompletion {
        value: value.to_string(),
        description: description.to_string(),
    }
}