tradestation-api 0.1.0

Complete TradeStation REST API v3 wrapper for Rust
Documentation
//! Brokerage account endpoints for TradeStation v3.
//!
//! Covers:
//! - `GET /v3/brokerage/accounts`
//! - `GET /v3/brokerage/accounts/{id}/balances`
//! - `GET /v3/brokerage/accounts/{id}/bodbalances`
//! - `GET /v3/brokerage/accounts/{id}/positions`
//! - `GET /v3/brokerage/accounts/{id}/orders`
//! - `GET /v3/brokerage/accounts/{id}/orders/{orderId}`
//! - `GET /v3/brokerage/accounts/{id}/historicalorders`
//! - `GET /v3/brokerage/accounts/{id}/historicalorders/{orderId}`
//! - `GET /v3/brokerage/accounts/{id}/wallets`

use serde::{Deserialize, Serialize};

use crate::Client;
use crate::Error;

/// A trading account on TradeStation.
///
/// Returned by [`Client::get_accounts`].
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Account {
    /// Unique account identifier.
    #[serde(alias = "AccountID")]
    pub account_id: String,
    /// Account type (e.g., "Margin", "Cash", "Futures").
    #[serde(default)]
    pub account_type: Option<String>,
    /// Currency (e.g., "USD").
    #[serde(default)]
    pub currency: Option<String>,
    /// Account display name.
    #[serde(default)]
    pub name: Option<String>,
    /// Account status (e.g., "Active", "Closed").
    #[serde(default)]
    pub status: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct AccountsResponse {
    accounts: Vec<Account>,
}

/// Real-time account balance information.
///
/// Returned by [`Client::get_balances`].
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Balance {
    /// Account this balance belongs to.
    pub account_id: Option<String>,
    /// Available cash balance.
    pub cash_balance: Option<String>,
    /// Total account equity.
    pub equity: Option<String>,
    /// Total market value of positions.
    pub market_value: Option<String>,
    /// Available buying power.
    pub buying_power: Option<String>,
    /// Realized profit/loss for the day.
    pub realized_profit_loss: Option<String>,
    /// Unrealized profit/loss on open positions.
    pub unrealized_profit_loss: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct BalancesResponse {
    balances: Vec<Balance>,
}

/// An open position in an account.
///
/// Returned by [`Client::get_positions`].
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Position {
    /// Account holding this position.
    pub account_id: Option<String>,
    /// Ticker symbol.
    pub symbol: Option<String>,
    /// Position quantity (positive = long, negative = short).
    pub quantity: Option<String>,
    /// Average entry price.
    pub average_price: Option<String>,
    /// Last traded price.
    pub last: Option<String>,
    /// Current market value of the position.
    pub market_value: Option<String>,
    /// Unrealized P&L in currency.
    pub unrealized_profit_loss: Option<String>,
    /// Unrealized P&L as a percentage.
    pub unrealized_profit_loss_percent: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct PositionsResponse {
    positions: Vec<Position>,
}

/// An order record (active or historical).
///
/// Returned by [`Client::get_orders`] and [`Client::get_historical_orders`].
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Order {
    /// Unique order identifier.
    pub order_id: Option<String>,
    /// Account this order belongs to.
    pub account_id: Option<String>,
    /// Ticker symbol.
    pub symbol: Option<String>,
    /// Ordered quantity.
    pub quantity: Option<String>,
    /// Filled quantity.
    pub filled_quantity: Option<String>,
    /// Order type (e.g., "Market", "Limit", "StopMarket").
    pub order_type: Option<String>,
    /// Current order status.
    pub status: Option<String>,
    /// Human-readable status description.
    pub status_description: Option<String>,
    /// Limit price (for Limit and StopLimit orders).
    pub limit_price: Option<String>,
    /// Stop price (for Stop and StopLimit orders).
    pub stop_price: Option<String>,
    /// Trade action (e.g., "BUY", "SELL", "SELLSHORT").
    pub trade_action: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct OrdersResponse {
    orders: Vec<Order>,
}

/// Beginning-of-day balance snapshot.
///
/// Returned by [`Client::get_bod_balances`]. Represents account state at market open.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct BodBalance {
    /// Account identifier.
    pub account_id: Option<String>,
    /// Cash balance at beginning of day.
    pub cash_balance: Option<String>,
    /// Equity at beginning of day.
    pub equity: Option<String>,
    /// Market value at beginning of day.
    pub market_value: Option<String>,
    /// Buying power at beginning of day.
    pub buying_power: Option<String>,
    /// Realized P&L at beginning of day.
    pub realized_profit_loss: Option<String>,
    /// Unrealized P&L at beginning of day.
    pub unrealized_profit_loss: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct BodBalancesResponse {
    #[serde(rename = "BODBalances")]
    bod_balances: Vec<BodBalance>,
}

/// A cryptocurrency wallet balance.
///
/// Returned by [`Client::get_wallets`].
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Wallet {
    /// Cryptocurrency symbol (e.g., "BTC", "ETH").
    pub currency: Option<String>,
    /// Wallet balance.
    pub balance: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct WalletsResponse {
    wallets: Vec<Wallet>,
}

impl Client {
    /// Get all trading accounts associated with the authenticated user.
    pub async fn get_accounts(&mut self) -> Result<Vec<Account>, Error> {
        let resp = self.get("/v3/brokerage/accounts").await?;
        let data: AccountsResponse = resp.json().await?;
        Ok(data.accounts)
    }

    /// Get real-time balances for the specified accounts.
    pub async fn get_balances(&mut self, account_ids: &[&str]) -> Result<Vec<Balance>, Error> {
        let ids = account_ids.join(",");
        let resp = self
            .get(&format!("/v3/brokerage/accounts/{ids}/balances"))
            .await?;
        let data: BalancesResponse = resp.json().await?;
        Ok(data.balances)
    }

    /// Get open positions for the specified accounts.
    pub async fn get_positions(&mut self, account_ids: &[&str]) -> Result<Vec<Position>, Error> {
        let ids = account_ids.join(",");
        let resp = self
            .get(&format!("/v3/brokerage/accounts/{ids}/positions"))
            .await?;
        let data: PositionsResponse = resp.json().await?;
        Ok(data.positions)
    }

    /// Get active orders for the specified accounts.
    pub async fn get_orders(&mut self, account_ids: &[&str]) -> Result<Vec<Order>, Error> {
        let ids = account_ids.join(",");
        let resp = self
            .get(&format!("/v3/brokerage/accounts/{ids}/orders"))
            .await?;
        let data: OrdersResponse = resp.json().await?;
        Ok(data.orders)
    }

    /// Get beginning-of-day balance snapshots for the specified accounts.
    pub async fn get_bod_balances(
        &mut self,
        account_ids: &[&str],
    ) -> Result<Vec<BodBalance>, Error> {
        let ids = account_ids.join(",");
        let resp = self
            .get(&format!("/v3/brokerage/accounts/{ids}/bodbalances"))
            .await?;
        let data: BodBalancesResponse = resp.json().await?;
        Ok(data.bod_balances)
    }

    /// Get specific orders by order ID.
    pub async fn get_orders_by_id(
        &mut self,
        account_ids: &[&str],
        order_ids: &[&str],
    ) -> Result<Vec<Order>, Error> {
        let ids = account_ids.join(",");
        let oids = order_ids.join(",");
        let resp = self
            .get(&format!("/v3/brokerage/accounts/{ids}/orders/{oids}"))
            .await?;
        let data: OrdersResponse = resp.json().await?;
        Ok(data.orders)
    }

    /// Get historical (filled/cancelled) orders for the specified accounts.
    ///
    /// `since` is a date string in YYYY-MM-DD format specifying how far back to look.
    /// TradeStation limits historical orders to ~90 days.
    pub async fn get_historical_orders(
        &mut self,
        account_ids: &[&str],
        since: &str,
    ) -> Result<Vec<Order>, Error> {
        let ids = account_ids.join(",");
        let headers = self.auth_headers().await?;
        let url = format!(
            "{}/v3/brokerage/accounts/{ids}/historicalorders",
            self.base_url()
        );
        let resp = self
            .http
            .get(&url)
            .headers(headers)
            .query(&[("since", since)])
            .send()
            .await?;
        if !resp.status().is_success() {
            let status = resp.status().as_u16();
            let body = resp.text().await.unwrap_or_default();
            return Err(Error::Api {
                status,
                message: body,
            });
        }
        let data: OrdersResponse = resp.json().await?;
        Ok(data.orders)
    }

    /// Get specific historical orders by order ID.
    pub async fn get_historical_orders_by_id(
        &mut self,
        account_ids: &[&str],
        order_ids: &[&str],
    ) -> Result<Vec<Order>, Error> {
        let ids = account_ids.join(",");
        let oids = order_ids.join(",");
        let resp = self
            .get(&format!(
                "/v3/brokerage/accounts/{ids}/historicalorders/{oids}"
            ))
            .await?;
        let data: OrdersResponse = resp.json().await?;
        Ok(data.orders)
    }

    /// Get cryptocurrency wallet balances for the specified accounts.
    pub async fn get_wallets(&mut self, account_ids: &[&str]) -> Result<Vec<Wallet>, Error> {
        let ids = account_ids.join(",");
        let resp = self
            .get(&format!("/v3/brokerage/accounts/{ids}/wallets"))
            .await?;
        let data: WalletsResponse = resp.json().await?;
        Ok(data.wallets)
    }
}