use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Deserializer, Serialize, de};
use std::collections::HashMap;
use uuid::Uuid;
use crate::trading::enums::{
AccountStatus, ActivityType, AssetClass, AssetExchange, AssetStatus, CorporateActionSubType,
CorporateActionType, DTBPCheck, ExerciseStyle, NonTradeActivityStatus, OrderClass, OrderSide,
OrderStatus, OrderType, PDTCheck, PositionIntent, PositionSide, TimeInForce, TradeActivityType,
TradeConfirmationEmail, TradeEvent,
};
use crate::types::ContractType;
use crate::types::serde_util::empty_string_as_none;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Asset {
pub id: Uuid,
#[serde(rename = "class")]
pub asset_class: AssetClass,
pub exchange: AssetExchange,
pub symbol: String,
#[serde(default)]
pub name: Option<String>,
pub status: AssetStatus,
pub tradable: bool,
pub marginable: bool,
pub shortable: bool,
pub easy_to_borrow: bool,
pub fractionable: bool,
#[serde(default, with = "crate::types::option_decimal")]
pub min_order_size: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub min_trade_increment: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub price_increment: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub maintenance_margin_requirement: Option<Decimal>,
#[serde(default)]
pub attributes: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UsdPositionValues {
#[serde(with = "crate::types::decimal")]
pub avg_entry_price: Decimal,
#[serde(with = "crate::types::decimal")]
pub market_value: Decimal,
#[serde(with = "crate::types::decimal")]
pub cost_basis: Decimal,
#[serde(with = "crate::types::decimal")]
pub unrealized_pl: Decimal,
#[serde(with = "crate::types::decimal")]
pub unrealized_plpc: Decimal,
#[serde(with = "crate::types::decimal")]
pub unrealized_intraday_pl: Decimal,
#[serde(with = "crate::types::decimal")]
pub unrealized_intraday_plpc: Decimal,
#[serde(with = "crate::types::decimal")]
pub current_price: Decimal,
#[serde(with = "crate::types::decimal")]
pub lastday_price: Decimal,
#[serde(with = "crate::types::decimal")]
pub change_today: Decimal,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Position {
pub asset_id: Uuid,
pub symbol: String,
pub exchange: AssetExchange,
pub asset_class: AssetClass,
#[serde(default)]
pub asset_marginable: Option<bool>,
#[serde(with = "crate::types::decimal")]
pub avg_entry_price: Decimal,
#[serde(with = "crate::types::decimal")]
pub qty: Decimal,
pub side: PositionSide,
#[serde(default, with = "crate::types::option_decimal")]
pub market_value: Option<Decimal>,
#[serde(with = "crate::types::decimal")]
pub cost_basis: Decimal,
#[serde(default, with = "crate::types::option_decimal")]
pub unrealized_pl: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub unrealized_plpc: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub unrealized_intraday_pl: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub unrealized_intraday_plpc: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub current_price: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub lastday_price: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub change_today: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub swap_rate: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub avg_entry_swap_rate: Option<Decimal>,
#[serde(default)]
pub usd: Option<UsdPositionValues>,
#[serde(default, with = "crate::types::option_decimal")]
pub qty_available: Option<Decimal>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Order {
pub id: Uuid,
pub client_order_id: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub submitted_at: DateTime<Utc>,
#[serde(default)]
pub filled_at: Option<DateTime<Utc>>,
#[serde(default)]
pub expired_at: Option<DateTime<Utc>>,
#[serde(default)]
pub expires_at: Option<DateTime<Utc>>,
#[serde(default)]
pub canceled_at: Option<DateTime<Utc>>,
#[serde(default)]
pub failed_at: Option<DateTime<Utc>>,
#[serde(default)]
pub replaced_at: Option<DateTime<Utc>>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub replaced_by: Option<Uuid>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub replaces: Option<Uuid>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub asset_id: Option<Uuid>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub symbol: Option<String>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub asset_class: Option<AssetClass>,
#[serde(default, with = "crate::types::option_decimal")]
pub notional: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub qty: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub filled_qty: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub filled_avg_price: Option<Decimal>,
#[serde(default = "order_class_default", deserialize_with = "order_class")]
pub order_class: OrderClass,
#[deprecated(
since = "0.1.0",
note = "Alpaca's legacy duplicate of the `type` key; use `Order::order_type` instead. \
Still sent on responses, so the field stays populated"
)]
#[serde(
rename = "order_type",
default,
deserialize_with = "empty_string_as_none"
)]
pub legacy_order_type: Option<OrderType>,
#[serde(rename = "type", default, deserialize_with = "empty_string_as_none")]
pub order_type: Option<OrderType>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub side: Option<OrderSide>,
pub time_in_force: TimeInForce,
#[serde(default, with = "crate::types::option_decimal")]
pub limit_price: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub stop_price: Option<Decimal>,
pub status: OrderStatus,
pub extended_hours: bool,
#[serde(default)]
pub legs: Option<Vec<Order>>,
#[serde(default, with = "crate::types::option_decimal")]
pub trail_percent: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub trail_price: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub hwm: Option<Decimal>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub position_intent: Option<PositionIntent>,
#[serde(default, with = "crate::types::option_decimal")]
pub ratio_qty: Option<Decimal>,
}
fn order_class_default() -> OrderClass {
OrderClass::Simple
}
fn order_class<'de, D: Deserializer<'de>>(deserializer: D) -> Result<OrderClass, D::Error> {
let raw = Option::<String>::deserialize(deserializer)?;
Ok(match raw.as_deref().map(str::trim) {
None | Some("") => OrderClass::Simple,
Some(value) => OrderClass::from(value),
})
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FailedClosePositionDetails {
#[serde(with = "crate::types::int")]
pub code: i64,
pub message: String,
#[serde(default, with = "crate::types::option_decimal")]
pub available: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub existing_qty: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub held_for_orders: Option<Decimal>,
#[serde(default)]
pub symbol: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum ClosePositionBody {
Order(Box<Order>),
Failed(FailedClosePositionDetails),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ClosePositionResponse {
#[serde(default, deserialize_with = "empty_string_as_none")]
pub order_id: Option<Uuid>,
#[serde(default, with = "crate::types::option_int")]
pub status: Option<i64>,
#[serde(default)]
pub symbol: Option<String>,
pub body: ClosePositionBody,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CancelOrderResponse {
pub id: Uuid,
#[serde(with = "crate::types::int")]
pub status: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PortfolioHistory {
pub timestamp: Vec<i64>,
pub equity: Vec<f64>,
pub profit_loss: Vec<f64>,
pub profit_loss_pct: Vec<Option<f64>>,
#[serde(default)]
pub base_value: Option<f64>,
pub timeframe: String,
#[serde(default)]
pub cashflow: HashMap<ActivityType, Vec<f64>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Watchlist {
pub id: Uuid,
pub account_id: Uuid,
pub name: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
#[serde(default)]
pub assets: Option<Vec<Asset>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Clock {
pub timestamp: DateTime<Utc>,
pub is_open: bool,
pub next_open: DateTime<Utc>,
pub next_close: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Calendar {
pub date: NaiveDate,
pub open: NaiveDateTime,
pub close: NaiveDateTime,
pub session_open: Option<NaiveDateTime>,
pub session_close: Option<NaiveDateTime>,
pub settlement_date: Option<NaiveDate>,
}
impl Serialize for Calendar {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct as _;
let present = 3
+ usize::from(self.session_open.is_some())
+ usize::from(self.session_close.is_some())
+ usize::from(self.settlement_date.is_some());
let mut state = serializer.serialize_struct("Calendar", present)?;
state.serialize_field("date", &self.date)?;
state.serialize_field("open", &self.open.format("%H:%M").to_string())?;
state.serialize_field("close", &self.close.format("%H:%M").to_string())?;
match self.session_open {
Some(at) => state.serialize_field("session_open", &at.format("%H%M").to_string())?,
None => state.skip_field("session_open")?,
}
match self.session_close {
Some(at) => state.serialize_field("session_close", &at.format("%H%M").to_string())?,
None => state.skip_field("session_close")?,
}
match self.settlement_date {
Some(date) => state.serialize_field("settlement_date", &date)?,
None => state.skip_field("settlement_date")?,
}
state.end()
}
}
impl<'de> Deserialize<'de> for Calendar {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
struct Raw {
date: NaiveDate,
open: String,
close: String,
#[serde(default)]
session_open: Option<String>,
#[serde(default)]
session_close: Option<String>,
#[serde(default)]
settlement_date: Option<NaiveDate>,
}
let raw = Raw::deserialize(deserializer)?;
let combine = |time: &str, format: &str, field: &str| {
NaiveDateTime::parse_from_str(&format!("{} {time}", raw.date), format)
.map_err(|e| de::Error::custom(format!("{field}: {e}")))
};
let session = |time: Option<&String>, field: &str| match time {
Some(time) => combine(time, "%Y-%m-%d %H%M", field).map(Some),
None => Ok(None),
};
Ok(Self {
open: combine(&raw.open, "%Y-%m-%d %H:%M", "open")?,
close: combine(&raw.close, "%Y-%m-%d %H:%M", "close")?,
session_open: session(raw.session_open.as_ref(), "session_open")?,
session_close: session(raw.session_close.as_ref(), "session_close")?,
settlement_date: raw.settlement_date,
date: raw.date,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NonTradeActivity {
pub id: String,
pub account_id: Uuid,
pub activity_type: ActivityType,
pub date: NaiveDate,
#[serde(with = "crate::types::decimal")]
pub net_amount: Decimal,
pub description: String,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub status: Option<NonTradeActivityStatus>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub symbol: Option<String>,
#[serde(default, with = "crate::types::option_decimal")]
pub qty: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub price: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub per_share_amount: Option<Decimal>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TradeActivity {
pub id: String,
pub account_id: Uuid,
pub activity_type: ActivityType,
pub transaction_time: DateTime<Utc>,
#[serde(rename = "type")]
pub trade_type: TradeActivityType,
#[serde(with = "crate::types::decimal")]
pub price: Decimal,
#[serde(with = "crate::types::decimal")]
pub qty: Decimal,
pub side: OrderSide,
pub symbol: String,
#[serde(with = "crate::types::decimal")]
pub leaves_qty: Decimal,
pub order_id: Uuid,
#[serde(with = "crate::types::decimal")]
pub cum_qty: Decimal,
pub order_status: OrderStatus,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum Activity {
Trade(TradeActivity),
NonTrade(NonTradeActivity),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TradeAccount {
pub id: Uuid,
pub account_number: String,
pub status: AccountStatus,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub crypto_status: Option<AccountStatus>,
#[serde(default)]
pub currency: Option<crate::types::SupportedCurrencies>,
#[serde(default, with = "crate::types::option_decimal")]
pub buying_power: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub regt_buying_power: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub daytrading_buying_power: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub non_marginable_buying_power: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub cash: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub accrued_fees: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub pending_transfer_out: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub pending_transfer_in: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub portfolio_value: Option<Decimal>,
#[serde(default)]
pub pattern_day_trader: Option<bool>,
#[serde(default)]
pub trading_blocked: Option<bool>,
#[serde(default)]
pub transfers_blocked: Option<bool>,
#[serde(default)]
pub account_blocked: Option<bool>,
#[serde(default)]
pub created_at: Option<DateTime<Utc>>,
#[serde(default)]
pub trade_suspended_by_user: Option<bool>,
#[serde(default, with = "crate::types::option_decimal")]
pub multiplier: Option<Decimal>,
#[serde(default)]
pub shorting_enabled: Option<bool>,
#[serde(default, with = "crate::types::option_decimal")]
pub equity: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub last_equity: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub long_market_value: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub short_market_value: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub initial_margin: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub maintenance_margin: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub last_maintenance_margin: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub sma: Option<Decimal>,
#[serde(default, with = "crate::types::option_int")]
pub daytrade_count: Option<i64>,
#[serde(default, with = "crate::types::option_decimal")]
pub options_buying_power: Option<Decimal>,
#[serde(default, with = "crate::types::option_int")]
pub options_approved_level: Option<i64>,
#[serde(default, with = "crate::types::option_int")]
pub options_trading_level: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AccountConfiguration {
#[serde(
default,
deserialize_with = "empty_string_as_none",
skip_serializing_if = "Option::is_none"
)]
pub dtbp_check: Option<DTBPCheck>,
pub fractional_trading: bool,
#[serde(with = "crate::types::decimal")]
pub max_margin_multiplier: Decimal,
pub no_shorting: bool,
#[serde(
default,
deserialize_with = "empty_string_as_none",
skip_serializing_if = "Option::is_none"
)]
pub pdt_check: Option<PDTCheck>,
pub suspend_trade: bool,
pub trade_confirm_email: TradeConfirmationEmail,
pub ptp_no_exception_entry: bool,
#[serde(
default,
with = "crate::types::option_int",
skip_serializing_if = "Option::is_none"
)]
pub max_options_trading_level: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CorporateActionAnnouncement {
pub id: Uuid,
pub corporate_action_id: String,
pub ca_type: CorporateActionType,
pub ca_sub_type: CorporateActionSubType,
pub initiating_symbol: String,
pub initiating_original_cusip: String,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub target_symbol: Option<String>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub target_original_cusip: Option<String>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub declaration_date: Option<NaiveDate>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub ex_date: Option<NaiveDate>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub record_date: Option<NaiveDate>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub payable_date: Option<NaiveDate>,
#[serde(with = "crate::types::decimal")]
pub cash: Decimal,
#[serde(with = "crate::types::decimal")]
pub old_rate: Decimal,
#[serde(with = "crate::types::decimal")]
pub new_rate: Decimal,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TradeUpdate {
pub event: TradeEvent,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub execution_id: Option<Uuid>,
pub order: Order,
pub timestamp: DateTime<Utc>,
#[serde(default, with = "crate::types::option_decimal")]
pub position_qty: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub price: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub qty: Option<Decimal>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OptionContract {
pub id: String,
pub symbol: String,
pub name: String,
pub status: AssetStatus,
pub tradable: bool,
pub expiration_date: NaiveDate,
pub root_symbol: String,
pub underlying_symbol: String,
pub underlying_asset_id: Uuid,
#[serde(rename = "type")]
pub contract_type: ContractType,
pub style: ExerciseStyle,
#[serde(with = "crate::types::decimal")]
pub strike_price: Decimal,
#[serde(with = "crate::types::int")]
pub size: i64,
#[serde(default, with = "crate::types::option_int")]
pub open_interest: Option<i64>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub open_interest_date: Option<NaiveDate>,
#[serde(default, with = "crate::types::option_decimal")]
pub close_price: Option<Decimal>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub close_price_date: Option<NaiveDate>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OptionContractsResponse {
#[serde(default)]
pub option_contracts: Option<Vec<OptionContract>>,
#[serde(default)]
pub next_page_token: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_calendar_round_trips_through_json() {
let wire = serde_json::json!({
"date": "2024-01-02",
"open": "09:30",
"close": "16:00",
"session_open": "0400",
"session_close": "2000",
"settlement_date": "2024-01-03"
});
let calendar: Calendar = serde_json::from_value(wire).unwrap();
let encoded = serde_json::to_string(&calendar).unwrap();
let decoded: Calendar = serde_json::from_str(&encoded).unwrap();
assert_eq!(decoded, calendar);
}
#[test]
fn a_calendar_serializes_back_into_alpacas_own_shape() {
let wire = serde_json::json!({
"date": "2024-01-02",
"open": "09:30",
"close": "16:00",
"session_open": "0400",
"session_close": "2000"
});
let calendar: Calendar = serde_json::from_value(wire.clone()).unwrap();
let encoded: serde_json::Value = serde_json::to_value(&calendar).unwrap();
assert_eq!(encoded, wire);
}
#[test]
fn a_calendar_without_session_times_encodes_without_them() {
let wire = serde_json::json!({
"date": "2024-01-02",
"open": "09:30",
"close": "16:00"
});
let calendar: Calendar = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(serde_json::to_value(&calendar).unwrap(), wire);
}
#[test]
fn a_calendar_without_session_times_round_trips() {
let wire = serde_json::json!({
"date": "2024-01-02",
"open": "09:30",
"close": "16:00"
});
let calendar: Calendar = serde_json::from_value(wire).unwrap();
let encoded = serde_json::to_string(&calendar).unwrap();
let decoded: Calendar = serde_json::from_str(&encoded).unwrap();
assert_eq!(decoded, calendar);
assert_eq!(decoded.session_open, None);
}
}