1mod auth;
2mod error;
3
4pub use crate::auth::AuthenticationInfo;
5pub use crate::error::ApiError;
6use chrono::{DateTime, Utc};
7use http::StatusCode;
8use itertools::Itertools;
9use reqwest::header::AUTHORIZATION;
10use reqwest::{Client, RequestBuilder};
11use serde::de::Error as SerdeError;
12use serde::{Deserialize, Deserializer, Serialize};
13use serde_json::{json, Number, Value};
14use std::cell::RefCell;
15use std::error::Error;
16
17type SymbolId = u32;
18type OrderId = u32;
19type ExecutionId = u32;
20type UserId = u32;
21
22const API_VERSION: &str = "v1";
24
25pub struct Questrade {
27 client: Client,
28 auth_info: RefCell<Option<AuthenticationInfo>>,
29}
30
31impl Questrade {
32 pub fn new() -> Self {
34 Self::with_client(Client::new())
35 }
36
37 pub fn with_client(client: Client) -> Self {
39 Questrade {
40 client,
41 auth_info: RefCell::new(None),
42 }
43 }
44
45 pub fn with_authentication(auth_info: AuthenticationInfo, client: Client) -> Self {
47 Questrade {
48 client,
49 auth_info: RefCell::new(Some(auth_info)),
50 }
51 }
52
53 pub async fn authenticate(
57 &self,
58 refresh_token: &str,
59 is_demo: bool,
60 ) -> Result<(), Box<dyn Error>> {
61 self.auth_info.replace(Some(
62 AuthenticationInfo::authenticate(refresh_token, is_demo, &self.client).await?,
63 ));
64
65 Ok(())
66 }
67
68 pub fn get_auth_info(&self) -> Option<AuthenticationInfo> {
70 self.auth_info.borrow().clone()
71 }
72
73 fn get_active_auth(&self) -> Result<AuthenticationInfo, ApiError> {
75 self.auth_info
76 .borrow()
77 .clone()
78 .ok_or(ApiError::NotAuthenticatedError(StatusCode::UNAUTHORIZED))
79 }
80
81 pub async fn accounts(&self) -> Result<Vec<Account>, Box<dyn Error>> {
87 #[derive(Serialize, Deserialize)]
88 struct AccountsResponse {
89 accounts: Vec<Account>,
90 }
91
92 let response = self
93 .get_request_builder("accounts")?
94 .send()
95 .await?
96 .error_for_status()
97 .map_err(|e| wrap_error(e))?
98 .json::<AccountsResponse>()
99 .await?;
100
101 Ok(response.accounts)
102 }
103
104 pub async fn account_activity(
106 &self,
107 account_number: &str,
108 start_time: DateTime<Utc>,
109 end_time: DateTime<Utc>,
110 ) -> Result<Vec<AccountActivity>, Box<dyn Error>> {
111 #[derive(Serialize, Deserialize)]
112 struct AccountActivityResponse {
113 activities: Vec<AccountActivity>,
114 }
115
116 let response = self
117 .get_request_builder(format!("accounts/{}/activities", account_number).as_str())?
118 .query(&[
119 ("startTime", start_time.to_rfc3339()),
120 ("endTime", end_time.to_rfc3339()),
121 ])
122 .send()
123 .await?
124 .error_for_status()
125 .map_err(|e| wrap_error(e))?
126 .json::<AccountActivityResponse>()
127 .await?;
128
129 Ok(response.activities)
130 }
131
132 pub async fn account_orders(
139 &self,
140 account_number: &str,
141 start_time: Option<DateTime<Utc>>,
142 end_time: Option<DateTime<Utc>>,
143 state: Option<OrderStateFilter>,
144 ) -> Result<Vec<AccountOrder>, Box<dyn Error>> {
145 #[derive(Debug, Serialize, Deserialize)]
146 struct AccountOrdersResponse {
147 orders: Vec<AccountOrder>,
148 }
149
150 let mut query_params: Vec<(&str, String)> = Vec::new();
151 if let Some(start_time) = start_time {
152 query_params.push(("startTime", start_time.to_rfc3339()))
153 }
154
155 if let Some(end_time) = end_time {
156 query_params.push(("endTime", end_time.to_rfc3339()))
157 }
158
159 if let Some(state) = state {
160 let state = match state {
161 OrderStateFilter::All => "All",
162 OrderStateFilter::Open => "Open",
163 OrderStateFilter::Closed => "Closed",
164 };
165
166 query_params.push(("stateFilter", state.to_string()))
167 }
168
169 let response = self
170 .get_request_builder(format!("accounts/{}/orders", account_number).as_str())?
171 .query(query_params.as_slice())
172 .send()
173 .await?
174 .error_for_status()
175 .map_err(|e| wrap_error(e))?
176 .json::<AccountOrdersResponse>()
177 .await?;
178
179 Ok(response.orders)
180 }
181
182 pub async fn account_order(
184 &self,
185 account_number: &str,
186 order_id: OrderId,
187 ) -> Result<Option<AccountOrder>, Box<dyn Error>> {
188 #[derive(Serialize, Deserialize)]
189 struct AccountOrdersResponse {
190 orders: Vec<AccountOrder>,
191 }
192
193 let mut response = self
194 .get_request_builder(
195 format!("accounts/{}/orders/{}", account_number, order_id).as_str(),
196 )?
197 .send()
198 .await?
199 .error_for_status()
200 .map_err(|e| wrap_error(e))?
201 .json::<AccountOrdersResponse>()
202 .await?;
203
204 return Ok(response.orders.pop());
205 }
206
207 pub async fn account_executions(
213 &self,
214 account_number: &str,
215 start_time: Option<DateTime<Utc>>,
216 end_time: Option<DateTime<Utc>>,
217 ) -> Result<Vec<AccountExecution>, Box<dyn Error>> {
218 #[derive(Serialize, Deserialize)]
219 struct AccountExecutionsResponse {
220 executions: Vec<AccountExecution>,
221 }
222
223 let mut query_params: Vec<(&str, String)> = Vec::new();
224 if let Some(start_time) = start_time {
225 query_params.push(("startTime", start_time.to_rfc3339()))
226 }
227
228 if let Some(end_time) = end_time {
229 query_params.push(("endTime", end_time.to_rfc3339()))
230 }
231
232 let response = self
233 .get_request_builder(format!("accounts/{}/executions", account_number).as_str())?
234 .query(query_params.as_slice())
235 .send()
236 .await?
237 .error_for_status()
238 .map_err(|e| wrap_error(e))?
239 .json::<AccountExecutionsResponse>()
240 .await?;
241
242 Ok(response.executions)
243 }
244
245 pub async fn account_balance(
247 &self,
248 account_number: &str,
249 ) -> Result<AccountBalances, Box<dyn Error>> {
250 let response = self
251 .get_request_builder(format!("accounts/{}/balances", account_number).as_str())?
252 .send()
253 .await?
254 .error_for_status()
255 .map_err(|e| wrap_error(e))?
256 .json::<AccountBalances>()
257 .await?;
258
259 Ok(response)
260 }
261
262 pub async fn account_positions(
264 &self,
265 account_number: &str,
266 ) -> Result<Vec<AccountPosition>, Box<dyn Error>> {
267 #[derive(Serialize, Deserialize)]
268 struct AccountPositionsResponse {
269 positions: Vec<AccountPosition>,
270 }
271
272 let response = self
273 .get_request_builder(format!("accounts/{}/positions", account_number).as_str())?
274 .send()
275 .await?
276 .error_for_status()
277 .map_err(|e| wrap_error(e))?
278 .json::<AccountPositionsResponse>()
279 .await?;
280
281 Ok(response.positions)
282 }
283
284 pub async fn market_quote(&self, ids: &[SymbolId]) -> Result<Vec<MarketQuote>, Box<dyn Error>> {
297 #[derive(Serialize, Deserialize)]
298 struct MarketQuoteResponse {
299 quotes: Vec<MarketQuote>,
300 }
301
302 let ids = ids.iter().map(ToString::to_string).join(",");
303
304 let response = self
305 .get_request_builder("markets/quotes")?
306 .query(&[("ids", ids)])
307 .send()
308 .await?
309 .error_for_status()
310 .map_err(|e| wrap_error(e))?
311 .json::<MarketQuoteResponse>()
312 .await?;
313
314 Ok(response.quotes)
315 }
316
317 pub async fn symbol_search(
327 &self,
328 prefix: &str,
329 offset: u32,
330 ) -> Result<Vec<SearchEquitySymbol>, Box<dyn Error>> {
331 #[derive(Serialize, Deserialize)]
332 struct SymbolSearchResponse {
333 symbols: Vec<SearchEquitySymbol>,
334 }
335
336 let response = self
337 .get_request_builder("symbols/search")?
338 .query(&[("prefix", prefix), ("offset", &offset.to_string())])
339 .send()
340 .await?
341 .error_for_status()
342 .map_err(|e| wrap_error(e))?
343 .json::<SymbolSearchResponse>()
344 .await?;
345
346 Ok(response.symbols)
347 }
348
349 pub async fn time(&self) -> Result<DateTime<Utc>, Box<dyn Error>> {
353 #[derive(Serialize, Deserialize)]
354 struct TimeResponse {
355 time: DateTime<Utc>,
356 }
357
358 let response = self
359 .get_request_builder("time")?
360 .send()
361 .await?
362 .error_for_status()
363 .map_err(|e| wrap_error(e))?
364 .json::<TimeResponse>()
365 .await?;
366
367 Ok(response.time)
368 }
369
370 fn get_request_builder(&self, url_suffix: &str) -> Result<RequestBuilder, Box<dyn Error>> {
372 let auth_info = self.get_active_auth()?;
373
374 Ok(self
375 .client
376 .get(&format!(
377 "{}/{}/{}",
378 auth_info.api_server, API_VERSION, url_suffix
379 ))
380 .header(AUTHORIZATION, format!("Bearer {}", auth_info.access_token)))
381 }
382}
383
384fn wrap_error(e: reqwest::Error) -> Box<dyn Error> {
385 if e.is_status() {
386 let status = e.status().unwrap();
387
388 if status == 401 || status == 403 {
389 return Box::new(ApiError::NotAuthenticatedError(status));
390 }
391 }
392
393 Box::new(e)
394}
395
396#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
400pub struct Account {
401 #[serde(rename = "type")]
403 pub account_type: AccountType,
404
405 pub number: String,
407
408 pub status: AccountStatus,
410
411 #[serde(rename = "isPrimary")]
413 pub is_primary: bool,
414
415 #[serde(rename = "isBilling")]
417 pub is_billing: bool,
418
419 #[serde(rename = "clientAccountType")]
421 pub client_account_type: ClientAccountType,
422}
423
424#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
426pub enum AccountType {
427 Cash,
429
430 Margin,
432
433 TFSA,
435
436 RRSP,
438
439 SRRSP,
441
442 LRRSP,
444
445 LIRA,
447
448 LIF,
450
451 RIF,
453
454 SRIF,
456
457 LRIF,
459
460 RRIF,
462
463 PRIF,
465
466 RESP,
468
469 FRESP,
471}
472
473#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
475pub enum AccountStatus {
476 Active,
477
478 #[serde(rename = "Suspended (Closed)")]
479 SuspendedClosed,
480
481 #[serde(rename = "Suspended (View Only)")]
482 SuspendedViewOnly,
483
484 #[serde(rename = "Liquidate Only")]
485 Liquidate,
486
487 Closed,
488}
489
490#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
492pub enum ClientAccountType {
493 Individual,
495
496 Joint,
498
499 #[serde(rename = "Informal Trust")]
501 InformalTrust,
502
503 Corporation,
505
506 #[serde(rename = "Investment Club")]
508 InvestmentClub,
509
510 #[serde(rename = "Formal Trust")]
512 FormalTrust,
513
514 Partnership,
516
517 #[serde(rename = "Sole Proprietorship")]
519 SoleProprietorship,
520
521 Family,
523
524 #[serde(rename = "Joint and Informal Trust")]
526 JointAndInformalTrust,
527
528 Institution,
530}
531
532#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
534pub struct AccountActivity {
535 #[serde(rename = "tradeDate")]
537 pub trade_date: DateTime<Utc>,
538
539 #[serde(rename = "transactionDate")]
541 pub transaction_date: DateTime<Utc>,
542
543 #[serde(rename = "settlementDate")]
545 pub settlement_date: DateTime<Utc>,
546
547 pub action: String,
549
550 pub symbol: String,
552
553 #[serde(rename = "symbolId")]
555 pub symbol_id: SymbolId,
556
557 pub description: String,
559
560 pub currency: String,
562
563 pub quantity: Number,
565
566 pub price: Number,
568
569 #[serde(rename = "grossAmount")]
571 pub gross_amount: Number,
572
573 pub commission: Number,
575
576 #[serde(rename = "netAmount")]
578 pub net_amount: Number,
579
580 #[serde(rename = "type")]
582 pub activity_type: String,
583}
584
585#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
586pub struct AccountOrder {
587 pub id: OrderId,
589
590 pub symbol: String,
592
593 #[serde(rename = "symbolId")]
595 pub symbol_id: SymbolId,
596
597 #[serde(rename = "totalQuantity")]
599 pub total_quantity: Number,
600
601 #[serde(rename = "openQuantity")]
603 #[serde(deserialize_with = "deserialize_nullable_number")]
604 pub open_quantity: Number,
605
606 #[serde(rename = "filledQuantity")]
608 #[serde(deserialize_with = "deserialize_nullable_number")]
609 pub filled_quantity: Number,
610
611 #[serde(rename = "canceledQuantity")]
613 #[serde(deserialize_with = "deserialize_nullable_number")]
614 pub canceled_quantity: Number,
615
616 pub side: OrderSide,
618
619 #[serde(rename = "orderType")]
621 #[serde(alias = "type")]
622 pub order_type: OrderType,
623
624 #[serde(rename = "limitPrice")]
626 pub limit_price: Option<Number>,
627
628 #[serde(rename = "stopPrice")]
630 pub stop_price: Option<Number>,
631
632 #[serde(rename = "isAllOrNone")]
634 pub is_all_or_none: bool,
635
636 #[serde(rename = "isAnonymous")]
638 pub is_anonymous: bool,
639
640 #[serde(rename = "icebergQuantity")]
642 pub iceberg_quantity: Option<Number>,
643
644 #[serde(rename = "minQuantity")]
646 pub min_quantity: Option<Number>,
647
648 #[serde(rename = "avgExecPrice")]
650 pub avg_execution_price: Option<Number>,
651
652 #[serde(rename = "lastExecPrice")]
654 pub last_execution_price: Option<Number>,
655
656 pub source: String,
658
659 #[serde(rename = "timeInForce")]
660 pub time_in_force: OrderTimeInForce,
661
662 #[serde(rename = "gtdDate")]
664 pub good_till_date: Option<DateTime<Utc>>,
665
666 pub state: OrderState,
668
669 #[serde(rename = "clientReasonStr")]
671 #[serde(alias = "rejectionReason")]
672 #[serde(deserialize_with = "serde_with::rust::string_empty_as_none::deserialize")]
673 pub rejection_reason: Option<String>,
674
675 #[serde(rename = "chainId")]
677 pub chain_id: OrderId,
678
679 #[serde(rename = "creationTime")]
681 pub creation_time: DateTime<Utc>,
682
683 #[serde(rename = "updateTime")]
685 pub update_time: DateTime<Utc>,
686
687 #[serde(deserialize_with = "serde_with::rust::string_empty_as_none::deserialize")]
689 pub notes: Option<String>,
690
691 #[serde(rename = "primaryRoute")]
692 pub primary_route: String,
693
694 #[serde(rename = "secondaryRoute")]
695 #[serde(deserialize_with = "serde_with::rust::string_empty_as_none::deserialize")]
696 pub secondary_route: Option<String>,
697
698 #[serde(rename = "orderRoute")]
700 pub order_route: String,
701
702 #[serde(rename = "venueHoldingOrder")]
704 #[serde(deserialize_with = "serde_with::rust::string_empty_as_none::deserialize")]
705 pub venue_holding_order: Option<String>,
706
707 #[serde(rename = "comissionCharged")]
709 #[serde(deserialize_with = "deserialize_nullable_number")]
710 pub commission_charged: Number,
711
712 #[serde(rename = "exchangeOrderId")]
714 pub exchange_order_id: String,
715
716 #[serde(rename = "isSignificantShareHolder")]
718 pub is_significant_shareholder: bool,
719
720 #[serde(rename = "isInsider")]
722 pub is_insider: bool,
723
724 #[serde(rename = "isLimitOffsetInDollar")]
726 pub is_limit_offset_in_dollars: bool,
727
728 #[serde(rename = "userId")]
730 pub user_id: UserId,
731
732 #[serde(rename = "placementCommission")]
734 #[serde(deserialize_with = "deserialize_nullable_number")]
735 pub placement_commission: Number,
736
737 #[serde(rename = "strategyType")]
741 pub strategy_type: String,
742
743 #[serde(rename = "triggerStopPrice")]
745 pub trigger_stop_price: Option<Number>,
746
747 #[serde(rename = "orderGroupId")]
749 pub order_group_id: OrderId,
750
751 #[serde(rename = "orderClass")]
753 pub order_class: Option<String>,
754}
755
756fn deserialize_nullable_number<'de, D>(deserializer: D) -> Result<Number, D::Error>
757where
758 D: Deserializer<'de>,
759{
760 let number: Option<Number> = Deserialize::deserialize(deserializer)?;
761
762 match number {
763 Some(num) => Ok(num),
764 None => match json!(0) {
765 Value::Number(n) => Ok(n),
766 _ => Err(D::Error::custom(format!(
767 "json!(0) did not return a Value::Number",
768 ))),
769 },
770 }
771}
772
773#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
774pub enum OrderSide {
775 Buy,
776
777 Sell,
778
779 Short,
781
782 #[serde(rename = "Cov")]
783 Cover,
784
785 #[serde(rename = "BTO")]
786 BuyToOpen,
787
788 #[serde(rename = "STC")]
789 SellToClose,
790
791 #[serde(rename = "STO")]
792 SellToOpen,
793
794 #[serde(rename = "BTC")]
795 BuyToClose,
796}
797
798#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
799pub enum OrderType {
800 Market,
801 Limit,
802 Stop,
803 StopLimit,
804 TrailStopInPercentage,
805 TrailStopInDollar,
806 TrailStopLimitInPercentage,
807 TrailStopLimitInDollar,
808 LimitOnOpen,
809 LimitOnClose,
810}
811
812#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
813pub enum OrderTimeInForce {
814 Day,
815 GoodTillCanceled,
816 GoodTillExtendedDay,
817 GoodTillDate,
818 ImmediateOrCancel,
819 FillOrKill,
820}
821
822#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
823pub enum OrderState {
824 Failed,
825 Pending,
826 Accepted,
827 Rejected,
828 CancelPending,
829 Canceled,
830 PartialCanceled,
831 Partial,
832 Executed,
833 ReplacePending,
834 Replaced,
835 Stopped,
836 Suspended,
837 Expired,
838 Queued,
839 Triggered,
840 Activated,
841 PendingRiskReview,
842 ContingentOrder,
843}
844
845#[derive(Clone, PartialEq, Debug)]
846pub enum OrderStateFilter {
847 All,
848 Open,
849 Closed,
850}
851
852#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
854pub struct AccountExecution {
855 pub id: ExecutionId,
857
858 #[serde(rename = "orderId")]
860 pub order_id: OrderId,
861
862 pub symbol: String,
864
865 #[serde(rename = "symbolId")]
867 pub symbol_id: SymbolId,
868
869 #[serde(rename = "quantity")]
871 pub quantity: Number,
872
873 pub side: OrderSide,
875
876 pub price: Number,
878
879 #[serde(rename = "orderChainId")]
881 pub order_chain_id: OrderId,
882
883 pub timestamp: DateTime<Utc>,
885
886 #[serde(deserialize_with = "serde_with::rust::string_empty_as_none::deserialize")]
888 pub notes: Option<String>,
889
890 pub commission: Number,
892
893 #[serde(rename = "executionFee")]
895 pub execution_fee: Number,
896
897 #[serde(rename = "secFee")]
899 pub sec_fee: Number,
900
901 #[serde(rename = "canadianExecutionFee")]
903 pub canadian_execution_fee: Number,
904
905 #[serde(rename = "parentId")]
907 pub parent_id: OrderId,
908}
909
910#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
912pub struct AccountBalance {
913 pub currency: Currency,
915
916 pub cash: Number,
918
919 #[serde(rename = "marketValue")]
921 pub market_value: Number,
922
923 #[serde(rename = "totalEquity")]
925 pub total_equity: Number,
926
927 #[serde(rename = "buyingPower")]
929 pub buying_power: Number,
930
931 #[serde(rename = "maintenanceExcess")]
933 pub maintenance_excess: Number,
934
935 #[serde(rename = "isRealTime")]
937 pub is_real_time: bool,
938}
939
940#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
941pub enum Currency {
942 CAD,
943 USD,
944}
945
946#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
948pub struct AccountBalances {
949 #[serde(rename = "perCurrencyBalances")]
950 pub per_currency_balances: Vec<AccountBalance>,
951
952 #[serde(rename = "combinedBalances")]
953 pub combined_balances: Vec<AccountBalance>,
954
955 #[serde(rename = "sodPerCurrencyBalances")]
956 pub sod_per_currency_balances: Vec<AccountBalance>,
957
958 #[serde(rename = "sodCombinedBalances")]
959 pub sod_combined_balances: Vec<AccountBalance>,
960}
961
962fn none_is_zero<'de, D>(deserializer: D) -> Result<Number, D::Error>
963where
964 D: Deserializer<'de>,
965{
966 let o: Option<Number> = Option::deserialize(deserializer)?;
967 Ok(o.unwrap_or(Number::from(0)))
968}
969
970#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
972pub struct AccountPosition {
973 pub symbol: String,
975
976 #[serde(rename = "symbolId")]
978 pub symbol_id: SymbolId,
979
980 #[serde(rename = "openQuantity")]
982 pub open_quantity: Number,
983
984 #[serde(rename = "closedQuantity")]
986 pub closed_quantity: Number,
987
988 #[serde(rename = "currentMarketValue")]
990 pub current_market_value: Number,
991
992 #[serde(rename = "currentPrice")]
994 pub current_price: Number,
995
996 #[serde(rename = "dayPnl")]
998 #[serde(deserialize_with = "none_is_zero")]
999 pub day_profit_and_loss: Number,
1000
1001 #[serde(rename = "averageEntryPrice")]
1003 pub average_entry_price: Number,
1004
1005 #[serde(rename = "closedPnl")]
1007 pub closed_profit_and_loss: Number,
1008
1009 #[serde(rename = "openPnl")]
1011 pub open_profit_and_loss: Number,
1012
1013 #[serde(rename = "totalCost")]
1015 pub total_cost: Number,
1016
1017 #[serde(rename = "isRealTime")]
1019 pub is_real_time: bool,
1020
1021 #[serde(rename = "isUnderReorg")]
1023 pub is_under_reorg: bool,
1024}
1025
1026#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
1032pub struct MarketQuote {
1033 pub symbol: String,
1035
1036 #[serde(rename = "symbolId")]
1038 pub symbol_id: SymbolId,
1039
1040 #[serde(deserialize_with = "serde_with::rust::string_empty_as_none::deserialize")]
1042 pub tier: Option<String>, #[serde(rename = "bidPrice")]
1046 pub bid_price: Option<Number>,
1047
1048 #[serde(rename = "bidSize")]
1050 pub bid_size: u32,
1051
1052 #[serde(rename = "askPrice")]
1054 pub ask_price: Option<Number>,
1055
1056 #[serde(rename = "askSize")]
1058 pub ask_size: u32,
1059
1060 #[serde(rename = "lastTradePriceTrHrs")]
1063 pub last_trade_price_tr_hrs: Number,
1064
1065 #[serde(rename = "lastTradePrice")]
1069 pub last_trade_price: Number,
1070
1071 #[serde(rename = "lastTradeSize")]
1073 pub last_trade_size: u32,
1074
1075 #[serde(rename = "lastTradeTick")]
1077 pub last_trade_tick: TickType,
1078
1079 pub volume: u32,
1081
1082 #[serde(rename = "openPrice")]
1084 pub open_price: Number,
1085
1086 #[serde(rename = "highPrice")]
1088 pub high_price: Number,
1089
1090 #[serde(rename = "lowPrice")]
1092 pub low_price: Number,
1093
1094 #[serde(deserialize_with = "deserialize_delay")]
1098 pub delay: bool,
1099
1100 #[serde(rename = "isHalted")]
1102 pub is_halted: bool,
1103}
1104
1105fn deserialize_delay<'de, D>(deserializer: D) -> Result<bool, D::Error>
1106where
1107 D: Deserializer<'de>,
1108{
1109 let delay: u8 = Deserialize::deserialize(deserializer)?;
1110
1111 match delay {
1112 0 => Ok(false),
1113 1 => Ok(true),
1114 _ => Err(D::Error::custom(format!(
1115 "expected delay to be '0' or '1'. Got: {}",
1116 delay
1117 ))),
1118 }
1119}
1120
1121#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
1123pub struct SearchEquitySymbol {
1124 pub symbol: String,
1126
1127 #[serde(rename = "symbolId")]
1129 pub symbol_id: SymbolId,
1130
1131 pub description: String,
1133
1134 #[serde(rename = "securityType")]
1136 pub security_type: SecurityType,
1137
1138 #[serde(rename = "listingExchange")]
1140 pub listing_exchange: ListingExchange,
1141
1142 #[serde(rename = "isQuotable")]
1144 pub is_quotable: bool,
1145
1146 #[serde(rename = "isTradable")]
1148 pub is_tradable: bool,
1149
1150 pub currency: Currency,
1152}
1153
1154#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
1156pub enum ListingExchange {
1157 TSX,
1159
1160 TSXI,
1162
1163 TSXV,
1165
1166 CNSX,
1168
1169 MX,
1171
1172 NASDAQ,
1174
1175 NASDAQI,
1177
1178 NYSE,
1180
1181 NYSEAM,
1183
1184 NYSEGIF,
1186
1187 ARCA,
1189
1190 OPRA,
1192
1193 #[serde(rename = "PINX")]
1195 PinkSheets,
1196
1197 OTCBB,
1199
1200 BATS,
1202
1203 #[serde(rename = "DJI")]
1205 DowJonesAverage,
1206
1207 #[serde(rename = "S&P")]
1209 SP,
1210
1211 NEO,
1213
1214 RUSSELL,
1216
1217 #[serde(rename = "")]
1219 None,
1220}
1221
1222#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
1224pub enum SecurityType {
1225 Stock,
1227
1228 Option,
1230
1231 Bond,
1233
1234 Right,
1236
1237 Gold,
1239
1240 MutualFund,
1242
1243 Index,
1245}
1246
1247#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
1249pub enum TickType {
1250 Up,
1252
1253 Down,
1255
1256 Equal,
1258}
1259
1260#[cfg(test)]
1263mod tests {
1264 use crate::auth::AuthenticationInfo;
1265 use crate::{
1266 Account, AccountBalance, AccountBalances, AccountExecution, AccountOrder, AccountPosition,
1267 AccountStatus, AccountType, ClientAccountType, Currency, ListingExchange, MarketQuote,
1268 OrderSide, OrderState, OrderTimeInForce, OrderType, Questrade, SearchEquitySymbol,
1269 SecurityType, TickType,
1270 };
1271 use chrono::{FixedOffset, TimeZone, Utc};
1272 use reqwest::Client;
1273 use std::error::Error;
1274 use std::time::Instant;
1275
1276 use mockito;
1277 use mockito::{mock, Matcher};
1278 use serde_json::{json, Number, Value};
1279 use std::fs::read_to_string;
1280
1281 trait AsNumber {
1282 fn to_number(self) -> Number;
1283 }
1284
1285 impl AsNumber for Value {
1286 fn to_number(self) -> Number {
1287 match self {
1288 Value::Number(n) => n,
1289 _ => panic!("Not a number"),
1290 }
1291 }
1292 }
1293
1294 fn get_api() -> Questrade {
1295 let auth_info = AuthenticationInfo {
1296 access_token: "mock-access-token".to_string(),
1297 api_server: mockito::server_url(),
1298 refresh_token: "".to_string(),
1299 expires_at: Instant::now(),
1300 is_demo: false,
1301 };
1302
1303 Questrade::with_authentication(auth_info, Client::new())
1304 }
1305
1306 #[tokio::test]
1308 async fn accounts() -> Result<(), Box<dyn Error>> {
1309 let _m = mock("GET", "/v1/accounts")
1310 .with_status(200)
1311 .with_header("content-type", "text/json")
1312 .with_body(read_to_string("test/response/accounts.json")?)
1313 .create();
1314
1315 let result = get_api().accounts().await;
1316
1317 assert_eq!(
1318 result?,
1319 vec![
1320 Account {
1321 account_type: AccountType::Margin,
1322 number: "123456".to_string(),
1323 status: AccountStatus::Active,
1324 is_primary: false,
1325 is_billing: false,
1326 client_account_type: ClientAccountType::Joint,
1327 },
1328 Account {
1329 account_type: AccountType::Cash,
1330 number: "26598145".to_string(),
1331 status: AccountStatus::Active,
1332 is_primary: true,
1333 is_billing: true,
1334 client_account_type: ClientAccountType::Individual,
1335 },
1336 ]
1337 );
1338
1339 Ok(())
1340 }
1341
1342 #[tokio::test]
1343 async fn account_orders() -> Result<(), Box<dyn Error>> {
1344 let _m = mock("GET", "/v1/accounts/123456/orders")
1345 .with_status(200)
1346 .with_header("content-type", "text/json")
1347 .with_body(read_to_string("test/response/account-orders.json")?)
1348 .create();
1349
1350 let result = get_api().account_orders("123456", None, None, None).await;
1351
1352 assert_eq!(
1353 result?,
1354 vec![
1355 AccountOrder {
1356 id: 173577870,
1357 symbol: "AAPL".to_string(),
1358 symbol_id: 8049,
1359 total_quantity: json!(100).to_number(),
1360 open_quantity: json!(100).to_number(),
1361 filled_quantity: json!(0).to_number(),
1362 canceled_quantity: json!(0).to_number(),
1363 side: OrderSide::Buy,
1364 order_type: OrderType::Limit,
1365 limit_price: Some(json!(500.95).to_number()),
1366 stop_price: None,
1367 is_all_or_none: false,
1368 is_anonymous: false,
1369 iceberg_quantity: None,
1370 min_quantity: None,
1371 avg_execution_price: None,
1372 last_execution_price: None,
1373 source: "TradingAPI".to_string(),
1374 time_in_force: OrderTimeInForce::Day,
1375 good_till_date: None,
1376 state: OrderState::Canceled,
1377 rejection_reason: None,
1378 chain_id: 173577870,
1379 creation_time: FixedOffset::west(4 * 3600)
1380 .ymd(2014, 10, 23)
1381 .and_hms_micro(20, 3, 41, 636000)
1382 .with_timezone(&Utc),
1383 update_time: FixedOffset::west(4 * 3600)
1384 .ymd(2014, 10, 23)
1385 .and_hms_micro(20, 3, 42, 890000)
1386 .with_timezone(&Utc),
1387 notes: None,
1388 primary_route: "AUTO".to_string(),
1389 secondary_route: None,
1390 order_route: "LAMP".to_string(),
1391 venue_holding_order: None,
1392 commission_charged: json!(0).to_number(),
1393 exchange_order_id: "XS173577870".to_string(),
1394 is_significant_shareholder: false,
1395 is_insider: false,
1396 is_limit_offset_in_dollars: false,
1397 user_id: 3000124,
1398 placement_commission: json!(0).to_number(),
1399 strategy_type: "SingleLeg".to_string(),
1400 trigger_stop_price: None,
1401 order_group_id: 0,
1402 order_class: None
1403 },
1404 AccountOrder {
1405 id: 173567569,
1406 symbol: "XSP".to_string(),
1407 symbol_id: 12873,
1408 total_quantity: json!(3).to_number(),
1409 open_quantity: json!(0).to_number(),
1410 filled_quantity: json!(0).to_number(),
1411 canceled_quantity: json!(0).to_number(),
1412 side: OrderSide::Buy,
1413 order_type: OrderType::Limit,
1414 limit_price: Some(json!(35.05).to_number()),
1415 stop_price: None,
1416 is_all_or_none: false,
1417 is_anonymous: false,
1418 iceberg_quantity: None,
1419 min_quantity: None,
1420 avg_execution_price: None,
1421 last_execution_price: None,
1422 source: "QuestradeIQEdge".to_string(),
1423 time_in_force: OrderTimeInForce::Day,
1424 good_till_date: None,
1425 state: OrderState::Replaced,
1426 rejection_reason: None,
1427 chain_id: 173567569,
1428 creation_time: FixedOffset::west(4 * 3600)
1429 .ymd(2015, 08, 12)
1430 .and_hms_micro(11, 2, 37, 86000)
1431 .with_timezone(&Utc),
1432 update_time: FixedOffset::west(4 * 3600)
1433 .ymd(2015, 08, 12)
1434 .and_hms_micro(11, 2, 41, 241000)
1435 .with_timezone(&Utc),
1436 notes: None,
1437 primary_route: "AUTO".to_string(),
1438 secondary_route: Some("AUTO".to_string()),
1439 order_route: "ITSR".to_string(),
1440 venue_holding_order: None,
1441 commission_charged: json!(0).to_number(),
1442 exchange_order_id: "XS173577869".to_string(),
1443 is_significant_shareholder: false,
1444 is_insider: false,
1445 is_limit_offset_in_dollars: false,
1446 user_id: 3000124,
1447 placement_commission: json!(0).to_number(),
1448 strategy_type: "SingleLeg".to_string(),
1449 trigger_stop_price: None,
1450 order_group_id: 0,
1451 order_class: None
1452 },
1453 AccountOrder {
1454 id: 173567570,
1455 symbol: "XSP".to_string(),
1456 symbol_id: 12873,
1457 total_quantity: json!(3).to_number(),
1458 open_quantity: json!(0).to_number(),
1459 filled_quantity: json!(3).to_number(),
1460 canceled_quantity: json!(0).to_number(),
1461 side: OrderSide::Buy,
1462 order_type: OrderType::Limit,
1463 limit_price: Some(json!(15.52).to_number()),
1464 stop_price: None,
1465 is_all_or_none: false,
1466 is_anonymous: false,
1467 iceberg_quantity: None,
1468 min_quantity: None,
1469 avg_execution_price: Some(json!(15.52).to_number()),
1470 last_execution_price: None,
1471 source: "QuestradeIQEdge".to_string(),
1472 time_in_force: OrderTimeInForce::Day,
1473 good_till_date: None,
1474 state: OrderState::Executed,
1475 rejection_reason: None,
1476 chain_id: 173567570,
1477 creation_time: FixedOffset::west(4 * 3600)
1478 .ymd(2015, 08, 12)
1479 .and_hms_micro(11, 3, 37, 86000)
1480 .with_timezone(&Utc),
1481 update_time: FixedOffset::west(4 * 3600)
1482 .ymd(2015, 08, 12)
1483 .and_hms_micro(11, 03, 41, 241000)
1484 .with_timezone(&Utc),
1485 notes: None,
1486 primary_route: "AUTO".to_string(),
1487 secondary_route: Some("AUTO".to_string()),
1488 order_route: "ITSR".to_string(),
1489 venue_holding_order: Some("ITSR".to_string()),
1490 commission_charged: json!(0.0105).to_number(),
1491 exchange_order_id: "XS173577870".to_string(),
1492 is_significant_shareholder: false,
1493 is_insider: false,
1494 is_limit_offset_in_dollars: false,
1495 user_id: 3000124,
1496 placement_commission: json!(0).to_number(),
1497 strategy_type: "SingleLeg".to_string(),
1498 trigger_stop_price: None,
1499 order_group_id: 0,
1500 order_class: None
1501 }
1502 ]
1503 );
1504
1505 Ok(())
1506 }
1507
1508 #[tokio::test]
1509 async fn account_order() -> Result<(), Box<dyn Error>> {
1510 let _m = mock("GET", "/v1/accounts/123456/orders/173577870")
1511 .with_status(200)
1512 .with_header("content-type", "text/json")
1513 .with_body(read_to_string(
1514 "test/response/account-order-173577870.json",
1515 )?)
1516 .create();
1517
1518 let result = get_api().account_order("123456", 173577870).await;
1519
1520 assert_eq!(
1521 result?,
1522 Some(AccountOrder {
1523 id: 173577870,
1524 symbol: "AAPL".to_string(),
1525 symbol_id: 8049,
1526 total_quantity: json!(100).to_number(),
1527 open_quantity: json!(100).to_number(),
1528 filled_quantity: json!(0).to_number(),
1529 canceled_quantity: json!(0).to_number(),
1530 side: OrderSide::Buy,
1531 order_type: OrderType::Limit,
1532 limit_price: Some(json!(500.95).to_number()),
1533 stop_price: None,
1534 is_all_or_none: false,
1535 is_anonymous: false,
1536 iceberg_quantity: None,
1537 min_quantity: None,
1538 avg_execution_price: None,
1539 last_execution_price: None,
1540 source: "TradingAPI".to_string(),
1541 time_in_force: OrderTimeInForce::Day,
1542 good_till_date: None,
1543 state: OrderState::Canceled,
1544 rejection_reason: None,
1545 chain_id: 173577870,
1546 creation_time: FixedOffset::west(4 * 3600)
1547 .ymd(2014, 10, 23)
1548 .and_hms_micro(20, 3, 41, 636000)
1549 .with_timezone(&Utc),
1550 update_time: FixedOffset::west(4 * 3600)
1551 .ymd(2014, 10, 23)
1552 .and_hms_micro(20, 3, 42, 890000)
1553 .with_timezone(&Utc),
1554 notes: None,
1555 primary_route: "AUTO".to_string(),
1556 secondary_route: None,
1557 order_route: "LAMP".to_string(),
1558 venue_holding_order: None,
1559 commission_charged: json!(0).to_number(),
1560 exchange_order_id: "XS173577870".to_string(),
1561 is_significant_shareholder: false,
1562 is_insider: false,
1563 is_limit_offset_in_dollars: false,
1564 user_id: 3000124,
1565 placement_commission: json!(0).to_number(),
1566 strategy_type: "SingleLeg".to_string(),
1567 trigger_stop_price: None,
1568 order_group_id: 0,
1569 order_class: None
1570 })
1571 );
1572
1573 Ok(())
1574 }
1575
1576 #[tokio::test]
1577 async fn account_order_empty() -> Result<(), Box<dyn Error>> {
1578 let _m = mock("GET", "/v1/accounts/123456/orders/123456")
1579 .with_status(200)
1580 .with_header("content-type", "text/json")
1581 .with_body(read_to_string("test/response/account-order-empty.json")?)
1582 .create();
1583
1584 let result = get_api().account_order("123456", 123456).await;
1585
1586 assert_eq!(result?, None);
1587
1588 Ok(())
1589 }
1590
1591 #[tokio::test]
1592 async fn account_executions() -> Result<(), Box<dyn Error>> {
1593 let _m = mock("GET", "/v1/accounts/26598145/executions")
1594 .with_status(200)
1595 .with_header("content-type", "text/json")
1596 .with_body(read_to_string("test/response/account-executions.json")?)
1597 .create();
1598
1599 let result = get_api().account_executions("26598145", None, None).await;
1600
1601 assert_eq!(
1602 result?,
1603 vec![
1604 AccountExecution {
1605 id: 53817310,
1606 order_id: 177106005,
1607 symbol: "AAPL".to_string(),
1608 symbol_id: 8049,
1609 quantity: json!(10).to_number(),
1610 side: OrderSide::Buy,
1611 price: json!(536.87).to_number(),
1612 order_chain_id: 17710600,
1613 timestamp: FixedOffset::west(4 * 3600)
1614 .ymd(2014, 03, 31)
1615 .and_hms(13, 38, 29)
1616 .with_timezone(&Utc),
1617 notes: None,
1618 commission: json!(4.95).to_number(),
1619 execution_fee: json!(0).to_number(),
1620 sec_fee: json!(0).to_number(),
1621 canadian_execution_fee: json!(0).to_number(),
1622 parent_id: 0
1623 },
1624 AccountExecution {
1625 id: 710654134,
1626 order_id: 700046545,
1627 symbol: "XSP.TO".to_string(),
1628 symbol_id: 23963,
1629 quantity: json!(3).to_number(),
1630 side: OrderSide::Buy,
1631 price: json!(36.52).to_number(),
1632 order_chain_id: 700065471,
1633 timestamp: FixedOffset::west(4 * 3600)
1634 .ymd(2015, 08, 19)
1635 .and_hms(11, 03, 41)
1636 .with_timezone(&Utc),
1637 notes: None,
1638 commission: json!(0).to_number(),
1639 execution_fee: json!(0.0105).to_number(),
1640 sec_fee: json!(0).to_number(),
1641 canadian_execution_fee: json!(0).to_number(),
1642 parent_id: 710651321
1643 }
1644 ]
1645 );
1646
1647 Ok(())
1648 }
1649
1650 #[tokio::test]
1651 async fn account_balance() -> Result<(), Box<dyn Error>> {
1652 let _m = mock("GET", "/v1/accounts/26598145/balances")
1653 .with_status(200)
1654 .with_header("content-type", "text/json")
1655 .with_body(read_to_string("test/response/account-balances.json")?)
1656 .create();
1657
1658 let result = get_api().account_balance("26598145").await;
1659
1660 assert_eq!(
1661 result?,
1662 AccountBalances {
1663 per_currency_balances: vec![
1664 AccountBalance {
1665 currency: Currency::CAD,
1666 cash: json!(322.7015).to_number(),
1667 market_value: json!(6239.64).to_number(),
1668 total_equity: json!(6562.3415).to_number(),
1669 buying_power: json!(15473.182995).to_number(),
1670 maintenance_excess: json!(4646.6015).to_number(),
1671 is_real_time: true
1672 },
1673 AccountBalance {
1674 currency: Currency::USD,
1675 cash: json!(0).to_number(),
1676 market_value: json!(0).to_number(),
1677 total_equity: json!(0).to_number(),
1678 buying_power: json!(0).to_number(),
1679 maintenance_excess: json!(0).to_number(),
1680 is_real_time: true
1681 }
1682 ],
1683 combined_balances: vec![
1684 AccountBalance {
1685 currency: Currency::CAD,
1686 cash: json!(322.7015).to_number(),
1687 market_value: json!(6239.64).to_number(),
1688 total_equity: json!(6562.3415).to_number(),
1689 buying_power: json!(15473.182995).to_number(),
1690 maintenance_excess: json!(4646.6015).to_number(),
1691 is_real_time: true
1692 },
1693 AccountBalance {
1694 currency: Currency::USD,
1695 cash: json!(242.541526).to_number(),
1696 market_value: json!(4689.695603).to_number(),
1697 total_equity: json!(4932.237129).to_number(),
1698 buying_power: json!(11629.600147).to_number(),
1699 maintenance_excess: json!(3492.372416).to_number(),
1700 is_real_time: true
1701 }
1702 ],
1703 sod_per_currency_balances: vec![
1704 AccountBalance {
1705 currency: Currency::CAD,
1706 cash: json!(322.7015).to_number(),
1707 market_value: json!(6177).to_number(),
1708 total_equity: json!(6499.7015).to_number(),
1709 buying_power: json!(15473.182995).to_number(),
1710 maintenance_excess: json!(4646.6015).to_number(),
1711 is_real_time: true
1712 },
1713 AccountBalance {
1714 currency: Currency::USD,
1715 cash: json!(0).to_number(),
1716 market_value: json!(0).to_number(),
1717 total_equity: json!(0).to_number(),
1718 buying_power: json!(0).to_number(),
1719 maintenance_excess: json!(0).to_number(),
1720 is_real_time: true
1721 }
1722 ],
1723 sod_combined_balances: vec![
1724 AccountBalance {
1725 currency: Currency::CAD,
1726 cash: json!(322.7015).to_number(),
1727 market_value: json!(6177).to_number(),
1728 total_equity: json!(6499.7015).to_number(),
1729 buying_power: json!(15473.182995).to_number(),
1730 maintenance_excess: json!(4646.6015).to_number(),
1731 is_real_time: true
1732 },
1733 AccountBalance {
1734 currency: Currency::USD,
1735 cash: json!(242.541526).to_number(),
1736 market_value: json!(4642.615558).to_number(),
1737 total_equity: json!(4885.157084).to_number(),
1738 buying_power: json!(11629.600147).to_number(),
1739 maintenance_excess: json!(3492.372416).to_number(),
1740 is_real_time: true
1741 }
1742 ]
1743 }
1744 );
1745
1746 Ok(())
1747 }
1748
1749 #[tokio::test]
1750 async fn account_positions() -> Result<(), Box<dyn Error>> {
1751 let _m = mock("GET", "/v1/accounts/26598145/positions")
1752 .with_status(200)
1753 .with_header("content-type", "text/json")
1754 .with_body(read_to_string("test/response/account-positions.json")?)
1755 .create();
1756
1757 let result = get_api().account_positions("26598145").await;
1758
1759 assert_eq!(
1760 result?,
1761 vec![
1762 AccountPosition {
1763 symbol: "THI.TO".to_string(),
1764 symbol_id: 38738,
1765 open_quantity: json!(100).to_number(),
1766 closed_quantity: json!(0).to_number(),
1767 current_market_value: json!(6017).to_number(),
1768 current_price: json!(60.17).to_number(),
1769 average_entry_price: json!(60.23).to_number(),
1770 closed_profit_and_loss: json!(0).to_number(),
1771 day_profit_and_loss: json!(0).to_number(),
1772 open_profit_and_loss: json!(-6).to_number(),
1773 total_cost: json!(6023).to_number(),
1774 is_real_time: true,
1775 is_under_reorg: false
1776 },
1777 AccountPosition {
1778 symbol: "XSP.TO".to_string(),
1779 symbol_id: 38738,
1780 open_quantity: json!(100).to_number(),
1781 closed_quantity: json!(0).to_number(),
1782 current_market_value: json!(3571).to_number(),
1783 current_price: json!(35.71).to_number(),
1784 average_entry_price: json!(32.831898).to_number(),
1785 closed_profit_and_loss: json!(0).to_number(),
1786 day_profit_and_loss: json!(0).to_number(),
1787 open_profit_and_loss: json!(500.789748).to_number(),
1788 total_cost: json!(3070.750252).to_number(),
1789 is_real_time: false,
1790 is_under_reorg: false
1791 },
1792 ]
1793 );
1794
1795 Ok(())
1796 }
1797
1798 #[tokio::test]
1802 async fn market_quote() -> Result<(), Box<dyn Error>> {
1803 let _m = mock("GET", "/v1/markets/quotes")
1804 .match_query(Matcher::UrlEncoded("ids".into(), "2434553,27725609".into()))
1805 .with_status(200)
1806 .with_header("content-type", "text/json")
1807 .with_body(read_to_string("test/response/market-quotes.json")?)
1808 .create();
1809
1810 let result = get_api().market_quote(&[2434553, 27725609]).await;
1811
1812 assert_eq!(
1813 result?,
1814 vec![
1815 MarketQuote {
1816 symbol: "XMU.TO".to_string(),
1817 symbol_id: 2434553,
1818 tier: None,
1819 bid_price: Some(json!(57.01).to_number()),
1820 bid_size: 24,
1821 ask_price: Some(json!(57.13).to_number()),
1822 ask_size: 33,
1823 last_trade_price_tr_hrs: json!(57.15).to_number(),
1824 last_trade_price: json!(57.15).to_number(),
1825 last_trade_size: 100,
1826 last_trade_tick: TickType::Up,
1827 volume: 2728,
1828 open_price: json!(55.76).to_number(),
1829 high_price: json!(57.15).to_number(),
1830 low_price: json!(55.76).to_number(),
1831 delay: false,
1832 is_halted: false
1833 },
1834 MarketQuote {
1835 symbol: "XMU.U.TO".to_string(),
1836 symbol_id: 27725609,
1837 tier: None,
1838 bid_price: Some(json!(42.65).to_number()),
1839 bid_size: 10,
1840 ask_price: Some(json!(42.79).to_number()),
1841 ask_size: 10,
1842 last_trade_price_tr_hrs: json!(44.22).to_number(),
1843 last_trade_price: json!(44.22).to_number(),
1844 last_trade_size: 0,
1845 last_trade_tick: TickType::Equal,
1846 volume: 0,
1847 open_price: json!(0).to_number(),
1848 high_price: json!(0).to_number(),
1849 low_price: json!(0).to_number(),
1850 delay: false,
1851 is_halted: false
1852 }
1853 ]
1854 );
1855
1856 Ok(())
1857 }
1858
1859 #[tokio::test]
1860 async fn symbol_search() -> Result<(), Box<dyn Error>> {
1861 let _m = mock("GET", "/v1/symbols/search?prefix=V&offset=0")
1862 .with_status(200)
1863 .with_header("content-type", "text/json")
1864 .with_body(read_to_string("test/response/symbol-search.json")?)
1865 .create();
1866
1867 let result = get_api().symbol_search("V", 0).await;
1868
1869 assert_eq!(
1870 result?,
1871 vec![
1872 SearchEquitySymbol {
1873 symbol: "V".into(),
1874 symbol_id: 40825,
1875 description: "VISA INC".into(),
1876 security_type: SecurityType::Stock,
1877 listing_exchange: ListingExchange::NYSE,
1878 is_quotable: true,
1879 is_tradable: true,
1880 currency: Currency::USD
1881 },
1882 SearchEquitySymbol {
1883 symbol: "VA.TO".into(),
1884 symbol_id: 11419773,
1885 description: "VANGUARD FTSE DEV ASIA PAC ALL CAP IDX".into(),
1886 security_type: SecurityType::Stock,
1887 listing_exchange: ListingExchange::TSX,
1888 is_quotable: true,
1889 is_tradable: true,
1890 currency: Currency::CAD
1891 },
1892 SearchEquitySymbol {
1893 symbol: "VABB".into(),
1894 symbol_id: 40790,
1895 description: "VIRGINIA BANK BANKSHARES INC".into(),
1896 security_type: SecurityType::Stock,
1897 listing_exchange: ListingExchange::PinkSheets,
1898 is_quotable: true,
1899 is_tradable: true,
1900 currency: Currency::USD
1901 },
1902 SearchEquitySymbol {
1903 symbol: "VAC".into(),
1904 symbol_id: 1261992,
1905 description: "MARRIOTT VACATIONS WORLDWIDE CORP".into(),
1906 security_type: SecurityType::Stock,
1907 listing_exchange: ListingExchange::NYSE,
1908 is_quotable: true,
1909 is_tradable: true,
1910 currency: Currency::USD
1911 },
1912 SearchEquitySymbol {
1913 symbol: "VACNY".into(),
1914 symbol_id: 20491473,
1915 description: "VAT GROUP AG".into(),
1916 security_type: SecurityType::Stock,
1917 listing_exchange: ListingExchange::PinkSheets,
1918 is_quotable: true,
1919 is_tradable: true,
1920 currency: Currency::USD
1921 },
1922 SearchEquitySymbol {
1923 symbol: "VACQU".into(),
1924 symbol_id: 32441174,
1925 description: "VECTOR ACQUISITION CORP UNITS(1 ORD A & 1/3 WT)30/09/2027".into(),
1926 security_type: SecurityType::Stock,
1927 listing_exchange: ListingExchange::NASDAQ,
1928 is_quotable: true,
1929 is_tradable: true,
1930 currency: Currency::USD
1931 },
1932 SearchEquitySymbol {
1933 symbol: "VAEEM.IN".into(),
1934 symbol_id: 1630037,
1935 description: "CBOE VXEEM Ask Index".into(),
1936 security_type: SecurityType::Index,
1937 listing_exchange: ListingExchange::SP,
1938 is_quotable: true,
1939 is_tradable: false,
1940 currency: Currency::USD
1941 }
1942 ]
1943 );
1944
1945 Ok(())
1946 }
1947
1948 }