use clap::Parser;
use eyre::Result;
use sim_cli::{ApiKeyArg, engine::format_slot};
use simulator_client::{BacktestClient, RangeBound, filter_ranges};
const SIZE_TOLERANCE: f64 = 0.1;
#[derive(Parser, Debug, Clone)]
pub struct RangesArgs {
#[arg(long)]
pub after: Option<String>,
#[command(flatten)]
pub api_key: Option<ApiKeyArg>,
#[arg(long)]
pub before: Option<String>,
#[arg(long)]
pub size: Option<u64>,
#[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_builder = BacktestClient::builder().url(url);
let client = if let Some(api_key) = args.api_key.as_ref() {
client_builder.api_key(api_key.get()).build()
} else {
client_builder.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(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn api_key_is_optional_and_flows_when_given() {
let args = RangesArgs::try_parse_from(["ranges", "--api-key", "k"]).unwrap();
assert_eq!(
args.api_key.as_ref().map(ApiKeyArg::get),
Some("k".to_string())
);
}
}