use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::types::wire::wire_enum;
wire_enum! {
pub enum OptionsLevel {
Zero => "0",
One => "1",
Two => "2",
Three => "3",
}
}
wire_enum! {
pub enum OptionsApprovalStatus {
Pending => "PENDING",
Approved => "APPROVED",
LowerLevelApproved => "LOWER_LEVEL_APPROVED",
Rejected => "REJECTED",
}
}
wire_enum! {
pub enum OptionsApprovalRequester {
Correspondent => "CORRESPONDENT",
AlpacaAdmin => "ALPACA_ADMIN",
}
}
wire_enum! {
pub enum ActivityCategory {
TradeActivity => "trade_activity",
NonTradeActivity => "non_trade_activity",
}
}
wire_enum! {
pub enum RiskRating {
Low => "low",
Medium => "medium",
High => "high",
Prohibited => "prohibited",
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OptionsApproval {
#[serde(default)]
pub id: Option<Uuid>,
#[serde(default)]
pub account_id: Option<Uuid>,
#[serde(default)]
pub requested_level: Option<OptionsLevel>,
#[serde(default)]
pub approved_level: Option<OptionsLevel>,
#[serde(default)]
pub status: Option<OptionsApprovalStatus>,
#[serde(default)]
pub requester: Option<OptionsApprovalRequester>,
#[serde(default)]
pub created_at: Option<DateTime<Utc>>,
#[serde(default)]
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OptionsApprovalsPage {
#[serde(
default,
deserialize_with = "crate::types::serde_util::null_as_default"
)]
pub options_approvals: Vec<OptionsApproval>,
#[serde(default)]
pub next_page_token: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RequestOptionsApprovalRequest {
pub level: OptionsLevel,
}
impl RequestOptionsApprovalRequest {
pub fn new(level: OptionsLevel) -> crate::Result<Self> {
if level == OptionsLevel::Zero {
return Err(crate::Error::InvalidRequest(
"options level 0 is an outcome, not a level that can be requested".to_owned(),
));
}
Ok(Self { level })
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetOptionsApprovalsRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub account_id: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub requested_level: Option<OptionsLevel>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approved_level: Option<OptionsLevel>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<OptionsApprovalStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page_size: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page_token: Option<String>,
}
impl GetOptionsApprovalsRequest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn account_id(mut self, account_id: Uuid) -> Self {
self.account_id = Some(account_id);
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OnfidoToken {
#[serde(default)]
pub token: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetOnfidoTokenRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub referrer: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub platform: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UpdateOnfidoOutcomeRequest {
pub token: String,
pub outcome: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
impl UpdateOnfidoOutcomeRequest {
#[must_use]
pub fn new(token: impl Into<String>, outcome: impl Into<String>) -> Self {
Self {
token: token.into(),
outcome: outcome.into(),
reason: None,
}
}
#[must_use]
pub fn reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CountryInfo {
pub full_name: String,
pub securities_risk_rating: RiskRating,
pub crypto_risk_rating: RiskRating,
#[serde(
default,
deserialize_with = "crate::types::serde_util::null_as_default"
)]
pub crypto_supported_states: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct IraExcessContribution {
#[serde(default)]
pub account_id: Option<String>,
#[serde(default)]
pub tax_year: Option<f64>,
#[serde(default)]
pub total_contribution_amount: Option<f64>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TradingLimitsUsd {
#[serde(default, with = "crate::types::option_decimal")]
pub daily_net_limit: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub used: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub available: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub held: Option<Decimal>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TradingLimits {
#[serde(default, with = "crate::types::option_decimal")]
pub daily_net_limit: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub used: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub available: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub held: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub swap_rate: Option<Decimal>,
#[serde(default)]
pub usd: Option<TradingLimitsUsd>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct EstimateOrderRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub symbol: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub notional: Option<Decimal>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub side: Option<crate::trading::OrderSide>,
#[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
pub order_type: Option<crate::trading::OrderType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub time_in_force: Option<crate::trading::TimeInForce>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub swap_fee_bps: Option<i64>,
}
impl EstimateOrderRequest {
pub fn notional(
symbol: impl Into<String>,
notional: Decimal,
side: crate::trading::OrderSide,
) -> Self {
Self {
symbol: Some(symbol.into()),
notional: Some(notional),
side: Some(side),
..Self::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn level_zero_cannot_be_asked_for() {
assert!(RequestOptionsApprovalRequest::new(OptionsLevel::Zero).is_err());
assert!(RequestOptionsApprovalRequest::new(OptionsLevel::Two).is_ok());
}
#[test]
fn options_levels_are_numeric_strings_on_the_wire() {
assert_eq!(OptionsLevel::Three.as_str(), "3");
}
#[test]
fn a_country_with_no_state_carve_outs_decodes() {
let country: CountryInfo = serde_json::from_value(serde_json::json!({
"full_name": "United Kingdom",
"securities_risk_rating": "low",
"crypto_risk_rating": "medium",
}))
.unwrap();
assert!(country.crypto_supported_states.is_empty());
assert_eq!(country.crypto_risk_rating, RiskRating::Medium);
}
}