use reqwest::Method;
use serde::Serialize;
use tracing::instrument;
use crate::client::ApiBase;
use crate::models::trader::{
Account, AccountNumberHash, Order, PreviewOrder, Transaction, UserPreference,
};
use crate::query::{push_optional, required_text};
use crate::{Client, Error, OrderListOptions, OrderResponse, Result, TransactionListOptions};
impl Client {
#[instrument(skip_all)]
pub async fn get_accounts(&self, fields: Option<&str>) -> Result<Vec<Account>> {
let url = self.endpoint_url(ApiBase::Trader, &["accounts"])?;
let mut query = Vec::new();
push_optional(&mut query, "fields", fields);
self.send_json(Method::GET, url, &query, None).await
}
#[instrument(skip_all)]
pub async fn get_account_numbers(&self) -> Result<Vec<AccountNumberHash>> {
let url = self.endpoint_url(ApiBase::Trader, &["accounts", "accountNumbers"])?;
self.send_json(Method::GET, url, &[], None).await
}
#[instrument(skip_all)]
pub async fn get_account(
&self,
account_number: impl AsRef<str>,
fields: Option<&str>,
) -> Result<Account> {
let account_number = required_text("accountNumber", account_number.as_ref())?;
let url = self.endpoint_url(ApiBase::Trader, &["accounts", &account_number])?;
let mut query = Vec::new();
push_optional(&mut query, "fields", fields);
self.send_json(Method::GET, url, &query, None).await
}
#[instrument(skip_all)]
pub async fn get_orders(
&self,
account_number: impl AsRef<str>,
options: OrderListOptions,
) -> Result<Vec<Order>> {
let account_number = required_text("accountNumber", account_number.as_ref())?;
let url = self.endpoint_url(ApiBase::Trader, &["accounts", &account_number, "orders"])?;
self.send_json(Method::GET, url, &options.into_query()?, None)
.await
}
#[instrument(skip_all)]
pub async fn place_order<B>(
&self,
account_number: impl AsRef<str>,
order: &B,
) -> Result<OrderResponse>
where
B: Serialize + ?Sized,
{
let account_number = required_text("accountNumber", account_number.as_ref())?;
let url = self.endpoint_url(ApiBase::Trader, &["accounts", &account_number, "orders"])?;
self.send_empty_with_location(Method::POST, url, order)
.await
}
#[instrument(skip_all)]
pub async fn cancel_order(&self, account_number: impl AsRef<str>, order_id: i64) -> Result<()> {
let account_number = required_text("accountNumber", account_number.as_ref())?;
let order_id = order_id.to_string();
let url = self.endpoint_url(
ApiBase::Trader,
&["accounts", &account_number, "orders", &order_id],
)?;
self.send_empty(Method::DELETE, url).await
}
#[instrument(skip_all)]
pub async fn get_order(&self, account_number: impl AsRef<str>, order_id: i64) -> Result<Order> {
let account_number = required_text("accountNumber", account_number.as_ref())?;
let order_id = order_id.to_string();
let url = self.endpoint_url(
ApiBase::Trader,
&["accounts", &account_number, "orders", &order_id],
)?;
self.send_json(Method::GET, url, &[], None).await
}
#[instrument(skip_all)]
pub async fn replace_order<B>(
&self,
account_number: impl AsRef<str>,
order_id: i64,
order: &B,
) -> Result<OrderResponse>
where
B: Serialize + ?Sized,
{
let account_number = required_text("accountNumber", account_number.as_ref())?;
let order_id_text = order_id.to_string();
let url = self.endpoint_url(
ApiBase::Trader,
&["accounts", &account_number, "orders", &order_id_text],
)?;
let mut response = self
.send_empty_with_location(Method::PUT, url, order)
.await?;
if response.order_id.is_none() {
response.order_id = Some(order_id);
}
Ok(response)
}
#[instrument(skip_all)]
pub async fn preview_order<B>(
&self,
account_number: impl AsRef<str>,
order: &B,
) -> Result<PreviewOrder>
where
B: Serialize + ?Sized,
{
let account_number = required_text("accountNumber", account_number.as_ref())?;
let url = self.endpoint_url(
ApiBase::Trader,
&["accounts", &account_number, "previewOrder"],
)?;
self.send_json(
Method::POST,
url,
&[],
Some(serde_json::to_value(order).map_err(Error::Encode)?),
)
.await
}
#[instrument(skip_all)]
pub async fn get_transactions(
&self,
account_number: impl AsRef<str>,
options: TransactionListOptions,
) -> Result<Vec<Transaction>> {
let account_number = required_text("accountNumber", account_number.as_ref())?;
let url = self.endpoint_url(
ApiBase::Trader,
&["accounts", &account_number, "transactions"],
)?;
self.send_json(Method::GET, url, &options.into_query()?, None)
.await
}
#[instrument(skip_all)]
pub async fn get_transaction_by_id(
&self,
account_number: impl AsRef<str>,
transaction_id: i64,
) -> Result<Vec<Transaction>> {
let account_number = required_text("accountNumber", account_number.as_ref())?;
let transaction_id = transaction_id.to_string();
let url = self.endpoint_url(
ApiBase::Trader,
&["accounts", &account_number, "transactions", &transaction_id],
)?;
self.send_json(Method::GET, url, &[], None).await
}
#[instrument(skip_all)]
pub async fn get_all_orders(&self, options: OrderListOptions) -> Result<Vec<Order>> {
let url = self.endpoint_url(ApiBase::Trader, &["orders"])?;
self.send_json(Method::GET, url, &options.into_query()?, None)
.await
}
#[instrument(skip_all)]
pub async fn get_user_preference(&self) -> Result<Vec<UserPreference>> {
let url = self.endpoint_url(ApiBase::Trader, &["userPreference"])?;
self.send_json(Method::GET, url, &[], None).await
}
}
#[cfg(test)]
mod tests {
use mockito::Matcher;
use serde_json::json;
use crate::*;
#[tokio::test]
async fn get_account_uses_trader_base_url_path_escaping_and_bearer_auth() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/accounts/HASH%2FABC")
.match_query(Matcher::UrlEncoded("fields".into(), "positions".into()))
.match_header("authorization", "Bearer TOKEN")
.with_status(200)
.with_body(r#"{"securitiesAccount":{"type":"CASH"}}"#)
.create_async()
.await;
let url = server.url();
let config = Config::new()
.trader_base_url(&url)
.unwrap()
.bearer_token("TOKEN");
let client = Client::new(config);
client
.get_account("HASH/ABC", Some("positions"))
.await
.unwrap();
mock.assert_async().await;
}
#[tokio::test]
async fn place_order_sends_json_body_and_parses_location() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("POST", "/accounts/HASH/orders")
.match_body(Matcher::Json(json!({"orderType":"MARKET"})))
.with_status(201)
.with_header("Location", "/trader/v1/accounts/HASH/orders/9001")
.with_body("")
.create_async()
.await;
let url = server.url();
let client = Client::new(Config::new().trader_base_url(&url).unwrap());
let response = client
.place_order("HASH", &json!({"orderType":"MARKET"}))
.await
.unwrap();
mock.assert_async().await;
assert_eq!(response.order_id, Some(9001));
}
#[tokio::test]
async fn endpoint_request_shapes_are_covered() {
let mut server = mockito::Server::new_async().await;
let url = server.url();
let client = Client::new(Config::new().trader_base_url(&url).unwrap());
let m = server
.mock("GET", "/accounts")
.match_query(Matcher::UrlEncoded("fields".into(), "positions".into()))
.with_status(200)
.with_body("[]")
.create_async()
.await;
client.get_accounts(Some("positions")).await.unwrap();
m.assert_async().await;
let m = server
.mock("GET", "/accounts/accountNumbers")
.with_status(200)
.with_body("[]")
.create_async()
.await;
client.get_account_numbers().await.unwrap();
m.assert_async().await;
let m = server
.mock("GET", "/accounts/HASH/orders")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("fromEnteredTime".into(), "2024-01-01T00:00:00Z".into()),
Matcher::UrlEncoded("toEnteredTime".into(), "2024-01-31T00:00:00Z".into()),
Matcher::UrlEncoded("maxResults".into(), "10".into()),
Matcher::UrlEncoded("status".into(), "FILLED".into()),
]))
.with_status(200)
.with_body("[]")
.create_async()
.await;
client
.get_orders(
"HASH",
OrderListOptions::new("2024-01-01T00:00:00Z", "2024-01-31T00:00:00Z")
.max_results(10)
.status("FILLED"),
)
.await
.unwrap();
m.assert_async().await;
let m = server
.mock("DELETE", "/accounts/HASH/orders/9001")
.with_status(204)
.with_body("")
.create_async()
.await;
client.cancel_order("HASH", 9001).await.unwrap();
m.assert_async().await;
let m = server
.mock("GET", "/accounts/HASH/orders/9001")
.with_status(200)
.with_body(r#"{"orderId":9001}"#)
.create_async()
.await;
client.get_order("HASH", 9001).await.unwrap();
m.assert_async().await;
let m = server
.mock("PUT", "/accounts/HASH/orders/9001")
.match_body(Matcher::Json(json!({"orderType":"LIMIT"})))
.with_status(201)
.with_body("")
.create_async()
.await;
let response = client
.replace_order("HASH", 9001, &json!({"orderType":"LIMIT"}))
.await
.unwrap();
m.assert_async().await;
assert_eq!(response.order_id, Some(9001));
let m = server
.mock("POST", "/accounts/HASH/previewOrder")
.match_body(Matcher::Json(json!({"orderType":"MARKET"})))
.with_status(200)
.with_body(r#"{"previewId":"abc"}"#)
.create_async()
.await;
client
.preview_order("HASH", &json!({"orderType":"MARKET"}))
.await
.unwrap();
m.assert_async().await;
let m = server
.mock("GET", "/accounts/HASH/transactions")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("startDate".into(), "2024-01-01T00:00:00Z".into()),
Matcher::UrlEncoded("endDate".into(), "2024-01-31T00:00:00Z".into()),
Matcher::UrlEncoded("types".into(), "TRADE".into()),
Matcher::UrlEncoded("symbol".into(), "AAPL".into()),
]))
.with_status(200)
.with_body("[]")
.create_async()
.await;
client
.get_transactions(
"HASH",
TransactionListOptions::new(
"2024-01-01T00:00:00Z",
"2024-01-31T00:00:00Z",
"TRADE",
)
.symbol("AAPL"),
)
.await
.unwrap();
m.assert_async().await;
let m = server
.mock("GET", "/accounts/HASH/transactions/123")
.with_status(200)
.with_body(r#"[{"transactionId":123}]"#)
.create_async()
.await;
client.get_transaction_by_id("HASH", 123).await.unwrap();
m.assert_async().await;
let m = server
.mock("GET", "/orders")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("fromEnteredTime".into(), "2024-01-01T00:00:00Z".into()),
Matcher::UrlEncoded("toEnteredTime".into(), "2024-01-31T00:00:00Z".into()),
]))
.with_status(200)
.with_body("[]")
.create_async()
.await;
client
.get_all_orders(OrderListOptions::new(
"2024-01-01T00:00:00Z",
"2024-01-31T00:00:00Z",
))
.await
.unwrap();
m.assert_async().await;
let m = server
.mock("GET", "/userPreference")
.with_status(200)
.with_body(r#"[{"streamerInfo":[]}]"#)
.create_async()
.await;
client.get_user_preference().await.unwrap();
m.assert_async().await;
}
}