questrade_rs/
lib.rs

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
22/// Version of the API.
23const API_VERSION: &str = "v1";
24
25/// Questrade client
26pub struct Questrade {
27    client: Client,
28    auth_info: RefCell<Option<AuthenticationInfo>>,
29}
30
31impl Questrade {
32    /// Creates a new API instance with the default client.
33    pub fn new() -> Self {
34        Self::with_client(Client::new())
35    }
36
37    /// Creates a new API instance with the specified client
38    pub fn with_client(client: Client) -> Self {
39        Questrade {
40            client,
41            auth_info: RefCell::new(None),
42        }
43    }
44
45    /// Creates a new API instance with the specified auth info.
46    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    //region authentication
54
55    /// Authenticates using the supplied token.
56    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    /// Retrieves the current authentication info (if set).
69    pub fn get_auth_info(&self) -> Option<AuthenticationInfo> {
70        self.auth_info.borrow().clone()
71    }
72
73    /// Obtains an active authentication token or raises an error
74    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    //endregion
82
83    //region accounts
84
85    /// List all accounts associated with the authenticated user.
86    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    /// Retrieve account activities, including cash transactions, dividends, trades, etc.
105    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    /// Search for account orders.
133    ///
134    /// Parameters:
135    ///     - `start_time` optional start of time range. Defaults to start of today, 12:00am
136    ///     - `end_time` optional end of time range. Defaults to end of today, 11:59pm
137    ///     - `state_filter` optionally filters order states
138    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    /// Retrieve details for an order with a specific id
183    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    /// Retrieves executions for a specific account.
208    ///
209    /// Parameters:
210    ///     - `start_time` optional start of time range. Defaults to start of today, 12:00am
211    ///     - `end_time` optional end of time range. Defaults to end of today, 11:59pm
212    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    /// Retrieves per-currency and combined balances for a specified account.
246    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    /// Retrieves positions in a specified account.
263    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    //endregion
285
286    //region markets
287
288    /// Retrieves a single Level 1 market data quote for one or more symbols.
289    ///
290    /// IMPORTANT NOTE: Questrade user needs to be subscribed to a real-time data package, to
291    /// receive market quotes in real-time, otherwise call to get quote is considered snap quote and
292    /// limit per market can be quickly reached. Without real-time data package, once limit is
293    /// reached, the response will return delayed data.
294    /// (Please check "delay" parameter in response always)
295    ///
296    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    //endregion
318
319    //region symbols
320
321    /// Searches for the specified symbol.
322    ///
323    /// params
324    /// * `prefix` Prefix of a symbol or any word in the description.
325    /// * `offset` Offset in number of records from the beginning of a result set.
326    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    //endregion
350
351    /// Retrieves current server time.
352    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    /// Get a request builder for a `get` request
371    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// region accounts
397
398/// Account record
399#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
400pub struct Account {
401    /// Type of the account. (Eg: Cash / Margin)
402    #[serde(rename = "type")]
403    pub account_type: AccountType,
404
405    /// Eight-digit account number (e.g., "26598145").
406    pub number: String,
407
408    /// Status of the account (e.g., Active)
409    pub status: AccountStatus,
410
411    /// Whether this is a primary account for the holder.
412    #[serde(rename = "isPrimary")]
413    pub is_primary: bool,
414
415    /// Whether this account is one that gets billed for various expenses such as inactivity fees, market data, etc.
416    #[serde(rename = "isBilling")]
417    pub is_billing: bool,
418
419    /// Type of client holding the account (e.g., "Individual").
420    #[serde(rename = "clientAccountType")]
421    pub client_account_type: ClientAccountType,
422}
423
424/// Type of account.
425#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
426pub enum AccountType {
427    /// Cash account.
428    Cash,
429
430    ///Margin account.
431    Margin,
432
433    ///Tax Free Savings Account.
434    TFSA,
435
436    ///Registered Retirement Savings Plan.
437    RRSP,
438
439    ///Spousal RRSP.
440    SRRSP,
441
442    ///Locked-In RRSP.
443    LRRSP,
444
445    ///Locked-In Retirement Account.
446    LIRA,
447
448    ///	Life Income Fund.
449    LIF,
450
451    ///Retirement Income Fund.
452    RIF,
453
454    ///Spousal RIF.
455    SRIF,
456
457    ///Locked-In RIF.
458    LRIF,
459
460    ///Registered RIF.
461    RRIF,
462
463    ///Prescribed RIF.
464    PRIF,
465
466    ///Individual Registered Education Savings Plan.
467    RESP,
468
469    ///Family RESP.
470    FRESP,
471}
472
473/// Status of an account.
474#[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/// Type of client this account is associated with.
491#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
492pub enum ClientAccountType {
493    ///Account held by an individual.
494    Individual,
495
496    ///Account held jointly by several individuals (e.g., spouses).
497    Joint,
498
499    /// Non-individual account held by an informal trust.
500    #[serde(rename = "Informal Trust")]
501    InformalTrust,
502
503    ///Non-individual account held by a corporation.
504    Corporation,
505
506    ///Non-individual account held by an investment club.
507    #[serde(rename = "Investment Club")]
508    InvestmentClub,
509
510    ///Non-individual account held by a formal trust.
511    #[serde(rename = "Formal Trust")]
512    FormalTrust,
513
514    /// Non-individual account held by a partnership.
515    Partnership,
516
517    /// Non-individual account held by a sole proprietorship.
518    #[serde(rename = "Sole Proprietorship")]
519    SoleProprietorship,
520
521    ///Account held by a family.
522    Family,
523
524    /// Non-individual account held by a joint and informal trust.
525    #[serde(rename = "Joint and Informal Trust")]
526    JointAndInformalTrust,
527
528    ///	Non-individual account held by an institution.
529    Institution,
530}
531
532/// An activity that occurred in an account
533#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
534pub struct AccountActivity {
535    /// Trade date.
536    #[serde(rename = "tradeDate")]
537    pub trade_date: DateTime<Utc>,
538
539    /// Date of the transaction.
540    #[serde(rename = "transactionDate")]
541    pub transaction_date: DateTime<Utc>,
542
543    /// Date the trade was settled.
544    #[serde(rename = "settlementDate")]
545    pub settlement_date: DateTime<Utc>,
546
547    /// Activity action.
548    pub action: String,
549
550    /// Symbol name.
551    pub symbol: String,
552
553    /// Internal unique symbol identifier.
554    #[serde(rename = "symbolId")]
555    pub symbol_id: SymbolId,
556
557    /// Textual description of the activity
558    pub description: String,
559
560    /// Activity currency (ISO format).
561    pub currency: String,
562
563    /// Number of items exchanged in the activity
564    pub quantity: Number,
565
566    /// Price of the items
567    pub price: Number,
568
569    /// Gross amount of the action, before fees
570    #[serde(rename = "grossAmount")]
571    pub gross_amount: Number,
572
573    /// Questrade commission amount
574    pub commission: Number,
575
576    /// Net amount of the action, after fees
577    #[serde(rename = "netAmount")]
578    pub net_amount: Number,
579
580    /// Type of activity.
581    #[serde(rename = "type")]
582    pub activity_type: String,
583}
584
585#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
586pub struct AccountOrder {
587    /// Internal order identifier.
588    pub id: OrderId,
589
590    /// Symbol that follows Questrade symbology (e.g., "TD.TO").
591    pub symbol: String,
592
593    /// Internal symbol identifier.
594    #[serde(rename = "symbolId")]
595    pub symbol_id: SymbolId,
596
597    /// Total quantity of the order.
598    #[serde(rename = "totalQuantity")]
599    pub total_quantity: Number,
600
601    /// Unfilled portion of the order quantity.
602    #[serde(rename = "openQuantity")]
603    #[serde(deserialize_with = "deserialize_nullable_number")]
604    pub open_quantity: Number,
605
606    /// Filled portion of the order quantity.
607    #[serde(rename = "filledQuantity")]
608    #[serde(deserialize_with = "deserialize_nullable_number")]
609    pub filled_quantity: Number,
610
611    /// Unfilled portion of the order quantity after cancellation.
612    #[serde(rename = "canceledQuantity")]
613    #[serde(deserialize_with = "deserialize_nullable_number")]
614    pub canceled_quantity: Number,
615
616    /// Client view of the order side (e.g., "Buy-To-Open").
617    pub side: OrderSide,
618
619    /// Order price type (e.g., "Market").
620    #[serde(rename = "orderType")]
621    #[serde(alias = "type")]
622    pub order_type: OrderType,
623
624    /// Limit price.
625    #[serde(rename = "limitPrice")]
626    pub limit_price: Option<Number>,
627
628    /// Stop price.
629    #[serde(rename = "stopPrice")]
630    pub stop_price: Option<Number>,
631
632    /// Specifies all-or-none special instruction.
633    #[serde(rename = "isAllOrNone")]
634    pub is_all_or_none: bool,
635
636    /// Specifies Anonymous special instruction.
637    #[serde(rename = "isAnonymous")]
638    pub is_anonymous: bool,
639
640    /// Specifies Iceberg special instruction.
641    #[serde(rename = "icebergQuantity")]
642    pub iceberg_quantity: Option<Number>,
643
644    /// Specifies Minimum special instruction.
645    #[serde(rename = "minQuantity")]
646    pub min_quantity: Option<Number>,
647
648    /// Average price of all executions received for this order.
649    #[serde(rename = "avgExecPrice")]
650    pub avg_execution_price: Option<Number>,
651
652    /// Price of the last execution received for the order in question.
653    #[serde(rename = "lastExecPrice")]
654    pub last_execution_price: Option<Number>,
655
656    /// Identifies the software / gateway where the order originated
657    pub source: String,
658
659    #[serde(rename = "timeInForce")]
660    pub time_in_force: OrderTimeInForce,
661
662    /// Good-Till-Date marker and date parameter
663    #[serde(rename = "gtdDate")]
664    pub good_till_date: Option<DateTime<Utc>>,
665
666    /// Current order state
667    pub state: OrderState,
668
669    /// Human readable order rejection reason message.
670    #[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    /// Internal identifier of a chain to which the order belongs.
676    #[serde(rename = "chainId")]
677    pub chain_id: OrderId,
678
679    /// Order creation time.
680    #[serde(rename = "creationTime")]
681    pub creation_time: DateTime<Utc>,
682
683    /// Time of the last update.
684    #[serde(rename = "updateTime")]
685    pub update_time: DateTime<Utc>,
686
687    /// Notes that may have been manually added by Questrade staff.
688    #[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    /// Order route name.
699    #[serde(rename = "orderRoute")]
700    pub order_route: String,
701
702    /// Venue where non-marketable portion of the order was booked.
703    #[serde(rename = "venueHoldingOrder")]
704    #[serde(deserialize_with = "serde_with::rust::string_empty_as_none::deserialize")]
705    pub venue_holding_order: Option<String>,
706
707    /// Total commission amount charged for this order.
708    #[serde(rename = "comissionCharged")]
709    #[serde(deserialize_with = "deserialize_nullable_number")]
710    pub commission_charged: Number,
711
712    /// Identifier assigned to this order by exchange where it was routed.
713    #[serde(rename = "exchangeOrderId")]
714    pub exchange_order_id: String,
715
716    /// Whether user that placed the order is a significant shareholder.
717    #[serde(rename = "isSignificantShareHolder")]
718    pub is_significant_shareholder: bool,
719
720    /// Whether user that placed the order is an insider.
721    #[serde(rename = "isInsider")]
722    pub is_insider: bool,
723
724    /// Whether limit offset is specified in dollars (vs. percent).
725    #[serde(rename = "isLimitOffsetInDollar")]
726    pub is_limit_offset_in_dollars: bool,
727
728    /// Internal identifier of user that placed the order.
729    #[serde(rename = "userId")]
730    pub user_id: UserId,
731
732    /// Commission for placing the order via the Trade Desk over the phone.
733    #[serde(rename = "placementCommission")]
734    #[serde(deserialize_with = "deserialize_nullable_number")]
735    pub placement_commission: Number,
736
737    // /// List of OrderLeg elements.
738    // TODO: legs,
739    /// Multi-leg strategy to which the order belongs.
740    #[serde(rename = "strategyType")]
741    pub strategy_type: String,
742
743    /// Stop price at which order was triggered.
744    #[serde(rename = "triggerStopPrice")]
745    pub trigger_stop_price: Option<Number>,
746
747    /// Internal identifier of the order group.
748    #[serde(rename = "orderGroupId")]
749    pub order_group_id: OrderId,
750
751    /// Bracket Order class. Primary, Profit or Loss.
752    #[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    /// Sell short
780    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/// An account execution.
853#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
854pub struct AccountExecution {
855    ///Internal identifier of the execution.
856    pub id: ExecutionId,
857
858    /// Internal identifier of the order to which the execution belongs.
859    #[serde(rename = "orderId")]
860    pub order_id: OrderId,
861
862    /// Symbol that follows Questrade symbology (e.g., "TD.TO").
863    pub symbol: String,
864
865    /// Internal symbol identifier.
866    #[serde(rename = "symbolId")]
867    pub symbol_id: SymbolId,
868
869    /// Execution quantity.
870    #[serde(rename = "quantity")]
871    pub quantity: Number,
872
873    /// Client view of the order side (e.g., "Buy-To-Open").
874    pub side: OrderSide,
875
876    /// Execution price.
877    pub price: Number,
878
879    /// Internal identifier of the order chain to which the execution belongs.
880    #[serde(rename = "orderChainId")]
881    pub order_chain_id: OrderId,
882
883    /// Execution timestamp.
884    pub timestamp: DateTime<Utc>,
885
886    /// Notes that may have been manually added by Questrade staff.
887    #[serde(deserialize_with = "serde_with::rust::string_empty_as_none::deserialize")]
888    pub notes: Option<String>,
889
890    /// Questrade commission.
891    pub commission: Number,
892
893    /// Liquidity fee charged by execution venue.
894    #[serde(rename = "executionFee")]
895    pub execution_fee: Number,
896
897    /// SEC fee charged on all sales of US securities.
898    #[serde(rename = "secFee")]
899    pub sec_fee: Number,
900
901    /// Additional execution fee charged by TSX (if applicable).
902    #[serde(rename = "canadianExecutionFee")]
903    pub canadian_execution_fee: Number,
904
905    /// Internal identifierof the parent order.
906    #[serde(rename = "parentId")]
907    pub parent_id: OrderId,
908}
909
910/// Account balance for specific currency.
911#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
912pub struct AccountBalance {
913    /// Currency of the balance figure.
914    pub currency: Currency,
915
916    /// Balance amount.
917    pub cash: Number,
918
919    /// Market value of all securities in the account in a given currency.
920    #[serde(rename = "marketValue")]
921    pub market_value: Number,
922
923    /// Equity as a difference between cash and marketValue properties.
924    #[serde(rename = "totalEquity")]
925    pub total_equity: Number,
926
927    /// Buying power for that particular currency side of the account.
928    #[serde(rename = "buyingPower")]
929    pub buying_power: Number,
930
931    /// Maintenance excess for that particular side of the account.
932    #[serde(rename = "maintenanceExcess")]
933    pub maintenance_excess: Number,
934
935    /// Whether real-time data was used to calculate the above balance.
936    #[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/// Account balances.
947#[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/// Account Position.
971#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
972pub struct AccountPosition {
973    /// Symbol that follows Questrade symbology (e.g., "TD.TO").
974    pub symbol: String,
975
976    /// Internal symbol identifier.
977    #[serde(rename = "symbolId")]
978    pub symbol_id: SymbolId,
979
980    /// Position quantity remaining open.
981    #[serde(rename = "openQuantity")]
982    pub open_quantity: Number,
983
984    /// Portion of the position that was closed today.
985    #[serde(rename = "closedQuantity")]
986    pub closed_quantity: Number,
987
988    /// Market value of the position (quantity x price).
989    #[serde(rename = "currentMarketValue")]
990    pub current_market_value: Number,
991
992    /// Current price of the position symbol.
993    #[serde(rename = "currentPrice")]
994    pub current_price: Number,
995
996    /// Current price of the position symbol.
997    #[serde(rename = "dayPnl")]
998    #[serde(deserialize_with = "none_is_zero")]
999    pub day_profit_and_loss: Number,
1000
1001    /// Average price paid for all executions constituting the position.
1002    #[serde(rename = "averageEntryPrice")]
1003    pub average_entry_price: Number,
1004
1005    /// Realized profit/loss on this position.
1006    #[serde(rename = "closedPnl")]
1007    pub closed_profit_and_loss: Number,
1008
1009    /// Unrealized profit/loss on this position.
1010    #[serde(rename = "openPnl")]
1011    pub open_profit_and_loss: Number,
1012
1013    /// Total cost of the position.
1014    #[serde(rename = "totalCost")]
1015    pub total_cost: Number,
1016
1017    /// Designates whether real-time quote was used to compute PnL.
1018    #[serde(rename = "isRealTime")]
1019    pub is_real_time: bool,
1020
1021    /// Designates whether a symbol is currently undergoing a reorg.
1022    #[serde(rename = "isUnderReorg")]
1023    pub is_under_reorg: bool,
1024}
1025
1026// endregion
1027
1028// region markets
1029
1030/// Spot quote for a certain Equity
1031#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
1032pub struct MarketQuote {
1033    /// Symbol name following Questrade’s symbology.
1034    pub symbol: String,
1035
1036    /// Internal symbol identifier.
1037    #[serde(rename = "symbolId")]
1038    pub symbol_id: SymbolId,
1039
1040    /// Market tier.
1041    #[serde(deserialize_with = "serde_with::rust::string_empty_as_none::deserialize")]
1042    pub tier: Option<String>, //FIXME - enumeration
1043
1044    /// Bid price.
1045    #[serde(rename = "bidPrice")]
1046    pub bid_price: Option<Number>,
1047
1048    /// Bid quantity.
1049    #[serde(rename = "bidSize")]
1050    pub bid_size: u32,
1051
1052    /// Ask price.
1053    #[serde(rename = "askPrice")]
1054    pub ask_price: Option<Number>,
1055
1056    /// Ask quantity.
1057    #[serde(rename = "askSize")]
1058    pub ask_size: u32,
1059
1060    /// Price of the last trade during regular trade hours.
1061    /// The closing price.
1062    #[serde(rename = "lastTradePriceTrHrs")]
1063    pub last_trade_price_tr_hrs: Number,
1064
1065    /// Price of the last trade.
1066    ///
1067    /// May include after-hours trading.
1068    #[serde(rename = "lastTradePrice")]
1069    pub last_trade_price: Number,
1070
1071    /// Quantity of the last trade.
1072    #[serde(rename = "lastTradeSize")]
1073    pub last_trade_size: u32,
1074
1075    /// Trade direction.
1076    #[serde(rename = "lastTradeTick")]
1077    pub last_trade_tick: TickType,
1078
1079    /// Daily trading volume
1080    pub volume: u32,
1081
1082    /// Opening trade price.
1083    #[serde(rename = "openPrice")]
1084    pub open_price: Number,
1085
1086    /// Daily high price.
1087    #[serde(rename = "highPrice")]
1088    pub high_price: Number,
1089
1090    /// Daily low price.
1091    #[serde(rename = "lowPrice")]
1092    pub low_price: Number,
1093
1094    /// Whether a quote is delayed or real-time.
1095    ///
1096    /// If `true` then the quote is delayed 15 minutes
1097    #[serde(deserialize_with = "deserialize_delay")]
1098    pub delay: bool,
1099
1100    /// Whether trading in the symbol is currently halted.
1101    #[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/// Equity details from a search query
1122#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
1123pub struct SearchEquitySymbol {
1124    /// Symbol name. (EG: BMO)
1125    pub symbol: String,
1126
1127    /// Internal unique symbol identifier.
1128    #[serde(rename = "symbolId")]
1129    pub symbol_id: SymbolId,
1130
1131    /// Symbol description.
1132    pub description: String,
1133
1134    /// Symbol security type.
1135    #[serde(rename = "securityType")]
1136    pub security_type: SecurityType,
1137
1138    /// Primary listing exchange of the symbol.
1139    #[serde(rename = "listingExchange")]
1140    pub listing_exchange: ListingExchange,
1141
1142    /// Whether a symbol has live market data.
1143    #[serde(rename = "isQuotable")]
1144    pub is_quotable: bool,
1145
1146    /// Whether a symbol is tradable on the platform.
1147    #[serde(rename = "isTradable")]
1148    pub is_tradable: bool,
1149
1150    /// Symbol currency.
1151    pub currency: Currency,
1152}
1153
1154/// Exchange where a security is listed
1155#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
1156pub enum ListingExchange {
1157    /// Toronto Stock Exchange.
1158    TSX,
1159
1160    /// Toronto Stock Exchange Index.
1161    TSXI,
1162
1163    /// Toronto Venture Exchange.
1164    TSXV,
1165
1166    /// Canadian National Stock Exchange.
1167    CNSX,
1168
1169    /// Montreal Exchange.
1170    MX,
1171
1172    /// NASDAQ.
1173    NASDAQ,
1174
1175    /// NASDAQ Index Feed.
1176    NASDAQI,
1177
1178    /// New York Stock Exchange.
1179    NYSE,
1180
1181    /// NYSE AMERICAN.
1182    NYSEAM,
1183
1184    /// NYSE Global Index Feed.
1185    NYSEGIF,
1186
1187    /// NYSE Arca.
1188    ARCA,
1189
1190    /// Option Reporting Authority.
1191    OPRA,
1192
1193    /// Pink Sheets.
1194    #[serde(rename = "PINX")]
1195    PinkSheets,
1196
1197    /// OTC Bulletin Board.
1198    OTCBB,
1199
1200    /// BATS Exchange
1201    BATS,
1202
1203    /// Dow Jones Industrial Average
1204    #[serde(rename = "DJI")]
1205    DowJonesAverage,
1206
1207    /// S&P 500
1208    #[serde(rename = "S&P")]
1209    SP,
1210
1211    /// NEO Exchange
1212    NEO,
1213
1214    /// Russell Indexes
1215    RUSSELL,
1216
1217    /// Absent exchange
1218    #[serde(rename = "")]
1219    None,
1220}
1221
1222/// Type of security
1223#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
1224pub enum SecurityType {
1225    /// Common and preferred equities, ETFs, ETNs, units, ADRs, etc.
1226    Stock,
1227
1228    /// Equity and index options.
1229    Option,
1230
1231    /// Debentures, notes, bonds, both corporate and government.
1232    Bond,
1233
1234    /// Equity or bond rights and warrants.
1235    Right,
1236
1237    /// Physical gold (coins, wafers, bars).
1238    Gold,
1239
1240    /// Canadian or US mutual funds.
1241    MutualFund,
1242
1243    /// Stock indices (e.g., Dow Jones).
1244    Index,
1245}
1246
1247/// Direction of trading.
1248#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
1249pub enum TickType {
1250    /// Designates an uptick.
1251    Up,
1252
1253    /// Designates an downtick.
1254    Down,
1255
1256    /// Designates a tick that took place at the same price as a previous one.
1257    Equal,
1258}
1259
1260// endregion
1261
1262#[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    // region account
1307    #[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    // endregion
1799
1800    // region market
1801    #[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    // endregion
1949}