use serde::{Deserialize, Deserializer, Serialize};
fn null_as_zero<'de, D: Deserializer<'de>>(d: D) -> Result<f64, D::Error> {
Ok(Option::<f64>::deserialize(d)?.unwrap_or(0.0))
}
#[derive(Debug, Deserialize)]
pub struct ServerTimeResponse {
pub time: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SymbolSearchResponse {
pub symbols: Vec<SymbolResult>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SymbolResult {
pub symbol: String,
pub symbol_id: u64,
pub description: String,
pub security_type: String,
pub listing_exchange: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QuoteResponse {
pub quotes: Vec<Quote>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Quote {
pub symbol: String,
pub symbol_id: u64,
pub bid_price: Option<f64>,
pub ask_price: Option<f64>,
pub last_trade_price: Option<f64>,
pub volume: Option<u64>,
pub open_price: Option<f64>,
pub high_price: Option<f64>,
pub low_price: Option<f64>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OptionChainResponse {
pub option_chain: Vec<OptionExpiry>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OptionExpiry {
pub expiry_date: String,
pub description: String,
pub listing_exchange: String,
pub option_exercise_type: String,
pub chain_per_root: Vec<ChainPerRoot>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChainPerRoot {
pub option_root: String,
pub multiplier: Option<u32>,
pub chain_per_strike_price: Vec<ChainPerStrike>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChainPerStrike {
pub strike_price: f64,
pub call_symbol_id: u64,
pub put_symbol_id: u64,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OptionQuoteRequest {
pub option_ids: Vec<u64>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OptionQuoteResponse {
pub option_quotes: Vec<OptionQuote>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OptionQuote {
pub underlying: String,
pub underlying_id: u64,
pub symbol: String,
pub symbol_id: u64,
pub bid_price: Option<f64>,
pub ask_price: Option<f64>,
pub last_trade_price: Option<f64>,
pub volume: Option<u64>,
pub open_interest: Option<u64>,
pub volatility: Option<f64>,
pub delta: Option<f64>,
pub gamma: Option<f64>,
pub theta: Option<f64>,
pub vega: Option<f64>,
pub rho: Option<f64>,
pub strike_price: Option<f64>,
pub expiry_date: Option<String>,
pub option_type: Option<String>,
#[serde(rename = "VWAP")]
pub vwap: Option<f64>,
pub is_halted: Option<bool>,
pub bid_size: Option<u64>,
pub ask_size: Option<u64>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StrategyLeg {
pub symbol_id: u64,
pub action: String,
pub ratio: u32,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StrategyVariantRequest {
pub variant_id: u32,
pub strategy: String,
pub legs: Vec<StrategyLeg>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StrategyQuoteRequest {
pub variants: Vec<StrategyVariantRequest>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StrategyQuotesResponse {
pub strategy_quotes: Vec<StrategyQuote>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StrategyQuote {
pub variant_id: u32,
pub bid_price: Option<f64>,
pub ask_price: Option<f64>,
pub underlying: String,
pub underlying_id: u64,
pub open_price: Option<f64>,
pub volatility: Option<f64>,
pub delta: Option<f64>,
pub gamma: Option<f64>,
pub theta: Option<f64>,
pub vega: Option<f64>,
pub rho: Option<f64>,
pub is_real_time: bool,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountsResponse {
pub accounts: Vec<Account>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Account {
#[serde(rename = "type")]
pub account_type: String,
pub number: String,
pub status: String,
#[serde(default)]
pub is_primary: bool,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PositionsResponse {
pub positions: Vec<PositionItem>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PositionItem {
pub symbol: String,
pub symbol_id: u64,
#[serde(deserialize_with = "null_as_zero")]
pub open_quantity: f64,
pub current_market_value: Option<f64>,
pub current_price: Option<f64>,
#[serde(deserialize_with = "null_as_zero")]
pub average_entry_price: f64,
pub closed_pnl: Option<f64>,
pub open_pnl: Option<f64>,
#[serde(deserialize_with = "null_as_zero")]
pub total_cost: f64,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ActivitiesResponse {
pub activities: Vec<ActivityItem>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ActivityItem {
pub trade_date: String,
#[serde(default)]
pub transaction_date: Option<String>,
#[serde(default)]
pub settlement_date: Option<String>,
#[serde(default)]
pub description: Option<String>,
pub action: String,
pub symbol: String,
pub symbol_id: u64,
pub quantity: f64,
pub price: f64,
#[serde(default)]
pub gross_amount: f64,
#[serde(default)]
pub commission: f64,
pub net_amount: f64,
#[serde(default)]
pub currency: Option<String>,
#[serde(rename = "type")]
pub activity_type: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountBalances {
pub per_currency_balances: Vec<PerCurrencyBalance>,
pub combined_balances: Vec<CombinedBalance>,
pub sod_per_currency_balances: Vec<PerCurrencyBalance>,
pub sod_combined_balances: Vec<CombinedBalance>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PerCurrencyBalance {
pub currency: String,
pub cash: f64,
pub market_value: f64,
pub total_equity: f64,
pub buying_power: f64,
pub maintenance_excess: f64,
pub is_real_time: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CombinedBalance {
pub currency: String,
pub cash: f64,
pub market_value: f64,
pub total_equity: f64,
pub buying_power: f64,
pub maintenance_excess: f64,
pub is_real_time: bool,
}
#[derive(Debug, Deserialize)]
pub struct MarketsResponse {
pub markets: Vec<MarketInfo>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketInfo {
pub name: String,
#[serde(default)]
pub currency: Option<String>,
#[serde(default)]
pub start_time: Option<String>,
#[serde(default)]
pub end_time: Option<String>,
#[serde(default)]
pub extended_start_time: Option<String>,
#[serde(default)]
pub extended_end_time: Option<String>,
#[serde(default)]
pub snapshot: Option<MarketSnapshot>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketSnapshot {
pub is_open: bool,
#[serde(default)]
pub delay: u32,
}
#[derive(Debug, Deserialize)]
pub struct SymbolDetailResponse {
pub symbols: Vec<SymbolDetail>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SymbolDetail {
pub symbol: String,
pub symbol_id: u64,
pub description: String,
pub security_type: String,
pub listing_exchange: String,
pub currency: String,
pub is_tradable: bool,
pub is_quotable: bool,
pub has_options: bool,
pub prev_day_close_price: Option<f64>,
pub high_price52: Option<f64>,
pub low_price52: Option<f64>,
pub average_vol3_months: Option<u64>,
pub average_vol20_days: Option<u64>,
pub outstanding_shares: Option<u64>,
pub eps: Option<f64>,
pub pe: Option<f64>,
pub dividend: Option<f64>,
#[serde(rename = "yield")]
pub dividend_yield: Option<f64>,
pub ex_date: Option<String>,
pub dividend_date: Option<String>,
pub market_cap: Option<f64>,
pub industry_sector: Option<String>,
pub industry_group: Option<String>,
pub industry_sub_group: Option<String>,
pub option_type: Option<String>,
pub option_expiry: Option<String>,
pub option_strike_price: Option<f64>,
pub option_exercise_type: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize)]
pub enum OrderStateFilter {
All,
Open,
Closed,
}
impl std::fmt::Display for OrderStateFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::All => write!(f, "All"),
Self::Open => write!(f, "Open"),
Self::Closed => write!(f, "Closed"),
}
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrdersResponse {
pub orders: Vec<OrderItem>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderItem {
pub id: u64,
pub symbol: String,
pub symbol_id: u64,
#[serde(default)]
pub total_quantity: f64,
#[serde(default)]
pub open_quantity: f64,
#[serde(default)]
pub filled_quantity: f64,
#[serde(default)]
pub canceled_quantity: f64,
pub side: String,
pub order_type: String,
#[serde(default)]
pub limit_price: Option<f64>,
#[serde(default)]
pub stop_price: Option<f64>,
#[serde(default)]
pub avg_exec_price: Option<f64>,
#[serde(default)]
pub last_exec_price: Option<f64>,
#[serde(default)]
pub commission_charged: f64,
pub state: String,
pub time_in_force: String,
pub creation_time: String,
pub update_time: String,
#[serde(default)]
pub notes: Option<String>,
#[serde(default)]
pub is_all_or_none: bool,
#[serde(default)]
pub is_anonymous: bool,
#[serde(default)]
pub order_group_id: Option<u64>,
#[serde(default)]
pub chain_id: Option<u64>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecutionsResponse {
pub executions: Vec<Execution>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Execution {
pub symbol: String,
pub symbol_id: u64,
pub quantity: f64,
pub side: String,
pub price: f64,
pub id: u64,
pub order_id: u64,
pub order_chain_id: u64,
#[serde(default)]
pub exchange_exec_id: Option<String>,
pub timestamp: String,
#[serde(default)]
pub notes: Option<String>,
#[serde(default)]
pub venue: Option<String>,
#[serde(default, deserialize_with = "null_as_zero")]
pub total_cost: f64,
#[serde(default, deserialize_with = "null_as_zero")]
pub order_placement_commission: f64,
#[serde(default, deserialize_with = "null_as_zero")]
pub commission: f64,
#[serde(default, deserialize_with = "null_as_zero")]
pub execution_fee: f64,
#[serde(default, deserialize_with = "null_as_zero")]
pub sec_fee: f64,
#[serde(default, deserialize_with = "null_as_zero")]
pub canadian_execution_fee: f64,
#[serde(default)]
pub parent_id: u64,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CandleResponse {
pub candles: Vec<Candle>,
}
#[derive(Debug, Deserialize)]
pub struct Candle {
pub start: String,
pub end: String,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn markets_response_deserializes_from_questrade_json() {
let json = r#"{
"markets": [
{
"name": "NYSE",
"tradingVenues": ["NYSE"],
"defaultTradingVenue": "NYSE",
"primaryOrderRoutes": ["NYSE"],
"secondaryOrderRoutes": [],
"level1Feeds": ["NYSE"],
"level2Feeds": [],
"extendedStartTime": "2026-02-21T08:00:00.000000-05:00",
"startTime": "2026-02-21T09:30:00.000000-05:00",
"endTime": "2026-02-21T16:00:00.000000-05:00",
"extendedEndTime": "2026-02-21T20:00:00.000000-05:00",
"currency": "USD",
"snapshot": { "isOpen": true, "delay": 0 }
},
{
"name": "TSX",
"tradingVenues": ["TSX"],
"defaultTradingVenue": "TSX",
"primaryOrderRoutes": ["TSX"],
"secondaryOrderRoutes": [],
"level1Feeds": ["TSX"],
"level2Feeds": [],
"extendedStartTime": "2026-02-21T08:00:00.000000-05:00",
"startTime": "2026-02-21T09:30:00.000000-05:00",
"endTime": "2026-02-21T16:00:00.000000-05:00",
"extendedEndTime": "2026-02-21T17:00:00.000000-05:00",
"currency": "CAD",
"snapshot": null
}
]
}"#;
let resp: MarketsResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.markets.len(), 2);
let nyse = &resp.markets[0];
assert_eq!(nyse.name, "NYSE");
assert_eq!(nyse.currency.as_deref(), Some("USD"));
assert_eq!(
nyse.start_time.as_deref(),
Some("2026-02-21T09:30:00.000000-05:00")
);
assert_eq!(
nyse.end_time.as_deref(),
Some("2026-02-21T16:00:00.000000-05:00")
);
let snap = nyse.snapshot.as_ref().unwrap();
assert!(snap.is_open);
assert_eq!(snap.delay, 0);
let tsx = &resp.markets[1];
assert_eq!(tsx.name, "TSX");
assert!(tsx.snapshot.is_none());
}
#[test]
fn account_balances_deserializes_from_questrade_json() {
let json = r#"{
"perCurrencyBalances": [
{
"currency": "CAD",
"cash": 10000.0,
"marketValue": 50000.0,
"totalEquity": 60000.0,
"buyingPower": 60000.0,
"maintenanceExcess": 60000.0,
"isRealTime": false
}
],
"combinedBalances": [
{
"currency": "CAD",
"cash": 10000.0,
"marketValue": 50000.0,
"totalEquity": 60000.0,
"buyingPower": 60000.0,
"maintenanceExcess": 60000.0,
"isRealTime": false
}
],
"sodPerCurrencyBalances": [
{
"currency": "CAD",
"cash": 9000.0,
"marketValue": 49000.0,
"totalEquity": 58000.0,
"buyingPower": 58000.0,
"maintenanceExcess": 58000.0,
"isRealTime": false
}
],
"sodCombinedBalances": [
{
"currency": "CAD",
"cash": 9000.0,
"marketValue": 49000.0,
"totalEquity": 58000.0,
"buyingPower": 58000.0,
"maintenanceExcess": 58000.0,
"isRealTime": false
}
]
}"#;
let balances: AccountBalances = serde_json::from_str(json).unwrap();
assert_eq!(balances.per_currency_balances.len(), 1);
let cad = &balances.per_currency_balances[0];
assert_eq!(cad.currency, "CAD");
assert_eq!(cad.cash, 10000.0);
assert_eq!(cad.market_value, 50000.0);
assert_eq!(cad.total_equity, 60000.0);
assert!(!cad.is_real_time);
assert_eq!(balances.combined_balances.len(), 1);
assert_eq!(balances.sod_per_currency_balances.len(), 1);
assert_eq!(balances.sod_combined_balances.len(), 1);
}
#[test]
fn symbol_detail_deserializes_from_questrade_json() {
let json = r#"{
"symbols": [
{
"symbol": "AAPL",
"symbolId": 8049,
"description": "Apple Inc.",
"securityType": "Stock",
"listingExchange": "NASDAQ",
"currency": "USD",
"isTradable": true,
"isQuotable": true,
"hasOptions": true,
"prevDayClosePrice": 182.50,
"highPrice52": 199.62,
"lowPrice52": 124.17,
"averageVol3Months": 52000000,
"averageVol20Days": 50000000,
"outstandingShares": 15700000000,
"eps": 6.14,
"pe": 29.74,
"dividend": 0.96,
"yield": 0.53,
"exDate": "2023-11-10T00:00:00.000000-05:00",
"dividendDate": "2023-11-16T00:00:00.000000-05:00",
"marketCap": 2866625000000.0,
"industrySector": "Technology",
"industryGroup": "Technology Hardware, Storage & Peripherals",
"industrySubGroup": "Other",
"optionType": null,
"optionExpiry": null,
"optionStrikePrice": null,
"optionExerciseType": null
}
]
}"#;
let resp: SymbolDetailResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.symbols.len(), 1);
let s = &resp.symbols[0];
assert_eq!(s.symbol, "AAPL");
assert_eq!(s.symbol_id, 8049);
assert_eq!(s.description, "Apple Inc.");
assert_eq!(s.security_type, "Stock");
assert_eq!(s.listing_exchange, "NASDAQ");
assert_eq!(s.currency, "USD");
assert!(s.is_tradable);
assert!(s.is_quotable);
assert!(s.has_options);
assert_eq!(s.prev_day_close_price, Some(182.50));
assert_eq!(s.high_price52, Some(199.62));
assert_eq!(s.low_price52, Some(124.17));
assert_eq!(s.eps, Some(6.14));
assert_eq!(s.dividend_yield, Some(0.53));
assert_eq!(s.industry_sector.as_deref(), Some("Technology"));
assert!(s.option_type.is_none());
assert!(s.option_expiry.is_none());
}
#[test]
fn orders_response_deserializes_from_questrade_json() {
let json = r#"{
"orders": [
{
"id": 173577870,
"symbol": "AAPL",
"symbolId": 8049,
"totalQuantity": 100,
"openQuantity": 0,
"filledQuantity": 100,
"canceledQuantity": 0,
"side": "Buy",
"orderType": "Limit",
"limitPrice": 150.50,
"stopPrice": null,
"avgExecPrice": 150.25,
"lastExecPrice": 150.25,
"commissionCharged": 4.95,
"state": "Executed",
"timeInForce": "Day",
"creationTime": "2026-02-20T10:30:00.000000-05:00",
"updateTime": "2026-02-20T10:31:15.000000-05:00",
"notes": null,
"isAllOrNone": false,
"isAnonymous": false,
"orderGroupId": 0,
"chainId": 173577870
},
{
"id": 173600001,
"symbol": "MSFT",
"symbolId": 9291,
"totalQuantity": 50,
"openQuantity": 50,
"filledQuantity": 0,
"canceledQuantity": 0,
"side": "Buy",
"orderType": "Limit",
"limitPrice": 400.00,
"stopPrice": null,
"avgExecPrice": null,
"lastExecPrice": null,
"commissionCharged": 0,
"state": "Pending",
"timeInForce": "GoodTillCanceled",
"creationTime": "2026-02-21T09:45:00.000000-05:00",
"updateTime": "2026-02-21T09:45:00.000000-05:00",
"notes": "Staff note here",
"isAllOrNone": true,
"isAnonymous": false
}
]
}"#;
let resp: OrdersResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.orders.len(), 2);
let o1 = &resp.orders[0];
assert_eq!(o1.id, 173577870);
assert_eq!(o1.symbol, "AAPL");
assert_eq!(o1.symbol_id, 8049);
assert_eq!(o1.total_quantity, 100.0);
assert_eq!(o1.open_quantity, 0.0);
assert_eq!(o1.filled_quantity, 100.0);
assert_eq!(o1.canceled_quantity, 0.0);
assert_eq!(o1.side, "Buy");
assert_eq!(o1.order_type, "Limit");
assert_eq!(o1.limit_price, Some(150.50));
assert!(o1.stop_price.is_none());
assert_eq!(o1.avg_exec_price, Some(150.25));
assert_eq!(o1.last_exec_price, Some(150.25));
assert_eq!(o1.commission_charged, 4.95);
assert_eq!(o1.state, "Executed");
assert_eq!(o1.time_in_force, "Day");
assert!(o1.notes.is_none());
assert!(!o1.is_all_or_none);
assert_eq!(o1.chain_id, Some(173577870));
let o2 = &resp.orders[1];
assert_eq!(o2.id, 173600001);
assert_eq!(o2.symbol, "MSFT");
assert_eq!(o2.state, "Pending");
assert_eq!(o2.time_in_force, "GoodTillCanceled");
assert!(o2.avg_exec_price.is_none());
assert!(o2.last_exec_price.is_none());
assert_eq!(o2.commission_charged, 0.0);
assert_eq!(o2.notes.as_deref(), Some("Staff note here"));
assert!(o2.is_all_or_none);
assert!(o2.order_group_id.is_none());
assert!(o2.chain_id.is_none());
}
#[test]
fn execution_deserializes_from_questrade_json() {
let json = r#"{
"executions": [
{
"symbol": "AAPL",
"symbolId": 8049,
"quantity": 10,
"side": "Buy",
"price": 536.87,
"id": 53817310,
"orderId": 177106005,
"orderChainId": 17710600,
"exchangeExecId": "XS1771060050147",
"timestamp": "2014-03-31T13:38:29.000000-04:00",
"notes": "",
"venue": "LAMP",
"totalCost": 5368.7,
"orderPlacementCommission": 0,
"commission": 4.95,
"executionFee": 0,
"secFee": 0,
"canadianExecutionFee": 0,
"parentId": 0
}
]
}"#;
let resp: ExecutionsResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.executions.len(), 1);
let e = &resp.executions[0];
assert_eq!(e.symbol, "AAPL");
assert_eq!(e.symbol_id, 8049);
assert_eq!(e.quantity, 10.0);
assert_eq!(e.side, "Buy");
assert_eq!(e.price, 536.87);
assert_eq!(e.id, 53817310);
assert_eq!(e.order_id, 177106005);
assert_eq!(e.order_chain_id, 17710600);
assert_eq!(e.exchange_exec_id.as_deref(), Some("XS1771060050147"));
assert_eq!(e.timestamp, "2014-03-31T13:38:29.000000-04:00");
assert_eq!(e.venue.as_deref(), Some("LAMP"));
assert_eq!(e.total_cost, 5368.7);
assert_eq!(e.commission, 4.95);
assert_eq!(e.execution_fee, 0.0);
assert_eq!(e.sec_fee, 0.0);
assert_eq!(e.parent_id, 0);
}
#[test]
fn strategy_quotes_response_deserializes_from_questrade_json() {
let json = r#"{
"strategyQuotes": [
{
"variantId": 1,
"bidPrice": 27.2,
"askPrice": 27.23,
"underlying": "MSFT",
"underlyingId": 9291,
"openPrice": 27.0,
"volatility": 0.30,
"delta": 1.0,
"gamma": 0.0,
"theta": -0.05,
"vega": 0.01,
"rho": 0.002,
"isRealTime": true
}
]
}"#;
let resp: StrategyQuotesResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.strategy_quotes.len(), 1);
let q = &resp.strategy_quotes[0];
assert_eq!(q.variant_id, 1);
assert_eq!(q.bid_price, Some(27.2));
assert_eq!(q.ask_price, Some(27.23));
assert_eq!(q.underlying, "MSFT");
assert_eq!(q.underlying_id, 9291);
assert_eq!(q.open_price, Some(27.0));
assert_eq!(q.volatility, Some(0.30));
assert_eq!(q.delta, Some(1.0));
assert_eq!(q.gamma, Some(0.0));
assert_eq!(q.theta, Some(-0.05));
assert_eq!(q.vega, Some(0.01));
assert_eq!(q.rho, Some(0.002));
assert!(q.is_real_time);
}
#[test]
fn strategy_quote_request_serializes_to_questrade_json() {
let req = StrategyQuoteRequest {
variants: vec![StrategyVariantRequest {
variant_id: 1,
strategy: "Custom".to_string(),
legs: vec![
StrategyLeg {
symbol_id: 27426,
action: "Buy".to_string(),
ratio: 1000,
},
StrategyLeg {
symbol_id: 10550014,
action: "Sell".to_string(),
ratio: 10,
},
],
}],
};
let json = serde_json::to_value(&req).unwrap();
let variants = json["variants"].as_array().unwrap();
assert_eq!(variants.len(), 1);
assert_eq!(variants[0]["variantId"], 1);
assert_eq!(variants[0]["strategy"], "Custom");
let legs = variants[0]["legs"].as_array().unwrap();
assert_eq!(legs.len(), 2);
assert_eq!(legs[0]["symbolId"], 27426);
assert_eq!(legs[0]["action"], "Buy");
assert_eq!(legs[0]["ratio"], 1000);
assert_eq!(legs[1]["symbolId"], 10550014);
assert_eq!(legs[1]["action"], "Sell");
assert_eq!(legs[1]["ratio"], 10);
}
}