1use serde::{Deserialize, Deserializer, Serialize};
4
5fn null_as_zero<'de, D: Deserializer<'de>>(d: D) -> Result<f64, D::Error> {
8 Ok(Option::<f64>::deserialize(d)?.unwrap_or(0.0))
9}
10
11#[derive(Debug, Deserialize)]
15pub struct ServerTimeResponse {
16 pub time: String,
18}
19
20#[derive(Debug, Deserialize)]
24#[serde(rename_all = "camelCase")]
25pub struct SymbolSearchResponse {
26 pub symbols: Vec<SymbolResult>,
28}
29
30#[derive(Debug, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct SymbolResult {
34 pub symbol: String,
36 pub symbol_id: u64,
38 pub description: String,
40 pub security_type: String,
42 pub listing_exchange: String,
44}
45
46#[derive(Debug, Deserialize)]
50#[serde(rename_all = "camelCase")]
51pub struct QuoteResponse {
52 pub quotes: Vec<Quote>,
54}
55
56#[derive(Debug, Deserialize)]
58#[serde(rename_all = "camelCase")]
59pub struct Quote {
60 pub symbol: String,
62 pub symbol_id: u64,
64 pub bid_price: Option<f64>,
66 pub ask_price: Option<f64>,
68 pub last_trade_price: Option<f64>,
70 pub volume: Option<u64>,
72 pub open_price: Option<f64>,
74 pub high_price: Option<f64>,
76 pub low_price: Option<f64>,
78}
79
80#[derive(Debug, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct OptionChainResponse {
86 pub option_chain: Vec<OptionExpiry>,
88}
89
90#[derive(Debug, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct OptionExpiry {
94 pub expiry_date: String,
96 pub description: String,
98 pub listing_exchange: String,
100 pub option_exercise_type: String,
102 pub chain_per_root: Vec<ChainPerRoot>,
104}
105
106#[derive(Debug, Deserialize)]
108#[serde(rename_all = "camelCase")]
109pub struct ChainPerRoot {
110 pub option_root: String,
112 pub multiplier: Option<u32>,
114 pub chain_per_strike_price: Vec<ChainPerStrike>,
116}
117
118#[derive(Debug, Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub struct ChainPerStrike {
122 pub strike_price: f64,
124 pub call_symbol_id: u64,
126 pub put_symbol_id: u64,
128}
129
130#[derive(Debug, Serialize)]
134#[serde(rename_all = "camelCase")]
135pub struct OptionQuoteRequest {
136 pub option_ids: Vec<u64>,
138}
139
140#[derive(Debug, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct OptionQuoteResponse {
144 pub option_quotes: Vec<OptionQuote>,
146}
147
148#[derive(Debug, Deserialize)]
150#[serde(rename_all = "camelCase")]
151pub struct OptionQuote {
152 pub underlying: String,
154 pub underlying_id: u64,
156 pub symbol: String,
158 pub symbol_id: u64,
160 pub bid_price: Option<f64>,
162 pub ask_price: Option<f64>,
164 pub last_trade_price: Option<f64>,
166 pub volume: Option<u64>,
168 pub open_interest: Option<u64>,
170 pub volatility: Option<f64>,
172 pub delta: Option<f64>,
174 pub gamma: Option<f64>,
176 pub theta: Option<f64>,
178 pub vega: Option<f64>,
180 pub rho: Option<f64>,
182 pub strike_price: Option<f64>,
184 pub expiry_date: Option<String>,
186 pub option_type: Option<String>,
188 #[serde(rename = "VWAP")]
190 pub vwap: Option<f64>,
191 pub is_halted: Option<bool>,
193 pub bid_size: Option<u64>,
195 pub ask_size: Option<u64>,
197}
198
199#[derive(Debug, Clone, Serialize)]
203#[serde(rename_all = "camelCase")]
204pub struct StrategyLeg {
205 pub symbol_id: u64,
207 pub action: String,
209 pub ratio: u32,
211}
212
213#[derive(Debug, Clone, Serialize)]
215#[serde(rename_all = "camelCase")]
216pub struct StrategyVariantRequest {
217 pub variant_id: u32,
219 pub strategy: String,
221 pub legs: Vec<StrategyLeg>,
223}
224
225#[derive(Debug, Clone, Serialize)]
227#[serde(rename_all = "camelCase")]
228pub struct StrategyQuoteRequest {
229 pub variants: Vec<StrategyVariantRequest>,
231}
232
233#[derive(Debug, Deserialize)]
235#[serde(rename_all = "camelCase")]
236pub struct StrategyQuotesResponse {
237 pub strategy_quotes: Vec<StrategyQuote>,
239}
240
241#[derive(Debug, Deserialize)]
243#[serde(rename_all = "camelCase")]
244pub struct StrategyQuote {
245 pub variant_id: u32,
247 pub bid_price: Option<f64>,
249 pub ask_price: Option<f64>,
251 pub underlying: String,
253 pub underlying_id: u64,
255 pub open_price: Option<f64>,
257 pub volatility: Option<f64>,
259 pub delta: Option<f64>,
261 pub gamma: Option<f64>,
263 pub theta: Option<f64>,
265 pub vega: Option<f64>,
267 pub rho: Option<f64>,
269 pub is_real_time: bool,
271}
272
273#[derive(Debug, Deserialize)]
277#[serde(rename_all = "camelCase")]
278pub struct AccountsResponse {
279 pub accounts: Vec<Account>,
281}
282
283#[derive(Debug, Clone, Deserialize)]
285#[serde(rename_all = "camelCase")]
286pub struct Account {
287 #[serde(rename = "type")]
289 pub account_type: String,
290 pub number: String,
292 pub status: String,
294 #[serde(default)]
296 pub is_primary: bool,
297}
298
299#[derive(Debug, Deserialize)]
303#[serde(rename_all = "camelCase")]
304pub struct PositionsResponse {
305 pub positions: Vec<PositionItem>,
307}
308
309#[derive(Debug, Clone, Deserialize)]
311#[serde(rename_all = "camelCase")]
312pub struct PositionItem {
313 pub symbol: String,
315 pub symbol_id: u64,
317 #[serde(deserialize_with = "null_as_zero")]
320 pub open_quantity: f64,
321 pub current_market_value: Option<f64>,
323 pub current_price: Option<f64>,
325 #[serde(deserialize_with = "null_as_zero")]
327 pub average_entry_price: f64,
328 pub closed_pnl: Option<f64>,
330 pub open_pnl: Option<f64>,
332 #[serde(deserialize_with = "null_as_zero")]
334 pub total_cost: f64,
335}
336
337#[derive(Debug, Deserialize)]
341#[serde(rename_all = "camelCase")]
342pub struct ActivitiesResponse {
343 pub activities: Vec<ActivityItem>,
345}
346
347#[derive(Debug, Clone, Deserialize, Serialize)]
349#[serde(rename_all = "camelCase")]
350pub struct ActivityItem {
351 pub trade_date: String,
353 #[serde(default)]
355 pub transaction_date: Option<String>,
356 #[serde(default)]
358 pub settlement_date: Option<String>,
359 #[serde(default)]
361 pub description: Option<String>,
362 pub action: String,
364 pub symbol: String,
366 pub symbol_id: u64,
368 pub quantity: f64,
370 pub price: f64,
372 #[serde(default)]
374 pub gross_amount: f64,
375 #[serde(default)]
377 pub commission: f64,
378 pub net_amount: f64,
380 #[serde(default)]
382 pub currency: Option<String>,
383 #[serde(rename = "type")]
385 pub activity_type: String,
386}
387
388#[derive(Debug, Clone, Deserialize)]
394#[serde(rename_all = "camelCase")]
395pub struct AccountBalances {
396 pub per_currency_balances: Vec<PerCurrencyBalance>,
398 pub combined_balances: Vec<CombinedBalance>,
400 pub sod_per_currency_balances: Vec<PerCurrencyBalance>,
402 pub sod_combined_balances: Vec<CombinedBalance>,
404}
405
406#[derive(Debug, Clone, Deserialize)]
408#[serde(rename_all = "camelCase")]
409pub struct PerCurrencyBalance {
410 pub currency: String,
412 pub cash: f64,
414 pub market_value: f64,
416 pub total_equity: f64,
418 pub buying_power: f64,
420 pub maintenance_excess: f64,
422 pub is_real_time: bool,
424}
425
426#[derive(Debug, Clone, Deserialize)]
428#[serde(rename_all = "camelCase")]
429pub struct CombinedBalance {
430 pub currency: String,
432 pub cash: f64,
434 pub market_value: f64,
436 pub total_equity: f64,
438 pub buying_power: f64,
440 pub maintenance_excess: f64,
442 pub is_real_time: bool,
444}
445
446#[derive(Debug, Deserialize)]
450pub struct MarketsResponse {
451 pub markets: Vec<MarketInfo>,
453}
454
455#[derive(Debug, Clone, Deserialize, Serialize)]
457#[serde(rename_all = "camelCase")]
458pub struct MarketInfo {
459 pub name: String,
461 #[serde(default)]
463 pub currency: Option<String>,
464 #[serde(default)]
466 pub start_time: Option<String>,
467 #[serde(default)]
469 pub end_time: Option<String>,
470 #[serde(default)]
472 pub extended_start_time: Option<String>,
473 #[serde(default)]
475 pub extended_end_time: Option<String>,
476 #[serde(default)]
478 pub snapshot: Option<MarketSnapshot>,
479}
480
481#[derive(Debug, Clone, Deserialize, Serialize)]
483#[serde(rename_all = "camelCase")]
484pub struct MarketSnapshot {
485 pub is_open: bool,
487 #[serde(default)]
489 pub delay: u32,
490}
491
492#[derive(Debug, Deserialize)]
496pub struct SymbolDetailResponse {
497 pub symbols: Vec<SymbolDetail>,
499}
500
501#[derive(Debug, Clone, Deserialize)]
503#[serde(rename_all = "camelCase")]
504pub struct SymbolDetail {
505 pub symbol: String,
507 pub symbol_id: u64,
509 pub description: String,
511 pub security_type: String,
513 pub listing_exchange: String,
515 pub currency: String,
517 pub is_tradable: bool,
519 pub is_quotable: bool,
521 pub has_options: bool,
523 pub prev_day_close_price: Option<f64>,
525 pub high_price52: Option<f64>,
527 pub low_price52: Option<f64>,
529 pub average_vol3_months: Option<u64>,
531 pub average_vol20_days: Option<u64>,
533 pub outstanding_shares: Option<u64>,
535 pub eps: Option<f64>,
537 pub pe: Option<f64>,
539 pub dividend: Option<f64>,
541 #[serde(rename = "yield")]
543 pub dividend_yield: Option<f64>,
544 pub ex_date: Option<String>,
546 pub dividend_date: Option<String>,
548 pub market_cap: Option<f64>,
550 pub industry_sector: Option<String>,
552 pub industry_group: Option<String>,
554 pub industry_sub_group: Option<String>,
556 pub option_type: Option<String>,
558 pub option_expiry: Option<String>,
560 pub option_strike_price: Option<f64>,
562 pub option_exercise_type: Option<String>,
564}
565
566#[derive(Debug, Clone, Copy, Serialize)]
570pub enum OrderStateFilter {
571 All,
573 Open,
575 Closed,
577}
578
579impl std::fmt::Display for OrderStateFilter {
580 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
581 match self {
582 Self::All => write!(f, "All"),
583 Self::Open => write!(f, "Open"),
584 Self::Closed => write!(f, "Closed"),
585 }
586 }
587}
588
589#[derive(Debug, Deserialize)]
591#[serde(rename_all = "camelCase")]
592pub struct OrdersResponse {
593 pub orders: Vec<OrderItem>,
595}
596
597#[derive(Debug, Clone, Deserialize, Serialize)]
599#[serde(rename_all = "camelCase")]
600pub struct OrderItem {
601 pub id: u64,
603 pub symbol: String,
605 pub symbol_id: u64,
607 #[serde(default)]
609 pub total_quantity: f64,
610 #[serde(default)]
612 pub open_quantity: f64,
613 #[serde(default)]
615 pub filled_quantity: f64,
616 #[serde(default)]
618 pub canceled_quantity: f64,
619 pub side: String,
621 pub order_type: String,
623 #[serde(default)]
625 pub limit_price: Option<f64>,
626 #[serde(default)]
628 pub stop_price: Option<f64>,
629 #[serde(default)]
631 pub avg_exec_price: Option<f64>,
632 #[serde(default)]
634 pub last_exec_price: Option<f64>,
635 #[serde(default)]
637 pub commission_charged: f64,
638 pub state: String,
640 pub time_in_force: String,
642 pub creation_time: String,
644 pub update_time: String,
646 #[serde(default)]
648 pub notes: Option<String>,
649 #[serde(default)]
651 pub is_all_or_none: bool,
652 #[serde(default)]
654 pub is_anonymous: bool,
655 #[serde(default)]
657 pub order_group_id: Option<u64>,
658 #[serde(default)]
660 pub chain_id: Option<u64>,
661}
662
663#[derive(Debug, Deserialize)]
667#[serde(rename_all = "camelCase")]
668pub struct ExecutionsResponse {
669 pub executions: Vec<Execution>,
671}
672
673#[derive(Debug, Clone, Deserialize, Serialize)]
675#[serde(rename_all = "camelCase")]
676pub struct Execution {
677 pub symbol: String,
679 pub symbol_id: u64,
681 pub quantity: f64,
683 pub side: String,
685 pub price: f64,
687 pub id: u64,
689 pub order_id: u64,
691 pub order_chain_id: u64,
693 #[serde(default)]
695 pub exchange_exec_id: Option<String>,
696 pub timestamp: String,
698 #[serde(default)]
700 pub notes: Option<String>,
701 #[serde(default)]
703 pub venue: Option<String>,
704 #[serde(default, deserialize_with = "null_as_zero")]
706 pub total_cost: f64,
707 #[serde(default, deserialize_with = "null_as_zero")]
709 pub order_placement_commission: f64,
710 #[serde(default, deserialize_with = "null_as_zero")]
712 pub commission: f64,
713 #[serde(default, deserialize_with = "null_as_zero")]
715 pub execution_fee: f64,
716 #[serde(default, deserialize_with = "null_as_zero")]
718 pub sec_fee: f64,
719 #[serde(default, deserialize_with = "null_as_zero")]
721 pub canadian_execution_fee: f64,
722 #[serde(default)]
724 pub parent_id: u64,
725}
726
727#[derive(Debug, Deserialize)]
731#[serde(rename_all = "camelCase")]
732pub struct CandleResponse {
733 pub candles: Vec<Candle>,
735}
736
737#[derive(Debug, Deserialize)]
739pub struct Candle {
740 pub start: String,
742 pub end: String,
744 pub open: f64,
746 pub high: f64,
748 pub low: f64,
750 pub close: f64,
752 pub volume: u64,
754}
755
756#[cfg(test)]
757mod tests {
758 use super::*;
759
760 #[test]
761 fn markets_response_deserializes_from_questrade_json() {
762 let json = r#"{
764 "markets": [
765 {
766 "name": "NYSE",
767 "tradingVenues": ["NYSE"],
768 "defaultTradingVenue": "NYSE",
769 "primaryOrderRoutes": ["NYSE"],
770 "secondaryOrderRoutes": [],
771 "level1Feeds": ["NYSE"],
772 "level2Feeds": [],
773 "extendedStartTime": "2026-02-21T08:00:00.000000-05:00",
774 "startTime": "2026-02-21T09:30:00.000000-05:00",
775 "endTime": "2026-02-21T16:00:00.000000-05:00",
776 "extendedEndTime": "2026-02-21T20:00:00.000000-05:00",
777 "currency": "USD",
778 "snapshot": { "isOpen": true, "delay": 0 }
779 },
780 {
781 "name": "TSX",
782 "tradingVenues": ["TSX"],
783 "defaultTradingVenue": "TSX",
784 "primaryOrderRoutes": ["TSX"],
785 "secondaryOrderRoutes": [],
786 "level1Feeds": ["TSX"],
787 "level2Feeds": [],
788 "extendedStartTime": "2026-02-21T08:00:00.000000-05:00",
789 "startTime": "2026-02-21T09:30:00.000000-05:00",
790 "endTime": "2026-02-21T16:00:00.000000-05:00",
791 "extendedEndTime": "2026-02-21T17:00:00.000000-05:00",
792 "currency": "CAD",
793 "snapshot": null
794 }
795 ]
796 }"#;
797
798 let resp: MarketsResponse = serde_json::from_str(json).unwrap();
799 assert_eq!(resp.markets.len(), 2);
800
801 let nyse = &resp.markets[0];
802 assert_eq!(nyse.name, "NYSE");
803 assert_eq!(nyse.currency.as_deref(), Some("USD"));
804 assert_eq!(
805 nyse.start_time.as_deref(),
806 Some("2026-02-21T09:30:00.000000-05:00")
807 );
808 assert_eq!(
809 nyse.end_time.as_deref(),
810 Some("2026-02-21T16:00:00.000000-05:00")
811 );
812 let snap = nyse.snapshot.as_ref().unwrap();
813 assert!(snap.is_open);
814 assert_eq!(snap.delay, 0);
815
816 let tsx = &resp.markets[1];
818 assert_eq!(tsx.name, "TSX");
819 assert!(tsx.snapshot.is_none());
820 }
821
822 #[test]
823 fn account_balances_deserializes_from_questrade_json() {
824 let json = r#"{
825 "perCurrencyBalances": [
826 {
827 "currency": "CAD",
828 "cash": 10000.0,
829 "marketValue": 50000.0,
830 "totalEquity": 60000.0,
831 "buyingPower": 60000.0,
832 "maintenanceExcess": 60000.0,
833 "isRealTime": false
834 }
835 ],
836 "combinedBalances": [
837 {
838 "currency": "CAD",
839 "cash": 10000.0,
840 "marketValue": 50000.0,
841 "totalEquity": 60000.0,
842 "buyingPower": 60000.0,
843 "maintenanceExcess": 60000.0,
844 "isRealTime": false
845 }
846 ],
847 "sodPerCurrencyBalances": [
848 {
849 "currency": "CAD",
850 "cash": 9000.0,
851 "marketValue": 49000.0,
852 "totalEquity": 58000.0,
853 "buyingPower": 58000.0,
854 "maintenanceExcess": 58000.0,
855 "isRealTime": false
856 }
857 ],
858 "sodCombinedBalances": [
859 {
860 "currency": "CAD",
861 "cash": 9000.0,
862 "marketValue": 49000.0,
863 "totalEquity": 58000.0,
864 "buyingPower": 58000.0,
865 "maintenanceExcess": 58000.0,
866 "isRealTime": false
867 }
868 ]
869 }"#;
870
871 let balances: AccountBalances = serde_json::from_str(json).unwrap();
872 assert_eq!(balances.per_currency_balances.len(), 1);
873 let cad = &balances.per_currency_balances[0];
874 assert_eq!(cad.currency, "CAD");
875 assert_eq!(cad.cash, 10000.0);
876 assert_eq!(cad.market_value, 50000.0);
877 assert_eq!(cad.total_equity, 60000.0);
878 assert!(!cad.is_real_time);
879 assert_eq!(balances.combined_balances.len(), 1);
880 assert_eq!(balances.sod_per_currency_balances.len(), 1);
881 assert_eq!(balances.sod_combined_balances.len(), 1);
882 }
883
884 #[test]
885 fn symbol_detail_deserializes_from_questrade_json() {
886 let json = r#"{
887 "symbols": [
888 {
889 "symbol": "AAPL",
890 "symbolId": 8049,
891 "description": "Apple Inc.",
892 "securityType": "Stock",
893 "listingExchange": "NASDAQ",
894 "currency": "USD",
895 "isTradable": true,
896 "isQuotable": true,
897 "hasOptions": true,
898 "prevDayClosePrice": 182.50,
899 "highPrice52": 199.62,
900 "lowPrice52": 124.17,
901 "averageVol3Months": 52000000,
902 "averageVol20Days": 50000000,
903 "outstandingShares": 15700000000,
904 "eps": 6.14,
905 "pe": 29.74,
906 "dividend": 0.96,
907 "yield": 0.53,
908 "exDate": "2023-11-10T00:00:00.000000-05:00",
909 "dividendDate": "2023-11-16T00:00:00.000000-05:00",
910 "marketCap": 2866625000000.0,
911 "industrySector": "Technology",
912 "industryGroup": "Technology Hardware, Storage & Peripherals",
913 "industrySubGroup": "Other",
914 "optionType": null,
915 "optionExpiry": null,
916 "optionStrikePrice": null,
917 "optionExerciseType": null
918 }
919 ]
920 }"#;
921
922 let resp: SymbolDetailResponse = serde_json::from_str(json).unwrap();
923 assert_eq!(resp.symbols.len(), 1);
924 let s = &resp.symbols[0];
925 assert_eq!(s.symbol, "AAPL");
926 assert_eq!(s.symbol_id, 8049);
927 assert_eq!(s.description, "Apple Inc.");
928 assert_eq!(s.security_type, "Stock");
929 assert_eq!(s.listing_exchange, "NASDAQ");
930 assert_eq!(s.currency, "USD");
931 assert!(s.is_tradable);
932 assert!(s.is_quotable);
933 assert!(s.has_options);
934 assert_eq!(s.prev_day_close_price, Some(182.50));
935 assert_eq!(s.high_price52, Some(199.62));
936 assert_eq!(s.low_price52, Some(124.17));
937 assert_eq!(s.eps, Some(6.14));
938 assert_eq!(s.dividend_yield, Some(0.53));
939 assert_eq!(s.industry_sector.as_deref(), Some("Technology"));
940 assert!(s.option_type.is_none());
941 assert!(s.option_expiry.is_none());
942 }
943
944 #[test]
945 fn orders_response_deserializes_from_questrade_json() {
946 let json = r#"{
947 "orders": [
948 {
949 "id": 173577870,
950 "symbol": "AAPL",
951 "symbolId": 8049,
952 "totalQuantity": 100,
953 "openQuantity": 0,
954 "filledQuantity": 100,
955 "canceledQuantity": 0,
956 "side": "Buy",
957 "orderType": "Limit",
958 "limitPrice": 150.50,
959 "stopPrice": null,
960 "avgExecPrice": 150.25,
961 "lastExecPrice": 150.25,
962 "commissionCharged": 4.95,
963 "state": "Executed",
964 "timeInForce": "Day",
965 "creationTime": "2026-02-20T10:30:00.000000-05:00",
966 "updateTime": "2026-02-20T10:31:15.000000-05:00",
967 "notes": null,
968 "isAllOrNone": false,
969 "isAnonymous": false,
970 "orderGroupId": 0,
971 "chainId": 173577870
972 },
973 {
974 "id": 173600001,
975 "symbol": "MSFT",
976 "symbolId": 9291,
977 "totalQuantity": 50,
978 "openQuantity": 50,
979 "filledQuantity": 0,
980 "canceledQuantity": 0,
981 "side": "Buy",
982 "orderType": "Limit",
983 "limitPrice": 400.00,
984 "stopPrice": null,
985 "avgExecPrice": null,
986 "lastExecPrice": null,
987 "commissionCharged": 0,
988 "state": "Pending",
989 "timeInForce": "GoodTillCanceled",
990 "creationTime": "2026-02-21T09:45:00.000000-05:00",
991 "updateTime": "2026-02-21T09:45:00.000000-05:00",
992 "notes": "Staff note here",
993 "isAllOrNone": true,
994 "isAnonymous": false
995 }
996 ]
997 }"#;
998
999 let resp: OrdersResponse = serde_json::from_str(json).unwrap();
1000 assert_eq!(resp.orders.len(), 2);
1001
1002 let o1 = &resp.orders[0];
1004 assert_eq!(o1.id, 173577870);
1005 assert_eq!(o1.symbol, "AAPL");
1006 assert_eq!(o1.symbol_id, 8049);
1007 assert_eq!(o1.total_quantity, 100.0);
1008 assert_eq!(o1.open_quantity, 0.0);
1009 assert_eq!(o1.filled_quantity, 100.0);
1010 assert_eq!(o1.canceled_quantity, 0.0);
1011 assert_eq!(o1.side, "Buy");
1012 assert_eq!(o1.order_type, "Limit");
1013 assert_eq!(o1.limit_price, Some(150.50));
1014 assert!(o1.stop_price.is_none());
1015 assert_eq!(o1.avg_exec_price, Some(150.25));
1016 assert_eq!(o1.last_exec_price, Some(150.25));
1017 assert_eq!(o1.commission_charged, 4.95);
1018 assert_eq!(o1.state, "Executed");
1019 assert_eq!(o1.time_in_force, "Day");
1020 assert!(o1.notes.is_none());
1021 assert!(!o1.is_all_or_none);
1022 assert_eq!(o1.chain_id, Some(173577870));
1023
1024 let o2 = &resp.orders[1];
1026 assert_eq!(o2.id, 173600001);
1027 assert_eq!(o2.symbol, "MSFT");
1028 assert_eq!(o2.state, "Pending");
1029 assert_eq!(o2.time_in_force, "GoodTillCanceled");
1030 assert!(o2.avg_exec_price.is_none());
1031 assert!(o2.last_exec_price.is_none());
1032 assert_eq!(o2.commission_charged, 0.0);
1033 assert_eq!(o2.notes.as_deref(), Some("Staff note here"));
1034 assert!(o2.is_all_or_none);
1035 assert!(o2.order_group_id.is_none());
1037 assert!(o2.chain_id.is_none());
1038 }
1039
1040 #[test]
1041 fn execution_deserializes_from_questrade_json() {
1042 let json = r#"{
1043 "executions": [
1044 {
1045 "symbol": "AAPL",
1046 "symbolId": 8049,
1047 "quantity": 10,
1048 "side": "Buy",
1049 "price": 536.87,
1050 "id": 53817310,
1051 "orderId": 177106005,
1052 "orderChainId": 17710600,
1053 "exchangeExecId": "XS1771060050147",
1054 "timestamp": "2014-03-31T13:38:29.000000-04:00",
1055 "notes": "",
1056 "venue": "LAMP",
1057 "totalCost": 5368.7,
1058 "orderPlacementCommission": 0,
1059 "commission": 4.95,
1060 "executionFee": 0,
1061 "secFee": 0,
1062 "canadianExecutionFee": 0,
1063 "parentId": 0
1064 }
1065 ]
1066 }"#;
1067
1068 let resp: ExecutionsResponse = serde_json::from_str(json).unwrap();
1069 assert_eq!(resp.executions.len(), 1);
1070
1071 let e = &resp.executions[0];
1072 assert_eq!(e.symbol, "AAPL");
1073 assert_eq!(e.symbol_id, 8049);
1074 assert_eq!(e.quantity, 10.0);
1075 assert_eq!(e.side, "Buy");
1076 assert_eq!(e.price, 536.87);
1077 assert_eq!(e.id, 53817310);
1078 assert_eq!(e.order_id, 177106005);
1079 assert_eq!(e.order_chain_id, 17710600);
1080 assert_eq!(e.exchange_exec_id.as_deref(), Some("XS1771060050147"));
1081 assert_eq!(e.timestamp, "2014-03-31T13:38:29.000000-04:00");
1082 assert_eq!(e.venue.as_deref(), Some("LAMP"));
1083 assert_eq!(e.total_cost, 5368.7);
1084 assert_eq!(e.commission, 4.95);
1085 assert_eq!(e.execution_fee, 0.0);
1086 assert_eq!(e.sec_fee, 0.0);
1087 assert_eq!(e.parent_id, 0);
1088 }
1089
1090 #[test]
1091 fn strategy_quotes_response_deserializes_from_questrade_json() {
1092 let json = r#"{
1093 "strategyQuotes": [
1094 {
1095 "variantId": 1,
1096 "bidPrice": 27.2,
1097 "askPrice": 27.23,
1098 "underlying": "MSFT",
1099 "underlyingId": 9291,
1100 "openPrice": 27.0,
1101 "volatility": 0.30,
1102 "delta": 1.0,
1103 "gamma": 0.0,
1104 "theta": -0.05,
1105 "vega": 0.01,
1106 "rho": 0.002,
1107 "isRealTime": true
1108 }
1109 ]
1110 }"#;
1111
1112 let resp: StrategyQuotesResponse = serde_json::from_str(json).unwrap();
1113 assert_eq!(resp.strategy_quotes.len(), 1);
1114
1115 let q = &resp.strategy_quotes[0];
1116 assert_eq!(q.variant_id, 1);
1117 assert_eq!(q.bid_price, Some(27.2));
1118 assert_eq!(q.ask_price, Some(27.23));
1119 assert_eq!(q.underlying, "MSFT");
1120 assert_eq!(q.underlying_id, 9291);
1121 assert_eq!(q.open_price, Some(27.0));
1122 assert_eq!(q.volatility, Some(0.30));
1123 assert_eq!(q.delta, Some(1.0));
1124 assert_eq!(q.gamma, Some(0.0));
1125 assert_eq!(q.theta, Some(-0.05));
1126 assert_eq!(q.vega, Some(0.01));
1127 assert_eq!(q.rho, Some(0.002));
1128 assert!(q.is_real_time);
1129 }
1130
1131 #[test]
1132 fn strategy_quote_request_serializes_to_questrade_json() {
1133 let req = StrategyQuoteRequest {
1134 variants: vec![StrategyVariantRequest {
1135 variant_id: 1,
1136 strategy: "Custom".to_string(),
1137 legs: vec![
1138 StrategyLeg {
1139 symbol_id: 27426,
1140 action: "Buy".to_string(),
1141 ratio: 1000,
1142 },
1143 StrategyLeg {
1144 symbol_id: 10550014,
1145 action: "Sell".to_string(),
1146 ratio: 10,
1147 },
1148 ],
1149 }],
1150 };
1151
1152 let json = serde_json::to_value(&req).unwrap();
1153 let variants = json["variants"].as_array().unwrap();
1154 assert_eq!(variants.len(), 1);
1155 assert_eq!(variants[0]["variantId"], 1);
1156 assert_eq!(variants[0]["strategy"], "Custom");
1157 let legs = variants[0]["legs"].as_array().unwrap();
1158 assert_eq!(legs.len(), 2);
1159 assert_eq!(legs[0]["symbolId"], 27426);
1160 assert_eq!(legs[0]["action"], "Buy");
1161 assert_eq!(legs[0]["ratio"], 1000);
1162 assert_eq!(legs[1]["symbolId"], 10550014);
1163 assert_eq!(legs[1]["action"], "Sell");
1164 assert_eq!(legs[1]["ratio"], 10);
1165 }
1166}