use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use rust_decimal::serde::float_option as decimal_opt;
use serde::Deserialize;
use crate::client::SchwabClient;
use crate::error::Result;
use crate::macros::string_enum;
use crate::secrets::{AccountHash, AccountNumber};
#[derive(Debug)]
pub struct Accounts<'a> {
client: &'a SchwabClient,
}
impl<'a> Accounts<'a> {
pub(crate) fn new(client: &'a SchwabClient) -> Self {
Self { client }
}
pub async fn numbers(&self) -> Result<Vec<AccountNumberHash>> {
self.client
.trader_http()
.get_json("/accounts/accountNumbers")
.await
}
pub fn list(&self) -> ListAccountsBuilder<'a> {
ListAccountsBuilder {
client: self.client,
include_positions: false,
}
}
pub fn get<'b>(&self, account_hash: &'b AccountHash) -> GetAccountBuilder<'a, 'b> {
GetAccountBuilder {
client: self.client,
account_hash,
include_positions: false,
}
}
}
#[derive(Debug)]
#[must_use = "call .send() to execute the request"]
pub struct ListAccountsBuilder<'a> {
client: &'a SchwabClient,
include_positions: bool,
}
impl<'a> ListAccountsBuilder<'a> {
pub fn with_positions(mut self) -> Self {
self.include_positions = true;
self
}
pub async fn send(self) -> Result<Vec<Account>> {
let path = if self.include_positions {
"/accounts?fields=positions"
} else {
"/accounts"
};
self.client.trader_http().get_json(path).await
}
}
#[derive(Debug)]
#[must_use = "call .send() to execute the request"]
pub struct GetAccountBuilder<'a, 'b> {
client: &'a SchwabClient,
account_hash: &'b AccountHash,
include_positions: bool,
}
impl<'a, 'b> GetAccountBuilder<'a, 'b> {
pub fn with_positions(mut self) -> Self {
self.include_positions = true;
self
}
pub async fn send(self) -> Result<Account> {
let hash = self.account_hash.expose_secret();
let path = if self.include_positions {
format!("/accounts/{hash}?fields=positions")
} else {
format!("/accounts/{hash}")
};
self.client.trader_http().get_json(&path).await
}
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct AccountNumberHash {
#[serde(rename = "accountNumber")]
pub account_number: AccountNumber,
#[serde(rename = "hashValue")]
pub hash_value: AccountHash,
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct Account {
#[serde(rename = "securitiesAccount")]
pub securities_account: SecuritiesAccount,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum SecuritiesAccount {
Margin(MarginAccount),
Cash(CashAccount),
Unknown {
account_type: String,
raw: serde_json::Value,
},
}
impl<'de> Deserialize<'de> for SecuritiesAccount {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
let account_type = value
.get("type")
.and_then(|v| v.as_str())
.ok_or_else(|| serde::de::Error::missing_field("type"))?
.to_string();
match account_type.as_str() {
"MARGIN" => MarginAccount::deserialize(value)
.map(SecuritiesAccount::Margin)
.map_err(serde::de::Error::custom),
"CASH" => CashAccount::deserialize(value)
.map(SecuritiesAccount::Cash)
.map_err(serde::de::Error::custom),
_ => Ok(SecuritiesAccount::Unknown {
account_type,
raw: value,
}),
}
}
}
impl SecuritiesAccount {
pub fn account_type(&self) -> &str {
match self {
SecuritiesAccount::Margin(_) => "MARGIN",
SecuritiesAccount::Cash(_) => "CASH",
SecuritiesAccount::Unknown { account_type, .. } => account_type,
}
}
pub fn account_number(&self) -> Option<&AccountNumber> {
match self {
SecuritiesAccount::Margin(a) => Some(&a.account_number),
SecuritiesAccount::Cash(a) => Some(&a.account_number),
SecuritiesAccount::Unknown { .. } => None,
}
}
pub fn positions(&self) -> &[Position] {
match self {
SecuritiesAccount::Margin(a) => &a.positions,
SecuritiesAccount::Cash(a) => &a.positions,
SecuritiesAccount::Unknown { .. } => &[],
}
}
pub fn is_day_trader(&self) -> Option<bool> {
match self {
SecuritiesAccount::Margin(a) => Some(a.is_day_trader),
SecuritiesAccount::Cash(a) => Some(a.is_day_trader),
SecuritiesAccount::Unknown { .. } => None,
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct MarginAccount {
#[serde(rename = "accountNumber")]
pub account_number: AccountNumber,
#[serde(rename = "roundTrips", default)]
pub round_trips: i32,
#[serde(rename = "isDayTrader", default)]
pub is_day_trader: bool,
#[serde(rename = "isClosingOnlyRestricted", default)]
pub is_closing_only_restricted: bool,
#[serde(rename = "pfcbFlag", default)]
pub pfcb_flag: bool,
#[serde(default)]
pub positions: Vec<Position>,
#[serde(rename = "initialBalances", default)]
pub initial_balances: Option<MarginInitialBalance>,
#[serde(rename = "currentBalances", default)]
pub current_balances: Option<MarginBalance>,
#[serde(rename = "projectedBalances", default)]
pub projected_balances: Option<MarginBalance>,
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct CashAccount {
#[serde(rename = "accountNumber")]
pub account_number: AccountNumber,
#[serde(rename = "roundTrips", default)]
pub round_trips: i32,
#[serde(rename = "isDayTrader", default)]
pub is_day_trader: bool,
#[serde(rename = "isClosingOnlyRestricted", default)]
pub is_closing_only_restricted: bool,
#[serde(rename = "pfcbFlag", default)]
pub pfcb_flag: bool,
#[serde(default)]
pub positions: Vec<Position>,
#[serde(rename = "initialBalances", default)]
pub initial_balances: Option<CashInitialBalance>,
#[serde(rename = "currentBalances", default)]
pub current_balances: Option<CashBalance>,
#[serde(rename = "projectedBalances", default)]
pub projected_balances: Option<CashBalance>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct MarginInitialBalance {
#[serde(default, with = "decimal_opt", rename = "accruedInterest")]
pub accrued_interest: Option<Decimal>,
#[serde(
default,
with = "decimal_opt",
rename = "availableFundsNonMarginableTrade"
)]
pub available_funds_non_marginable_trade: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "bondValue")]
pub bond_value: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "buyingPower")]
pub buying_power: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "cashBalance")]
pub cash_balance: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "cashAvailableForTrading")]
pub cash_available_for_trading: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "cashReceipts")]
pub cash_receipts: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "dayTradingBuyingPower")]
pub day_trading_buying_power: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "dayTradingBuyingPowerCall")]
pub day_trading_buying_power_call: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "dayTradingEquityCall")]
pub day_trading_equity_call: Option<Decimal>,
#[serde(default, with = "decimal_opt")]
pub equity: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "equityPercentage")]
pub equity_percentage: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "liquidationValue")]
pub liquidation_value: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "longMarginValue")]
pub long_margin_value: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "longOptionMarketValue")]
pub long_option_market_value: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "longStockValue")]
pub long_stock_value: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "maintenanceCall")]
pub maintenance_call: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "maintenanceRequirement")]
pub maintenance_requirement: Option<Decimal>,
#[serde(default, with = "decimal_opt")]
pub margin: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "marginEquity")]
pub margin_equity: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "moneyMarketFund")]
pub money_market_fund: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "mutualFundValue")]
pub mutual_fund_value: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "regTCall")]
pub reg_t_call: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "shortMarginValue")]
pub short_margin_value: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "shortOptionMarketValue")]
pub short_option_market_value: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "shortStockValue")]
pub short_stock_value: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "totalCash")]
pub total_cash: Option<Decimal>,
#[serde(default, rename = "isInCall")]
pub is_in_call: Option<bool>,
#[serde(default, with = "decimal_opt", rename = "unsettledCash")]
pub unsettled_cash: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "pendingDeposits")]
pub pending_deposits: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "marginBalance")]
pub margin_balance: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "shortBalance")]
pub short_balance: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "accountValue")]
pub account_value: Option<Decimal>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct MarginBalance {
#[serde(default, with = "decimal_opt", rename = "availableFunds")]
pub available_funds: Option<Decimal>,
#[serde(
default,
with = "decimal_opt",
rename = "availableFundsNonMarginableTrade"
)]
pub available_funds_non_marginable_trade: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "buyingPower")]
pub buying_power: Option<Decimal>,
#[serde(
default,
with = "decimal_opt",
rename = "buyingPowerNonMarginableTrade"
)]
pub buying_power_non_marginable_trade: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "dayTradingBuyingPower")]
pub day_trading_buying_power: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "dayTradingBuyingPowerCall")]
pub day_trading_buying_power_call: Option<Decimal>,
#[serde(default, with = "decimal_opt")]
pub equity: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "equityPercentage")]
pub equity_percentage: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "longMarginValue")]
pub long_margin_value: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "maintenanceCall")]
pub maintenance_call: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "maintenanceRequirement")]
pub maintenance_requirement: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "marginBalance")]
pub margin_balance: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "regTCall")]
pub reg_t_call: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "shortBalance")]
pub short_balance: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "shortMarginValue")]
pub short_margin_value: Option<Decimal>,
#[serde(default, with = "decimal_opt")]
pub sma: Option<Decimal>,
#[serde(default, rename = "isInCall")]
pub is_in_call: Option<bool>,
#[serde(default, with = "decimal_opt", rename = "stockBuyingPower")]
pub stock_buying_power: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "optionBuyingPower")]
pub option_buying_power: Option<Decimal>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct CashInitialBalance {
#[serde(default, with = "decimal_opt", rename = "accruedInterest")]
pub accrued_interest: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "cashAvailableForTrading")]
pub cash_available_for_trading: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "cashAvailableForWithdrawal")]
pub cash_available_for_withdrawal: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "cashBalance")]
pub cash_balance: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "bondValue")]
pub bond_value: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "cashReceipts")]
pub cash_receipts: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "liquidationValue")]
pub liquidation_value: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "longOptionMarketValue")]
pub long_option_market_value: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "longStockValue")]
pub long_stock_value: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "moneyMarketFund")]
pub money_market_fund: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "mutualFundValue")]
pub mutual_fund_value: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "shortOptionMarketValue")]
pub short_option_market_value: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "shortStockValue")]
pub short_stock_value: Option<Decimal>,
#[serde(default, rename = "isInCall")]
pub is_in_call: Option<bool>,
#[serde(default, with = "decimal_opt", rename = "unsettledCash")]
pub unsettled_cash: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "cashDebitCallValue")]
pub cash_debit_call_value: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "pendingDeposits")]
pub pending_deposits: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "accountValue")]
pub account_value: Option<Decimal>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct CashBalance {
#[serde(default, with = "decimal_opt", rename = "cashAvailableForTrading")]
pub cash_available_for_trading: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "cashAvailableForWithdrawal")]
pub cash_available_for_withdrawal: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "cashCall")]
pub cash_call: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "longNonMarginableMarketValue")]
pub long_non_marginable_market_value: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "totalCash")]
pub total_cash: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "cashDebitCallValue")]
pub cash_debit_call_value: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "unsettledCash")]
pub unsettled_cash: Option<Decimal>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Position {
#[serde(default, with = "decimal_opt", rename = "shortQuantity")]
pub short_quantity: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "averagePrice")]
pub average_price: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "currentDayProfitLoss")]
pub current_day_profit_loss: Option<Decimal>,
#[serde(
default,
with = "decimal_opt",
rename = "currentDayProfitLossPercentage"
)]
pub current_day_profit_loss_percentage: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "longQuantity")]
pub long_quantity: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "settledLongQuantity")]
pub settled_long_quantity: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "settledShortQuantity")]
pub settled_short_quantity: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "agedQuantity")]
pub aged_quantity: Option<Decimal>,
#[serde(default)]
pub instrument: Option<AccountsInstrument>,
#[serde(default, with = "decimal_opt", rename = "marketValue")]
pub market_value: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "maintenanceRequirement")]
pub maintenance_requirement: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "averageLongPrice")]
pub average_long_price: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "averageShortPrice")]
pub average_short_price: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "taxLotAverageLongPrice")]
pub tax_lot_average_long_price: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "taxLotAverageShortPrice")]
pub tax_lot_average_short_price: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "longOpenProfitLoss")]
pub long_open_profit_loss: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "shortOpenProfitLoss")]
pub short_open_profit_loss: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "previousSessionLongQuantity")]
pub previous_session_long_quantity: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "previousSessionShortQuantity")]
pub previous_session_short_quantity: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "currentDayCost")]
pub current_day_cost: Option<Decimal>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct AccountsInstrument {
#[serde(rename = "assetType")]
pub asset_type: AssetType,
#[serde(default)]
pub cusip: Option<String>,
#[serde(default)]
pub symbol: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default, rename = "instrumentId")]
pub instrument_id: Option<i64>,
#[serde(default, with = "decimal_opt", rename = "netChange")]
pub net_change: Option<Decimal>,
#[serde(default, rename = "optionDeliverables")]
pub option_deliverables: Vec<AccountApiOptionDeliverable>,
#[serde(default, rename = "putCall")]
pub put_call: Option<PutCall>,
#[serde(default, rename = "optionMultiplier")]
pub option_multiplier: Option<i32>,
#[serde(default, rename = "type")]
pub option_type: Option<OptionType>,
#[serde(default, rename = "underlyingSymbol")]
pub underlying_symbol: Option<String>,
#[serde(default, rename = "maturityDate")]
pub maturity_date: Option<DateTime<Utc>>,
#[serde(default, with = "decimal_opt")]
pub factor: Option<Decimal>,
#[serde(default, with = "decimal_opt", rename = "variableRate")]
pub variable_rate: Option<Decimal>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct AccountApiOptionDeliverable {
#[serde(default)]
pub symbol: Option<String>,
#[serde(default, with = "decimal_opt", rename = "deliverableUnits")]
pub deliverable_units: Option<Decimal>,
#[serde(default, rename = "apiCurrencyType")]
pub currency_type: Option<ApiCurrencyType>,
#[serde(default, rename = "assetType")]
pub asset_type: Option<AssetType>,
}
string_enum! {
AssetType {
Equity = "EQUITY",
MutualFund = "MUTUAL_FUND",
Option = "OPTION",
Future = "FUTURE",
Forex = "FOREX",
Index = "INDEX",
CashEquivalent = "CASH_EQUIVALENT",
FixedIncome = "FIXED_INCOME",
Product = "PRODUCT",
Currency = "CURRENCY",
CollectiveInvestment = "COLLECTIVE_INVESTMENT",
}
}
impl Default for AssetType {
fn default() -> Self {
AssetType::Unknown(String::new())
}
}
string_enum! {
PutCall {
Put = "PUT",
Call = "CALL",
UnknownSchwab = "UNKNOWN",
}
}
string_enum! {
OptionType {
Vanilla = "VANILLA",
Binary = "BINARY",
Barrier = "BARRIER",
UnknownSchwab = "UNKNOWN",
}
}
string_enum! {
ApiCurrencyType {
Usd = "USD",
Cad = "CAD",
Eur = "EUR",
Jpy = "JPY",
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn account_number_hash_parses() {
let json = r#"{"accountNumber":"12345678","hashValue":"ABCDEF1234567890"}"#;
let parsed: AccountNumberHash = serde_json::from_str(json).unwrap();
assert_eq!(parsed.account_number.expose_secret(), "12345678");
assert_eq!(parsed.hash_value.expose_secret(), "ABCDEF1234567890");
}
#[test]
fn margin_account_parses() {
let json = r#"{
"securitiesAccount": {
"type": "MARGIN",
"accountNumber": "12345678",
"roundTrips": 0,
"isDayTrader": false,
"isClosingOnlyRestricted": false,
"pfcbFlag": false,
"currentBalances": {
"availableFunds": 1000.50,
"buyingPower": 2000.00,
"equity": 5000.25
}
}
}"#;
let parsed: Account = serde_json::from_str(json).unwrap();
let margin = match &parsed.securities_account {
SecuritiesAccount::Margin(m) => m,
other => panic!("expected MARGIN, got {other:?}"),
};
assert_eq!(margin.account_number.expose_secret(), "12345678");
let balances = margin.current_balances.as_ref().unwrap();
assert_eq!(balances.available_funds, Some(dec!(1000.50)));
assert_eq!(balances.equity, Some(dec!(5000.25)));
assert_eq!(balances.buying_power_non_marginable_trade, None);
}
#[test]
fn cash_account_parses() {
let json = r#"{
"securitiesAccount": {
"type": "CASH",
"accountNumber": "87654321",
"roundTrips": 0,
"isDayTrader": false,
"isClosingOnlyRestricted": false,
"pfcbFlag": false,
"currentBalances": {
"cashAvailableForTrading": 500.00,
"totalCash": 500.00
}
}
}"#;
let parsed: Account = serde_json::from_str(json).unwrap();
let cash = match &parsed.securities_account {
SecuritiesAccount::Cash(c) => c,
other => panic!("expected CASH, got {other:?}"),
};
assert_eq!(cash.account_number.expose_secret(), "87654321");
let balances = cash.current_balances.as_ref().unwrap();
assert_eq!(balances.cash_available_for_trading, Some(dec!(500.00)));
}
#[test]
fn unknown_account_type_parses_into_unknown_variant() {
let json = r#"{
"securitiesAccount": {
"type": "FUTURES",
"accountNumber": "99999999",
"extraField": 42
}
}"#;
let parsed: Account = serde_json::from_str(json).unwrap();
match &parsed.securities_account {
SecuritiesAccount::Unknown { account_type, raw } => {
assert_eq!(account_type, "FUTURES");
assert_eq!(
raw.get("accountNumber").and_then(|v| v.as_str()),
Some("99999999")
);
assert_eq!(raw.get("extraField").and_then(|v| v.as_i64()), Some(42));
}
other => panic!("expected Unknown, got {other:?}"),
}
assert_eq!(parsed.securities_account.account_type(), "FUTURES");
assert!(parsed.securities_account.account_number().is_none());
assert!(parsed.securities_account.positions().is_empty());
assert!(parsed.securities_account.is_day_trader().is_none());
}
#[test]
fn missing_account_type_is_a_parse_error() {
let json = r#"{
"securitiesAccount": {
"accountNumber": "99999999"
}
}"#;
let err = serde_json::from_str::<Account>(json).unwrap_err();
assert!(err.to_string().contains("type"), "got: {err}");
}
#[test]
fn account_with_equity_position() {
let json = r#"{
"securitiesAccount": {
"type": "MARGIN",
"accountNumber": "11111111",
"positions": [{
"longQuantity": 10,
"averagePrice": 145.32,
"marketValue": 1500.00,
"instrument": {
"assetType": "EQUITY",
"symbol": "AAPL",
"cusip": "037833100",
"description": "Apple Inc",
"instrumentId": 12345
}
}]
}
}"#;
let parsed: Account = serde_json::from_str(json).unwrap();
let positions = parsed.securities_account.positions();
assert_eq!(positions.len(), 1);
let pos = &positions[0];
assert_eq!(pos.long_quantity, Some(dec!(10)));
assert_eq!(pos.average_price, Some(dec!(145.32)));
let inst = pos.instrument.as_ref().unwrap();
assert_eq!(inst.asset_type, AssetType::Equity);
assert_eq!(inst.symbol.as_deref(), Some("AAPL"));
assert_eq!(inst.instrument_id, Some(12345));
}
#[test]
fn account_with_option_position() {
let json = r#"{
"securitiesAccount": {
"type": "MARGIN",
"accountNumber": "11111111",
"positions": [{
"longQuantity": 1,
"averagePrice": 6.45,
"instrument": {
"assetType": "OPTION",
"symbol": "AAPL 240315C00200000",
"underlyingSymbol": "AAPL",
"putCall": "CALL",
"type": "VANILLA",
"optionMultiplier": 100,
"optionDeliverables": [{
"symbol": "AAPL",
"deliverableUnits": 100,
"apiCurrencyType": "USD",
"assetType": "EQUITY"
}]
}
}]
}
}"#;
let parsed: Account = serde_json::from_str(json).unwrap();
let pos = &parsed.securities_account.positions()[0];
let inst = pos.instrument.as_ref().unwrap();
assert_eq!(inst.asset_type, AssetType::Option);
assert_eq!(inst.put_call, Some(PutCall::Call));
assert_eq!(inst.option_type, Some(OptionType::Vanilla));
assert_eq!(inst.option_multiplier, Some(100));
assert_eq!(inst.option_deliverables.len(), 1);
let deliv = &inst.option_deliverables[0];
assert_eq!(deliv.currency_type, Some(ApiCurrencyType::Usd));
assert_eq!(deliv.deliverable_units, Some(dec!(100)));
}
#[test]
fn unknown_asset_type_preserves_raw_string() {
let json =
r#"{"assetType":"NEW_ASSET_KIND","symbol":"WHAT","description":"Tomorrows thing"}"#;
let inst: AccountsInstrument = serde_json::from_str(json).unwrap();
match &inst.asset_type {
AssetType::Unknown(raw) => assert_eq!(raw, "NEW_ASSET_KIND"),
other => panic!("expected Unknown, got {other:?}"),
}
let just_asset: AssetType = serde_json::from_str(r#""NEW_ASSET_KIND""#).unwrap();
assert_eq!(
serde_json::to_string(&just_asset).unwrap(),
r#""NEW_ASSET_KIND""#
);
}
#[test]
fn empty_positions_field_omitted() {
let json = r#"{
"securitiesAccount": {
"type": "MARGIN",
"accountNumber": "12345"
}
}"#;
let parsed: Account = serde_json::from_str(json).unwrap();
assert!(parsed.securities_account.positions().is_empty());
}
#[test]
fn asset_type_round_trips_each_known_variant() {
for raw in [
"EQUITY",
"MUTUAL_FUND",
"OPTION",
"FUTURE",
"FOREX",
"INDEX",
"CASH_EQUIVALENT",
"FIXED_INCOME",
"PRODUCT",
"CURRENCY",
"COLLECTIVE_INVESTMENT",
] {
let json = format!(r#""{raw}""#);
let parsed: AssetType = serde_json::from_str(&json).unwrap();
let serialized = serde_json::to_string(&parsed).unwrap();
assert_eq!(serialized, json, "round trip failed for {raw}");
}
}
}