use chrono::{Duration, Utc};
use std::env;
use tradestation_api::{Client, Credentials, Token};
async fn live_client() -> Option<Client> {
let client_id = env::var("TSAPI_CLIENT_ID").ok()?;
let client_secret = env::var("TSAPI_CLIENT_SECRET").ok()?;
let creds = Credentials::new(client_id, client_secret);
let mut client = Client::new(creds);
if let Ok(refresh_tok) = env::var("TRADESTATION_REFRESH_TOKEN") {
let http = reqwest::Client::new();
let creds_clone = Credentials::new(
env::var("TSAPI_CLIENT_ID").unwrap(),
env::var("TSAPI_CLIENT_SECRET").unwrap(),
);
match tradestation_api::auth::refresh_token(&http, &creds_clone, &refresh_tok).await {
Ok(fresh_token) => {
eprintln!("✅ Token refreshed successfully");
client = client.with_token(fresh_token);
}
Err(e) => {
eprintln!("⚠️ Token refresh failed: {e}");
if let Ok(access) = env::var("TRADESTATION_ACCESS_TOKEN") {
let tok = Token {
access_token: access,
refresh_token: Some(refresh_tok),
token_type: "Bearer".to_string(),
expires_at: Utc::now() + Duration::minutes(19),
refresh_expires_at: Some(Utc::now() + Duration::days(30)),
};
client = client.with_token(tok);
}
}
}
} else if let Ok(access) = env::var("TRADESTATION_ACCESS_TOKEN") {
let tok = Token {
access_token: access,
refresh_token: None,
token_type: "Bearer".to_string(),
expires_at: Utc::now() + Duration::minutes(19),
refresh_expires_at: None,
};
client = client.with_token(tok);
}
if env::var("TRADESTATION_USE_LIVE").is_err() {
client = client.with_sim();
} else {
eprintln!("⚠️ WARNING: Running against LIVE TradeStation API!");
}
Some(client)
}
#[tokio::test]
#[ignore = "requires live API credentials"]
async fn test_live_get_quotes() {
let mut client = live_client().await.expect("API credentials not set");
let quotes = client.get_quotes(&["AAPL"]).await;
match quotes {
Ok(q) => {
assert!(!q.is_empty(), "Expected at least one quote");
println!("AAPL Last: {}", q[0].last);
println!("AAPL Bid: {}, Ask: {}", q[0].bid, q[0].ask);
assert!(!q[0].last.is_empty(), "Last price should not be empty");
assert!(!q[0].symbol.is_empty(), "Symbol should not be empty");
}
Err(e) => {
println!("Quote fetch failed (expected if no token): {e}");
}
}
}
#[tokio::test]
#[ignore = "requires live API credentials"]
async fn test_live_get_bars() {
let mut client = live_client().await.expect("API credentials not set");
let query = tradestation_api::BarChartQuery::daily_bars("AAPL", 5);
let bars = client.get_bars(&query).await;
match bars {
Ok(b) => {
assert!(!b.is_empty(), "Expected at least one bar");
println!("Got {} bars for AAPL", b.len());
for bar in &b {
let (o, h, l, c, v) = bar.ohlcv().expect("Should parse OHLCV");
println!(
" {} O:{:.2} H:{:.2} L:{:.2} C:{:.2} V:{}",
bar.time_stamp, o, h, l, c, v
);
assert!(h >= l, "High should be >= Low");
assert!(o > 0.0, "Open should be positive");
}
}
Err(e) => println!("Bar fetch failed: {e}"),
}
}
#[tokio::test]
#[ignore = "requires live API credentials"]
async fn test_live_get_symbol_info() {
let mut client = live_client().await.expect("API credentials not set");
let symbols = client.get_symbol_info(&["AAPL", "MSFT"]).await;
match symbols {
Ok(s) => {
assert!(s.len() >= 2, "Expected info for both symbols");
for sym in &s {
println!(
"{}: {} ({})",
sym.symbol,
sym.description.as_deref().unwrap_or("?"),
sym.exchange.as_deref().unwrap_or("?")
);
}
}
Err(e) => println!("Symbol info failed: {e}"),
}
}
#[tokio::test]
#[ignore = "requires live API credentials + ReadAccount scope"]
async fn test_live_get_accounts() {
let mut client = live_client().await.expect("API credentials not set");
let accounts = client.get_accounts().await;
match accounts {
Ok(a) => {
assert!(!a.is_empty(), "Expected at least one account");
for acct in &a {
let marker = if acct.account_id.starts_with("SIM") {
"✅ SIM"
} else {
"⚠️ LIVE"
};
println!(
"Account: {} ({}) {}",
acct.account_id,
acct.account_type.as_deref().unwrap_or("?"),
marker
);
}
}
Err(e) => println!("Accounts failed: {e}"),
}
}
fn find_sim_account(accounts: &[tradestation_api::Account]) -> Option<&str> {
accounts
.iter()
.find(|a| a.account_id.starts_with("SIM"))
.map(|a| a.account_id.as_str())
}
#[tokio::test]
#[ignore = "requires live API credentials + ReadAccount scope"]
async fn test_live_get_balances() {
let mut client = live_client().await.expect("API credentials not set");
let accounts = client.get_accounts().await;
match accounts {
Ok(a) if !a.is_empty() => {
let id = find_sim_account(&a).unwrap_or_else(|| {
println!("⚠️ No SIM account, skipping");
&a[0].account_id
});
println!("Using account: {id}");
let balances = client.get_balances(&[id]).await;
match balances {
Ok(b) => {
assert!(!b.is_empty(), "Expected balance data");
for bal in &b {
println!(
"Cash: {}, Equity: {}, Buying Power: {}",
bal.cash_balance.as_deref().unwrap_or("?"),
bal.equity.as_deref().unwrap_or("?"),
bal.buying_power.as_deref().unwrap_or("?")
);
}
}
Err(e) => println!("Balances failed: {e}"),
}
}
_ => println!("No accounts available for balance test"),
}
}
#[tokio::test]
#[ignore = "requires live API credentials + ReadAccount scope"]
async fn test_live_get_positions() {
let mut client = live_client().await.expect("API credentials not set");
let accounts = client.get_accounts().await;
match accounts {
Ok(a) if !a.is_empty() => {
let id = find_sim_account(&a).unwrap_or_else(|| {
println!("⚠️ No SIM account, skipping");
&a[0].account_id
});
println!("Using account: {id}");
let positions = client.get_positions(&[id]).await;
match positions {
Ok(p) => {
println!("Open positions: {}", p.len());
for pos in &p {
println!(
" {} qty:{} avg:{} pnl:{}",
pos.symbol.as_deref().unwrap_or("?"),
pos.quantity.as_deref().unwrap_or("?"),
pos.average_price.as_deref().unwrap_or("?"),
pos.unrealized_profit_loss.as_deref().unwrap_or("?")
);
}
}
Err(e) => println!("Positions failed: {e}"),
}
}
_ => println!("No accounts available"),
}
}
#[tokio::test]
#[ignore = "requires live API credentials + ReadAccount scope"]
async fn test_live_get_historical_orders() {
let mut client = live_client().await.expect("API credentials not set");
let accounts = client.get_accounts().await;
match accounts {
Ok(a) if !a.is_empty() => {
let id = find_sim_account(&a).unwrap_or_else(|| {
println!("⚠️ No SIM account, skipping");
&a[0].account_id
});
let since = (chrono::Utc::now() - chrono::Duration::days(60))
.format("%Y-%m-%d")
.to_string();
println!("Historical orders for {id} since {since}");
let orders = client.get_historical_orders(&[id], &since).await;
match orders {
Ok(o) => {
println!("Historical orders: {}", o.len());
for ord in o.iter().take(10) {
println!(
" {} {} {} qty:{} @ {} [{}]",
ord.order_id.as_deref().unwrap_or("?"),
ord.trade_action.as_deref().unwrap_or("?"),
ord.symbol.as_deref().unwrap_or("?"),
ord.quantity.as_deref().unwrap_or("?"),
ord.filled_quantity.as_deref().unwrap_or("?"),
ord.status_description.as_deref().unwrap_or("?"),
);
}
}
Err(e) => println!("Historical orders failed: {e}"),
}
}
_ => println!("No accounts available"),
}
}
#[tokio::test]
#[ignore = "requires live API credentials"]
async fn test_live_get_option_expirations() {
let mut client = live_client().await.expect("API credentials not set");
let exps = client.get_option_expirations("AAPL").await;
match exps {
Ok(e) => {
assert!(!e.is_empty(), "Expected option expirations for AAPL");
println!("AAPL has {} expiration dates", e.len());
for exp in e.iter().take(5) {
println!(
" {} ({})",
exp.date,
exp.expiration_type.as_deref().unwrap_or("?")
);
}
}
Err(e) => println!("Option expirations failed: {e}"),
}
}
#[tokio::test]
#[ignore = "requires live API credentials + active market"]
async fn test_live_stream_quotes() {
use futures::StreamExt;
let mut client = live_client().await.expect("API credentials not set");
let stream_result = client.stream_quotes(&["AAPL"]).await;
match stream_result {
Ok(mut stream) => {
println!("Connected to quote stream for AAPL");
let mut count = 0;
while let Some(event) = stream.next().await {
match event {
Ok(quote) => {
if quote.is_status() {
println!(" Status: {:?}", quote.status);
} else {
println!(
" AAPL: Last={} Bid={} Ask={}",
quote.last.as_deref().unwrap_or("?"),
quote.bid.as_deref().unwrap_or("?"),
quote.ask.as_deref().unwrap_or("?")
);
}
count += 1;
if count >= 5 {
break;
} }
Err(e) => {
println!("Stream error: {e}");
break;
}
}
}
assert!(count > 0, "Should have received at least one stream event");
}
Err(e) => println!("Stream connect failed: {e}"),
}
}