sim-cli 0.3.0

CLI tool for running and comparing Solana simulator backtests
use clap::Parser;
use eyre::Result;
use simulator_client::{BacktestClient, RangeBound, filter_ranges};

use crate::output::format_slot;

#[derive(Parser, Debug, Clone)]
pub struct RangesArgs {
    /// Simulator endpoint hostname (e.g. staging.simulator.termina.technology)
    #[arg(
        long,
        env = "SIMULATOR_URL",
        default_value = "simulator.termina.technology"
    )]
    pub url: String,

    /// Only show ranges that start at or after this point.
    /// Accepts a slot number or a timestamp (RFC 3339 or YYYY-MM-DD).
    #[arg(long)]
    pub after: Option<String>,

    /// Only show ranges that end at or before this point.
    /// Accepts a slot number or a timestamp (RFC 3339 or YYYY-MM-DD).
    #[arg(long)]
    pub before: Option<String>,

    /// Print output as JSON
    #[arg(long)]
    pub json: bool,
}

pub async fn ranges(args: RangesArgs) -> Result<()> {
    let after: Option<RangeBound> = args.after.as_deref().map(str::parse).transpose()?;
    let before: Option<RangeBound> = args.before.as_deref().map(str::parse).transpose()?;

    let client = BacktestClient::builder()
        .url(format!("wss://{}/backtest", args.url))
        .build();
    let all = client.available_ranges().await?;
    let matched = filter_ranges(&all, after.as_ref(), before.as_ref());

    if matched.is_empty() {
        if args.json {
            println!("[]");
        } else {
            println!("No available ranges matching the given filters.");
        }
        return Ok(());
    }

    if args.json {
        println!("{}", serde_json::to_string_pretty(&matched)?);
        return Ok(());
    }

    println!(
        "\n{:<30} {:<30} {:<15} {:<15} Bundle Size",
        "Start Time (UTC)", "End Time (UTC)", "Start Slot", "End Slot"
    );
    println!("{}", "-".repeat(105));
    for range in &matched {
        let start_time = range.bundle_start_slot_utc.as_deref().unwrap_or("-");
        let end_time = range.max_bundle_end_slot_utc.as_deref().unwrap_or("-");
        let start_slot = format_slot(range.bundle_start_slot);
        let end_slot = range
            .max_bundle_end_slot
            .map(format_slot)
            .unwrap_or_else(|| "unbounded".to_string());
        let bundle_size = range.max_bundle_size;
        println!(
            "{start_time:<30} {end_time:<30} {start_slot:<15} {end_slot:<15} {}",
            bundle_size
                .map(format_slot)
                .unwrap_or_else(|| "unbounded".to_string()),
        );
    }

    Ok(())
}