Skip to main content

tradestation_api/
brokerage.rs

1//! Brokerage account endpoints for TradeStation v3.
2//!
3//! Covers:
4//! - `GET /v3/brokerage/accounts`
5//! - `GET /v3/brokerage/accounts/{id}/balances`
6//! - `GET /v3/brokerage/accounts/{id}/bodbalances`
7//! - `GET /v3/brokerage/accounts/{id}/positions`
8//! - `GET /v3/brokerage/accounts/{id}/orders`
9//! - `GET /v3/brokerage/accounts/{id}/orders/{orderId}`
10//! - `GET /v3/brokerage/accounts/{id}/historicalorders`
11//! - `GET /v3/brokerage/accounts/{id}/historicalorders/{orderId}`
12//! - `GET /v3/brokerage/accounts/{id}/wallets`
13
14use serde::{Deserialize, Serialize};
15
16use crate::Client;
17use crate::Error;
18
19/// A trading account on TradeStation.
20///
21/// Returned by [`Client::get_accounts`].
22#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(rename_all = "PascalCase")]
24pub struct Account {
25    /// Unique account identifier.
26    #[serde(alias = "AccountID")]
27    pub account_id: String,
28    /// Account type (e.g., "Margin", "Cash", "Futures").
29    #[serde(default)]
30    pub account_type: Option<String>,
31    /// Currency (e.g., "USD").
32    #[serde(default)]
33    pub currency: Option<String>,
34    /// Account display name.
35    #[serde(default)]
36    pub name: Option<String>,
37    /// Account status (e.g., "Active", "Closed").
38    #[serde(default)]
39    pub status: Option<String>,
40}
41
42#[derive(Debug, Deserialize)]
43#[serde(rename_all = "PascalCase")]
44struct AccountsResponse {
45    accounts: Vec<Account>,
46}
47
48/// Real-time account balance information.
49///
50/// Returned by [`Client::get_balances`].
51#[derive(Debug, Clone, Serialize, Deserialize)]
52#[serde(rename_all = "PascalCase")]
53pub struct Balance {
54    /// Account this balance belongs to.
55    pub account_id: Option<String>,
56    /// Available cash balance.
57    pub cash_balance: Option<String>,
58    /// Total account equity.
59    pub equity: Option<String>,
60    /// Total market value of positions.
61    pub market_value: Option<String>,
62    /// Available buying power.
63    pub buying_power: Option<String>,
64    /// Realized profit/loss for the day.
65    pub realized_profit_loss: Option<String>,
66    /// Unrealized profit/loss on open positions.
67    pub unrealized_profit_loss: Option<String>,
68}
69
70#[derive(Debug, Deserialize)]
71#[serde(rename_all = "PascalCase")]
72struct BalancesResponse {
73    balances: Vec<Balance>,
74}
75
76/// An open position in an account.
77///
78/// Returned by [`Client::get_positions`].
79#[derive(Debug, Clone, Serialize, Deserialize)]
80#[serde(rename_all = "PascalCase")]
81pub struct Position {
82    /// Account holding this position.
83    pub account_id: Option<String>,
84    /// Ticker symbol.
85    pub symbol: Option<String>,
86    /// Position quantity (positive = long, negative = short).
87    pub quantity: Option<String>,
88    /// Average entry price.
89    pub average_price: Option<String>,
90    /// Last traded price.
91    pub last: Option<String>,
92    /// Current market value of the position.
93    pub market_value: Option<String>,
94    /// Unrealized P&L in currency.
95    pub unrealized_profit_loss: Option<String>,
96    /// Unrealized P&L as a percentage.
97    pub unrealized_profit_loss_percent: Option<String>,
98}
99
100#[derive(Debug, Deserialize)]
101#[serde(rename_all = "PascalCase")]
102struct PositionsResponse {
103    positions: Vec<Position>,
104}
105
106/// An order record (active or historical).
107///
108/// Returned by [`Client::get_orders`] and [`Client::get_historical_orders`].
109#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(rename_all = "PascalCase")]
111pub struct Order {
112    /// Unique order identifier.
113    pub order_id: Option<String>,
114    /// Account this order belongs to.
115    pub account_id: Option<String>,
116    /// Ticker symbol.
117    pub symbol: Option<String>,
118    /// Ordered quantity.
119    pub quantity: Option<String>,
120    /// Filled quantity.
121    pub filled_quantity: Option<String>,
122    /// Order type (e.g., "Market", "Limit", "StopMarket").
123    pub order_type: Option<String>,
124    /// Current order status.
125    pub status: Option<String>,
126    /// Human-readable status description.
127    pub status_description: Option<String>,
128    /// Limit price (for Limit and StopLimit orders).
129    pub limit_price: Option<String>,
130    /// Stop price (for Stop and StopLimit orders).
131    pub stop_price: Option<String>,
132    /// Trade action (e.g., "BUY", "SELL", "SELLSHORT").
133    pub trade_action: Option<String>,
134}
135
136#[derive(Debug, Deserialize)]
137#[serde(rename_all = "PascalCase")]
138struct OrdersResponse {
139    orders: Vec<Order>,
140}
141
142/// Beginning-of-day balance snapshot.
143///
144/// Returned by [`Client::get_bod_balances`]. Represents account state at market open.
145#[derive(Debug, Clone, Serialize, Deserialize)]
146#[serde(rename_all = "PascalCase")]
147pub struct BodBalance {
148    /// Account identifier.
149    pub account_id: Option<String>,
150    /// Cash balance at beginning of day.
151    pub cash_balance: Option<String>,
152    /// Equity at beginning of day.
153    pub equity: Option<String>,
154    /// Market value at beginning of day.
155    pub market_value: Option<String>,
156    /// Buying power at beginning of day.
157    pub buying_power: Option<String>,
158    /// Realized P&L at beginning of day.
159    pub realized_profit_loss: Option<String>,
160    /// Unrealized P&L at beginning of day.
161    pub unrealized_profit_loss: Option<String>,
162}
163
164#[derive(Debug, Deserialize)]
165#[serde(rename_all = "PascalCase")]
166struct BodBalancesResponse {
167    #[serde(rename = "BODBalances")]
168    bod_balances: Vec<BodBalance>,
169}
170
171/// A cryptocurrency wallet balance.
172///
173/// Returned by [`Client::get_wallets`].
174#[derive(Debug, Clone, Serialize, Deserialize)]
175#[serde(rename_all = "PascalCase")]
176pub struct Wallet {
177    /// Cryptocurrency symbol (e.g., "BTC", "ETH").
178    pub currency: Option<String>,
179    /// Wallet balance.
180    pub balance: Option<String>,
181}
182
183#[derive(Debug, Deserialize)]
184#[serde(rename_all = "PascalCase")]
185struct WalletsResponse {
186    wallets: Vec<Wallet>,
187}
188
189impl Client {
190    /// Get all trading accounts associated with the authenticated user.
191    pub async fn get_accounts(&mut self) -> Result<Vec<Account>, Error> {
192        let resp = self.get("/v3/brokerage/accounts").await?;
193        let data: AccountsResponse = resp.json().await?;
194        Ok(data.accounts)
195    }
196
197    /// Get real-time balances for the specified accounts.
198    pub async fn get_balances(&mut self, account_ids: &[&str]) -> Result<Vec<Balance>, Error> {
199        let ids = account_ids.join(",");
200        let resp = self
201            .get(&format!("/v3/brokerage/accounts/{ids}/balances"))
202            .await?;
203        let data: BalancesResponse = resp.json().await?;
204        Ok(data.balances)
205    }
206
207    /// Get open positions for the specified accounts.
208    pub async fn get_positions(&mut self, account_ids: &[&str]) -> Result<Vec<Position>, Error> {
209        let ids = account_ids.join(",");
210        let resp = self
211            .get(&format!("/v3/brokerage/accounts/{ids}/positions"))
212            .await?;
213        let data: PositionsResponse = resp.json().await?;
214        Ok(data.positions)
215    }
216
217    /// Get active orders for the specified accounts.
218    pub async fn get_orders(&mut self, account_ids: &[&str]) -> Result<Vec<Order>, Error> {
219        let ids = account_ids.join(",");
220        let resp = self
221            .get(&format!("/v3/brokerage/accounts/{ids}/orders"))
222            .await?;
223        let data: OrdersResponse = resp.json().await?;
224        Ok(data.orders)
225    }
226
227    /// Get beginning-of-day balance snapshots for the specified accounts.
228    pub async fn get_bod_balances(
229        &mut self,
230        account_ids: &[&str],
231    ) -> Result<Vec<BodBalance>, Error> {
232        let ids = account_ids.join(",");
233        let resp = self
234            .get(&format!("/v3/brokerage/accounts/{ids}/bodbalances"))
235            .await?;
236        let data: BodBalancesResponse = resp.json().await?;
237        Ok(data.bod_balances)
238    }
239
240    /// Get specific orders by order ID.
241    pub async fn get_orders_by_id(
242        &mut self,
243        account_ids: &[&str],
244        order_ids: &[&str],
245    ) -> Result<Vec<Order>, Error> {
246        let ids = account_ids.join(",");
247        let oids = order_ids.join(",");
248        let resp = self
249            .get(&format!("/v3/brokerage/accounts/{ids}/orders/{oids}"))
250            .await?;
251        let data: OrdersResponse = resp.json().await?;
252        Ok(data.orders)
253    }
254
255    /// Get historical (filled/cancelled) orders for the specified accounts.
256    ///
257    /// `since` is a date string in YYYY-MM-DD format specifying how far back to look.
258    /// TradeStation limits historical orders to ~90 days.
259    pub async fn get_historical_orders(
260        &mut self,
261        account_ids: &[&str],
262        since: &str,
263    ) -> Result<Vec<Order>, Error> {
264        let ids = account_ids.join(",");
265        let headers = self.auth_headers().await?;
266        let url = format!(
267            "{}/v3/brokerage/accounts/{ids}/historicalorders",
268            self.base_url()
269        );
270        let resp = self
271            .http
272            .get(&url)
273            .headers(headers)
274            .query(&[("since", since)])
275            .send()
276            .await?;
277        if !resp.status().is_success() {
278            let status = resp.status().as_u16();
279            let body = resp.text().await.unwrap_or_default();
280            return Err(Error::Api {
281                status,
282                message: body,
283            });
284        }
285        let data: OrdersResponse = resp.json().await?;
286        Ok(data.orders)
287    }
288
289    /// Get specific historical orders by order ID.
290    pub async fn get_historical_orders_by_id(
291        &mut self,
292        account_ids: &[&str],
293        order_ids: &[&str],
294    ) -> Result<Vec<Order>, Error> {
295        let ids = account_ids.join(",");
296        let oids = order_ids.join(",");
297        let resp = self
298            .get(&format!(
299                "/v3/brokerage/accounts/{ids}/historicalorders/{oids}"
300            ))
301            .await?;
302        let data: OrdersResponse = resp.json().await?;
303        Ok(data.orders)
304    }
305
306    /// Get cryptocurrency wallet balances for the specified accounts.
307    pub async fn get_wallets(&mut self, account_ids: &[&str]) -> Result<Vec<Wallet>, Error> {
308        let ids = account_ids.join(",");
309        let resp = self
310            .get(&format!("/v3/brokerage/accounts/{ids}/wallets"))
311            .await?;
312        let data: WalletsResponse = resp.json().await?;
313        Ok(data.wallets)
314    }
315}