mod helpers;
use futures::StreamExt;
use wiremock::matchers::{method, path};
use wiremock::{Mock, ResponseTemplate};
#[tokio::test]
async fn test_stream_quotes() {
let server = helpers::setup_mock_server().await;
let mut client = helpers::mock_client(&server);
let streaming_body = concat!(
r#"{"Symbol":"AAPL","Last":"185.50","Ask":"185.55","Bid":"185.45","Volume":"45000000"}"#,
"\n",
r#"{"Symbol":"AAPL","Last":"185.60","Ask":"185.65","Bid":"185.55","Volume":"45010000"}"#,
"\n",
r#"{"Status":"EndSnapshot"}"#,
"\n",
);
Mock::given(method("GET"))
.and(path("/v3/marketdata/stream/quotes/AAPL"))
.respond_with(ResponseTemplate::new(200).set_body_raw(
streaming_body,
"application/vnd.tradestation.streams.v3+json",
))
.mount(&server)
.await;
let stream = client.stream_quotes(&["AAPL"]).await.unwrap();
let quotes: Vec<_> = stream
.take(3)
.collect::<Vec<_>>()
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(quotes.len(), 3);
assert_eq!(quotes[0].symbol.as_deref(), Some("AAPL"));
assert_eq!(quotes[0].last.as_deref(), Some("185.50"));
assert!(!quotes[0].is_status());
assert_eq!(quotes[1].last.as_deref(), Some("185.60"));
assert!(quotes[2].is_status());
assert_eq!(quotes[2].status.as_deref(), Some("EndSnapshot"));
}
#[tokio::test]
async fn test_stream_bars() {
let server = helpers::setup_mock_server().await;
let mut client = helpers::mock_client(&server);
let streaming_body = concat!(
r#"{"High":"185.50","Low":"184.20","Open":"184.50","Close":"185.30","TimeStamp":"2026-03-25T14:30:00Z","TotalVolume":"1250000"}"#,
"\n",
r#"{"High":"185.80","Low":"185.10","Open":"185.30","Close":"185.60","TimeStamp":"2026-03-25T14:31:00Z","TotalVolume":"980000"}"#,
"\n",
r#"{"Status":"EndSnapshot"}"#,
"\n",
);
Mock::given(method("GET"))
.and(path("/v3/marketdata/stream/barcharts/AAPL"))
.respond_with(ResponseTemplate::new(200).set_body_raw(
streaming_body,
"application/vnd.tradestation.streams.v3+json",
))
.mount(&server)
.await;
let stream = client.stream_bars("AAPL", "1", "Minute").await.unwrap();
let bars: Vec<_> = stream
.take(3)
.collect::<Vec<_>>()
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(bars.len(), 3);
assert_eq!(bars[0].close.as_deref(), Some("185.30"));
assert_eq!(bars[0].total_volume.as_deref(), Some("1250000"));
assert!(!bars[0].is_status());
assert_eq!(bars[1].open.as_deref(), Some("185.30"));
assert_eq!(bars[1].close.as_deref(), Some("185.60"));
assert!(bars[2].is_status());
}
#[tokio::test]
async fn test_stream_market_depth_quotes() {
let server = helpers::setup_mock_server().await;
let mut client = helpers::mock_client(&server);
let streaming_body = concat!(
r#"{"Symbol":"AAPL","Ask":"185.55","AskSize":"200","Bid":"185.45","BidSize":"300","Side":"Buy"}"#,
"\n",
r#"{"Symbol":"AAPL","Ask":"185.60","AskSize":"150","Bid":"185.50","BidSize":"250","Side":"Sell"}"#,
"\n",
r#"{"Status":"EndSnapshot"}"#,
"\n",
);
Mock::given(method("GET"))
.and(path("/v3/marketdata/stream/marketdepth/quotes/AAPL"))
.respond_with(ResponseTemplate::new(200).set_body_raw(
streaming_body,
"application/vnd.tradestation.streams.v3+json",
))
.mount(&server)
.await;
let stream = client.stream_market_depth_quotes("AAPL").await.unwrap();
let items: Vec<_> = stream
.take(3)
.collect::<Vec<_>>()
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(items.len(), 3);
assert_eq!(items[0].symbol.as_deref(), Some("AAPL"));
assert_eq!(items[0].ask.as_deref(), Some("185.55"));
assert_eq!(items[0].bid.as_deref(), Some("185.45"));
assert!(!items[0].is_status());
assert!(items[2].is_status());
}
#[tokio::test]
async fn test_stream_market_depth_aggregates() {
let server = helpers::setup_mock_server().await;
let mut client = helpers::mock_client(&server);
let streaming_body = concat!(
r#"{"Symbol":"AAPL","TotalAskSize":"5000","TotalBidSize":"6000","Levels":10}"#,
"\n",
r#"{"Status":"EndSnapshot"}"#,
"\n",
);
Mock::given(method("GET"))
.and(path("/v3/marketdata/stream/marketdepth/aggregates/AAPL"))
.respond_with(ResponseTemplate::new(200).set_body_raw(
streaming_body,
"application/vnd.tradestation.streams.v3+json",
))
.mount(&server)
.await;
let stream = client.stream_market_depth_aggregates("AAPL").await.unwrap();
let items: Vec<_> = stream
.take(2)
.collect::<Vec<_>>()
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(items.len(), 2);
assert_eq!(items[0].symbol.as_deref(), Some("AAPL"));
assert_eq!(items[0].total_ask_size.as_deref(), Some("5000"));
assert_eq!(items[0].total_bid_size.as_deref(), Some("6000"));
assert_eq!(items[0].levels, Some(10));
assert!(!items[0].is_status());
assert!(items[1].is_status());
}
#[tokio::test]
async fn test_stream_option_chains() {
let server = helpers::setup_mock_server().await;
let mut client = helpers::mock_client(&server);
let streaming_body = concat!(
r#"{"Symbol":"AAPL 260417C185","Underlying":"AAPL","Type":"Call","StrikePrice":"185.00","ExpirationDate":"2026-04-17","Bid":"5.20","Ask":"5.40","Last":"5.30"}"#,
"\n",
r#"{"Status":"EndSnapshot"}"#,
"\n",
);
Mock::given(method("GET"))
.and(path("/v3/marketdata/stream/options/chains/AAPL"))
.respond_with(ResponseTemplate::new(200).set_body_raw(
streaming_body,
"application/vnd.tradestation.streams.v3+json",
))
.mount(&server)
.await;
let stream = client.stream_option_chains("AAPL").await.unwrap();
let items: Vec<_> = stream
.take(2)
.collect::<Vec<_>>()
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(items.len(), 2);
assert_eq!(items[0].symbol.as_deref(), Some("AAPL 260417C185"));
assert_eq!(items[0].underlying.as_deref(), Some("AAPL"));
assert_eq!(items[0].option_type.as_deref(), Some("Call"));
assert_eq!(items[0].strike_price.as_deref(), Some("185.00"));
assert!(!items[0].is_status());
assert!(items[1].is_status());
}
#[tokio::test]
async fn test_stream_option_quotes() {
let server = helpers::setup_mock_server().await;
let mut client = helpers::mock_client(&server);
let streaming_body = concat!(
r#"{"Symbol":"AAPL260417C185","Bid":"5.20","Ask":"5.40","Last":"5.30","Volume":"1200","OpenInterest":"5000"}"#,
"\n",
r#"{"Status":"EndSnapshot"}"#,
"\n",
);
Mock::given(method("GET"))
.and(path("/v3/marketdata/stream/options/quotes/AAPL260417C185"))
.respond_with(ResponseTemplate::new(200).set_body_raw(
streaming_body,
"application/vnd.tradestation.streams.v3+json",
))
.mount(&server)
.await;
let stream = client
.stream_option_quotes(&["AAPL260417C185"])
.await
.unwrap();
let items: Vec<_> = stream
.take(2)
.collect::<Vec<_>>()
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(items.len(), 2);
assert_eq!(items[0].symbol.as_deref(), Some("AAPL260417C185"));
assert_eq!(items[0].bid.as_deref(), Some("5.20"));
assert_eq!(items[0].ask.as_deref(), Some("5.40"));
assert_eq!(items[0].volume.as_deref(), Some("1200"));
assert_eq!(items[0].open_interest.as_deref(), Some("5000"));
assert!(!items[0].is_status());
assert!(items[1].is_status());
}
#[tokio::test]
async fn test_stream_orders() {
let server = helpers::setup_mock_server().await;
let mut client = helpers::mock_client(&server);
let streaming_body = concat!(
r#"{"OrderId":"ORD001","AccountId":"123","Symbol":"AAPL","Quantity":"100","OrderType":"Limit","OrderStatus":"Open","FilledQuantity":"0"}"#,
"\n",
r#"{"Status":"EndSnapshot"}"#,
"\n",
);
Mock::given(method("GET"))
.and(path("/v3/brokerage/stream/accounts/123/orders"))
.respond_with(ResponseTemplate::new(200).set_body_raw(
streaming_body,
"application/vnd.tradestation.streams.v3+json",
))
.mount(&server)
.await;
let stream = client.stream_orders(&["123"]).await.unwrap();
let items: Vec<_> = stream
.take(2)
.collect::<Vec<_>>()
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(items.len(), 2);
assert_eq!(items[0].order_id.as_deref(), Some("ORD001"));
assert_eq!(items[0].symbol.as_deref(), Some("AAPL"));
assert_eq!(items[0].order_type.as_deref(), Some("Limit"));
assert!(!items[0].is_status());
assert!(items[1].is_status());
}
#[tokio::test]
async fn test_stream_orders_by_id() {
let server = helpers::setup_mock_server().await;
let mut client = helpers::mock_client(&server);
let streaming_body = concat!(
r#"{"OrderId":"ORD001","AccountId":"123","Symbol":"AAPL","Quantity":"100","OrderType":"Limit","OrderStatus":"Open","FilledQuantity":"0"}"#,
"\n",
r#"{"Status":"EndSnapshot"}"#,
"\n",
);
Mock::given(method("GET"))
.and(path("/v3/brokerage/stream/accounts/123/orders/ORD001"))
.respond_with(ResponseTemplate::new(200).set_body_raw(
streaming_body,
"application/vnd.tradestation.streams.v3+json",
))
.mount(&server)
.await;
let stream = client
.stream_orders_by_id(&["123"], &["ORD001"])
.await
.unwrap();
let items: Vec<_> = stream
.take(2)
.collect::<Vec<_>>()
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(items.len(), 2);
assert_eq!(items[0].order_id.as_deref(), Some("ORD001"));
assert!(!items[0].is_status());
assert!(items[1].is_status());
}
#[tokio::test]
async fn test_stream_positions() {
let server = helpers::setup_mock_server().await;
let mut client = helpers::mock_client(&server);
let streaming_body = concat!(
r#"{"AccountId":"123","Symbol":"AAPL","Quantity":"100","AveragePrice":"180.50","Last":"185.30","UnrealizedProfitLoss":"480.00"}"#,
"\n",
r#"{"Status":"EndSnapshot"}"#,
"\n",
);
Mock::given(method("GET"))
.and(path("/v3/brokerage/stream/accounts/123/positions"))
.respond_with(ResponseTemplate::new(200).set_body_raw(
streaming_body,
"application/vnd.tradestation.streams.v3+json",
))
.mount(&server)
.await;
let stream = client.stream_positions(&["123"]).await.unwrap();
let items: Vec<_> = stream
.take(2)
.collect::<Vec<_>>()
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(items.len(), 2);
assert_eq!(items[0].account_id.as_deref(), Some("123"));
assert_eq!(items[0].symbol.as_deref(), Some("AAPL"));
assert_eq!(items[0].quantity.as_deref(), Some("100"));
assert_eq!(items[0].unrealized_profit_loss.as_deref(), Some("480.00"));
assert!(!items[0].is_status());
assert!(items[1].is_status());
}