use std::env;
use tradingview::{
DataServer, Interval,
historical::{BatchConfig, BatchResult, HistoricalClient},
prelude::OHLCV,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let _ = dotenv::dotenv();
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.init();
let auth_token = env::var("TV_AUTH_TOKEN").unwrap_or_else(|_| {
eprintln!("TV_AUTH_TOKEN not set — using anonymous access (may fail)");
"unauthorized_user_token".to_string()
});
let symbols_raw = env::var("TV_SYMBOLS").unwrap_or_else(|_| "AAPL:NASDAQ;FPT:HOSE".to_string());
let mut symbols: Vec<(String, String)> = Vec::new();
for pair_str in symbols_raw.split(|c| c == ',' || c == ';') {
let parts: Vec<&str> = pair_str.splitn(2, ':').collect();
match parts.as_slice() {
[sym, exch] => symbols.push((sym.to_string(), exch.to_string())),
[sym] => symbols.push((sym.to_string(), "NASDAQ".to_string())),
_ => continue,
}
}
let interval = env::var("TV_INTERVAL")
.map(|s| match s.as_str() {
"1m" => Interval::OneMinute,
"5m" => Interval::FiveMinutes,
"15m" => Interval::FifteenMinutes,
"30m" => Interval::ThirtyMinutes,
"1h" => Interval::OneHour,
"4h" => Interval::FourHours,
"1W" => Interval::OneWeek,
"1M" => Interval::OneMonth,
_ => Interval::OneDay,
})
.unwrap_or(Interval::OneDay);
let num_bars: u64 = env::var("TV_NUM_BARS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(100);
let max_concurrency: usize = env::var("TV_CONCURRENCY")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(4);
let server = env::var("TV_SERVER")
.map(|s| match s.as_str() {
"pro" => DataServer::ProData,
"data" => DataServer::Data,
_ => DataServer::ProData,
})
.unwrap_or(DataServer::ProData);
let config = BatchConfig {
max_concurrency,
per_symbol_timeout: std::time::Duration::from_secs(30),
};
println!("Batch historical fetch");
println!(" Server: {server:?}");
println!(" Interval: {interval:?}");
println!(" Bars/symbol: {num_bars}");
println!(" Concurrency: {max_concurrency}");
println!(" Symbols ({}) :", symbols.len());
for (sym, exch) in &symbols {
println!(" {exch}:{sym}");
}
println!();
let client = HistoricalClient::new(&auth_token, server);
let result = client
.retrieve_batch(&symbols, interval, Some(num_bars), config)
.await;
print_batch_summary(&result);
if !result.successful.is_empty() {
println!("══ Successful symbols ══");
for entry in &result.successful {
if let Ok(hr) = &entry.result {
let bars = hr.data.len();
let first = hr
.first_datetime()
.map(|d| d.format("%Y-%m-%d").to_string());
let last = hr.last_datetime().map(|d| d.format("%Y-%m-%d").to_string());
println!(
" {exch}:{sym} bars={bars} range={first}..{last} elapsed={elapsed:?}",
exch = entry.exchange,
sym = entry.symbol,
first = first.unwrap_or_else(|| "N/A".into()),
last = last.unwrap_or_else(|| "N/A".into()),
elapsed = hr.elapsed,
);
for dp in hr.data.iter().take(3) {
let dt = dp.datetime();
println!(
" {:>12} O={:>10.2} H={:>10.2} L={:>10.2} C={:>10.2} V={:>12.0}",
dt.format("%Y-%m-%d"),
dp.open(),
dp.high(),
dp.low(),
dp.close(),
dp.volume(),
);
}
}
}
}
if !result.failed.is_empty() {
println!();
println!("══ Failed symbols ══");
for entry in &result.failed {
println!(
" {exch}:{sym} error: {err}",
exch = entry.exchange,
sym = entry.symbol,
err = entry.result.as_ref().unwrap_err(),
);
}
}
Ok(())
}
fn print_batch_summary(result: &BatchResult) {
println!();
println!("══ Batch Result ══");
println!(" Total requested: {}", result.total_requested);
println!(" Successful: {}", result.success_count());
println!(" Failed: {}", result.failure_count());
println!(" Elapsed: {:?}", result.elapsed);
let total_bars: usize = result
.successful
.iter()
.filter_map(|r| r.result.as_ref().ok())
.map(|hr| hr.total_bars_received)
.sum();
println!(" Total bars: {total_bars}");
println!();
}