sim-cli 0.7.0

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

const SIZE_TOLERANCE: f64 = 0.1;

#[derive(Parser, Debug, Clone)]
pub struct RangesArgs {
    /// 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>,

    /// Only show ranges whose bundle size is within 10% of this value.
    #[arg(long)]
    pub size: Option<u64>,

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

pub async fn ranges(args: RangesArgs, url: String) -> 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(url).build();
    let all = client.available_ranges().await?;
    let time_bound = filter_ranges(&all, after.as_ref(), before.as_ref());
    let matched: Vec<_> = match args.size {
        None => time_bound,
        Some(target) => time_bound
            .into_iter()
            .filter(|r| {
                r.max_bundle_size.is_some_and(|size| {
                    let delta = (size as f64 - target as f64).abs();
                    delta / target as f64 <= SIZE_TOLERANCE
                })
            })
            .collect(),
    };

    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(())
}