use crate::display::{Format, aggregate_ticks, parse_category, write_ohlcv, write_ticks};
use anyhow::{Context, Result};
use chrono::NaiveDate;
use futures::stream::{self, StreamExt};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use paracas_daemon::{DaemonSpawner, DownloadJob, InstrumentTask, StateManager};
use paracas_estimate::Estimator;
use paracas_lib::prelude::*;
use std::io::Write as _;
use std::path::PathBuf;
#[allow(clippy::too_many_arguments)]
pub(crate) async fn download_all(
category: Option<&str>,
start_str: Option<&str>,
end_str: Option<&str>,
output_dir: PathBuf,
format: Format,
timeframe_str: Option<&str>,
parallel_instruments: usize,
concurrency: usize,
background: bool,
yes: bool,
quiet: bool,
) -> Result<()> {
let registry = InstrumentRegistry::global();
let instruments: Vec<_> = match category {
Some(cat) => {
let category = parse_category(cat)?;
registry.by_category(category).collect()
}
None => registry.all().collect(),
};
if instruments.is_empty() {
anyhow::bail!("No instruments found matching criteria");
}
let today = chrono::Utc::now().date_naive();
let end = match end_str {
Some(s) => NaiveDate::parse_from_str(s, "%Y-%m-%d")
.with_context(|| format!("Invalid end date: {s}"))?,
None => today,
};
let start = match start_str {
Some(s) => NaiveDate::parse_from_str(s, "%Y-%m-%d")
.with_context(|| format!("Invalid start date: {s}"))?,
None => {
instruments
.iter()
.filter_map(|i| i.start_tick_date())
.map(|dt| dt.date_naive())
.min()
.unwrap_or_else(|| NaiveDate::from_ymd_opt(2003, 5, 5).expect("valid date"))
}
};
let range = DateRange::new(start, end)?;
let estimator = Estimator::global();
let estimate = estimator.estimate_batch(&instruments, &range);
if !yes && !quiet {
println!("Download plan:");
println!(" Instruments: {}", instruments.len());
println!(" Date range: {} to {}", start, end);
println!(
" Estimated download size: {}",
Estimator::format_bytes(estimate.estimated_compressed_bytes)
);
println!(
" Estimated output size: {}",
Estimator::format_bytes(estimate.estimated_output_bytes)
);
println!(
" Estimated time: {}",
Estimator::format_duration(estimate.estimated_duration)
);
println!();
print!("Proceed with download? [y/N] ");
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if !input.trim().eq_ignore_ascii_case("y") {
println!("Cancelled.");
return Ok(());
}
}
if background {
return spawn_background_download_all(
&instruments,
start,
end,
&output_dir,
format,
timeframe_str,
concurrency,
);
}
std::fs::create_dir_all(&output_dir)?;
let timeframe = match timeframe_str {
Some(tf) => tf
.parse::<Timeframe>()
.map_err(|e| anyhow::anyhow!("{e}"))?,
None => Timeframe::Tick,
};
let multi_progress = MultiProgress::new();
let results: Vec<_> = stream::iter(instruments.into_iter())
.map(|instrument| {
let pb = multi_progress.add(ProgressBar::new(100));
pb.set_style(
ProgressStyle::default_bar()
.template("{prefix:.bold} [{bar:30.cyan/blue}] {percent}% {msg}")
.unwrap()
.progress_chars("=>-"),
);
pb.set_prefix(format!("{:>12}", instrument.id()));
download_single_instrument(
instrument,
start,
end,
output_dir.clone(),
format,
timeframe,
concurrency,
pb,
quiet,
)
})
.buffer_unordered(parallel_instruments)
.collect()
.await;
let (successes, failures): (Vec<_>, Vec<_>) = results.iter().partition(|r| r.is_ok());
if !quiet {
println!("\nDownload complete:");
println!(" Successful: {}", successes.len());
if !failures.is_empty() {
println!(" Failed: {}", failures.len());
for (i, err) in failures.iter().enumerate() {
if let Err(e) = err {
println!(" {}: {}", i + 1, e);
}
}
}
}
if !failures.is_empty() {
anyhow::bail!(
"{} out of {} downloads failed",
failures.len(),
successes.len() + failures.len()
);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn download_single_instrument(
instrument: &Instrument,
start: NaiveDate,
end: NaiveDate,
output_dir: PathBuf,
format: Format,
timeframe: Timeframe,
concurrency: usize,
progress: ProgressBar,
quiet: bool,
) -> Result<()> {
let effective_start = instrument
.start_tick_date()
.map_or(start, |instrument_start| {
let instrument_start_date = instrument_start.date_naive();
if start < instrument_start_date {
instrument_start_date
} else {
start
}
});
if effective_start > end {
progress.finish_with_message("skipped (no data)");
return Ok(());
}
let range = DateRange::new(effective_start, end)?;
let total_hours = range.total_hours() as u64;
progress.set_length(total_hours);
let config = ClientConfig {
concurrency,
..Default::default()
};
let client = DownloadClient::new(config)?;
let mut all_ticks: Vec<Tick> = Vec::new();
let mut skipped_hours = 0u64;
let mut stream = paracas_lib::tick_stream_resilient(&client, instrument, range);
while let Some(batch) = stream.next().await {
if batch.had_error() {
skipped_hours += 1;
}
all_ticks.extend(batch.ticks);
progress.inc(1);
}
let tick_count = all_ticks.len();
let finish_msg = if skipped_hours > 0 {
format!("{} ticks ({} hrs skipped)", tick_count, skipped_hours)
} else {
format!("{} ticks", tick_count)
};
progress.finish_with_message(finish_msg);
let output_path = output_dir.join(format!("{}.{}", instrument.id(), format.extension()));
if timeframe.is_tick() {
write_ticks(&all_ticks, &output_path, format)?;
} else {
let bars = aggregate_ticks(&all_ticks, timeframe);
write_ohlcv(&bars, &output_path, format)?;
}
if !quiet {
progress.println(format!(" Written: {}", output_path.display()));
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn spawn_background_download_all(
instruments: &[&Instrument],
start: NaiveDate,
end: NaiveDate,
output_dir: &PathBuf,
format: Format,
timeframe_str: Option<&str>,
concurrency: usize,
) -> Result<()> {
let output_dir = if output_dir.is_absolute() {
output_dir.clone()
} else {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(output_dir)
};
std::fs::create_dir_all(&output_dir)?;
let timeframe = timeframe_str
.map(|s| s.to_string())
.unwrap_or_else(|| "tick".to_string());
let mut tasks = Vec::with_capacity(instruments.len());
for instrument in instruments {
let effective_start = instrument
.start_tick_date()
.map_or(start, |instrument_start| {
let instrument_start_date = instrument_start.date_naive();
if start < instrument_start_date {
instrument_start_date
} else {
start
}
});
if effective_start > end {
continue;
}
let range = DateRange::new(effective_start, end)?;
let output_path = output_dir.join(format!("{}.{}", instrument.id(), format.extension()));
let task = InstrumentTask::new(
instrument.id().to_string(),
effective_start.format("%Y-%m-%d").to_string(),
end.format("%Y-%m-%d").to_string(),
output_path,
format.to_string(),
timeframe.clone(),
range.total_hours() as u32,
);
tasks.push(task);
}
if tasks.is_empty() {
anyhow::bail!("No instruments with data in the specified date range");
}
let mut job = DownloadJob::new(tasks, concurrency);
let state_manager =
StateManager::with_default_path().context("Failed to initialize state manager")?;
let spawner = DaemonSpawner::new(state_manager).context("Failed to create daemon spawner")?;
let job_id = spawner
.spawn(&mut job)
.context("Failed to spawn background job")?;
println!("Background download started.");
println!("Job ID: {}", job_id);
println!("Instruments: {}", job.tasks.len());
println!("Check status with: paracas status {}", job_id);
Ok(())
}