Skip to main content

tradestation_api/
execution.rs

1//! Order execution endpoints for TradeStation v3.
2//!
3//! Covers:
4//! - `POST /v3/orderexecution/orders` (place)
5//! - `PUT /v3/orderexecution/orders/{id}` (replace)
6//! - `DELETE /v3/orderexecution/orders/{id}` (cancel)
7//! - `POST /v3/orderexecution/ordergroups` (OCO/bracket)
8//! - `POST /v3/orderexecution/ordergroupconfirm` (preview group)
9//! - `POST /v3/orderexecution/orderconfirm` (preview)
10//! - `GET /v3/orderexecution/activationtriggers`
11//! - `GET /v3/orderexecution/routes`
12
13use serde::{Deserialize, Serialize};
14
15use crate::Client;
16use crate::Error;
17
18/// Request body for placing a new order.
19///
20/// # Example
21///
22/// ```
23/// use tradestation_api::{OrderRequest, TimeInForce};
24///
25/// let order = OrderRequest {
26///     account_id: "123456".into(),
27///     symbol: "AAPL".into(),
28///     quantity: "10".into(),
29///     order_type: "Market".into(),
30///     trade_action: "BUY".into(),
31///     time_in_force: TimeInForce::day(),
32///     limit_price: None,
33///     stop_price: None,
34/// };
35/// ```
36#[derive(Debug, Serialize)]
37#[serde(rename_all = "PascalCase")]
38pub struct OrderRequest {
39    /// Target account ID.
40    pub account_id: String,
41    /// Ticker symbol to trade.
42    pub symbol: String,
43    /// Number of shares/contracts.
44    pub quantity: String,
45    /// Order type: "Market", "Limit", "StopMarket", or "StopLimit".
46    pub order_type: String,
47    /// Trade action: "BUY", "SELL", "BUYTOCOVER", or "SELLSHORT".
48    pub trade_action: String,
49    /// Order duration.
50    pub time_in_force: TimeInForce,
51    /// Limit price (required for Limit and StopLimit orders).
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub limit_price: Option<String>,
54    /// Stop price (required for StopMarket and StopLimit orders).
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub stop_price: Option<String>,
57}
58
59/// Order duration / time-in-force specification.
60///
61/// Use [`TimeInForce::day`] for day orders or [`TimeInForce::gtc`] for
62/// good-til-cancelled.
63#[derive(Debug, Serialize, Deserialize)]
64pub struct TimeInForce {
65    /// Duration string: "DAY", "GTC", "GTD", "IOC", "FOK", "OPG", "CLO".
66    #[serde(rename = "Duration")]
67    pub duration: String,
68}
69
70impl TimeInForce {
71    /// Day order -- expires at end of trading session.
72    pub fn day() -> Self {
73        Self {
74            duration: "DAY".to_string(),
75        }
76    }
77
78    /// Good-til-cancelled -- remains active until filled or cancelled.
79    pub fn gtc() -> Self {
80        Self {
81            duration: "GTC".to_string(),
82        }
83    }
84}
85
86/// Request body for placing OCO (one-cancels-other) or bracket order groups.
87///
88/// # Example
89///
90/// ```
91/// use tradestation_api::{OrderGroupRequest, OrderRequest, TimeInForce};
92///
93/// let group = OrderGroupRequest {
94///     group_type: "OCO".into(),
95///     orders: vec![
96///         // ... two OrderRequest legs
97///     ],
98/// };
99/// ```
100#[derive(Debug, Serialize)]
101#[serde(rename_all = "PascalCase")]
102pub struct OrderGroupRequest {
103    /// Group type: "OCO" or "BRK" (bracket).
104    #[serde(rename = "Type")]
105    pub group_type: String,
106    /// The order legs in this group.
107    pub orders: Vec<OrderRequest>,
108}
109
110/// Response from order placement, replacement, or cancellation.
111#[derive(Debug, Deserialize)]
112#[serde(rename_all = "PascalCase")]
113pub struct OrderResponse {
114    /// Successful order confirmations.
115    pub orders: Option<Vec<OrderConfirmation>>,
116    /// Errors that occurred during order processing.
117    pub errors: Option<Vec<OrderError>>,
118}
119
120/// Confirmation details for a successfully placed/modified order.
121#[derive(Debug, Deserialize)]
122#[serde(rename_all = "PascalCase")]
123pub struct OrderConfirmation {
124    /// The assigned order ID.
125    pub order_id: Option<String>,
126    /// Confirmation message.
127    pub message: Option<String>,
128}
129
130/// Error details for a failed order operation.
131#[derive(Debug, Deserialize)]
132#[serde(rename_all = "PascalCase")]
133pub struct OrderError {
134    /// Error code or type.
135    pub error: Option<String>,
136    /// Human-readable error message.
137    pub message: Option<String>,
138}
139
140/// Activation trigger definition for conditional orders.
141///
142/// Returned by [`Client::get_activation_triggers`].
143#[derive(Debug, Clone, Serialize, Deserialize)]
144#[serde(rename_all = "PascalCase")]
145pub struct ActivationTrigger {
146    /// Trigger key identifier.
147    pub key: String,
148    /// Display name.
149    pub name: String,
150    /// Human-readable description.
151    #[serde(default)]
152    pub description: Option<String>,
153}
154
155/// Response from activation triggers endpoint.
156#[derive(Debug, Deserialize)]
157#[serde(rename_all = "PascalCase")]
158struct ActivationTriggersResponse {
159    activation_triggers: Vec<ActivationTrigger>,
160}
161
162/// Order routing destination.
163///
164/// Returned by [`Client::get_routes`].
165#[derive(Debug, Clone, Serialize, Deserialize)]
166#[serde(rename_all = "PascalCase")]
167pub struct Route {
168    /// Route identifier.
169    pub id: String,
170    /// Route display name.
171    pub name: String,
172    /// Asset types this route supports (e.g., "Stock", "Option").
173    #[serde(default)]
174    pub asset_types: Vec<String>,
175}
176
177/// Response from routes endpoint.
178#[derive(Debug, Deserialize)]
179#[serde(rename_all = "PascalCase")]
180struct RoutesResponse {
181    routes: Vec<Route>,
182}
183
184impl Client {
185    /// Place a new order.
186    ///
187    /// # Errors
188    ///
189    /// Returns [`Error::Api`] if the order is rejected by TradeStation.
190    pub async fn place_order(&mut self, order: &OrderRequest) -> Result<OrderResponse, Error> {
191        let resp = self.post("/v3/orderexecution/orders", order).await?;
192        Ok(resp.json().await?)
193    }
194
195    /// Preview an order without executing it.
196    ///
197    /// Returns estimated fill details and any validation errors.
198    pub async fn confirm_order(&mut self, order: &OrderRequest) -> Result<OrderResponse, Error> {
199        let resp = self.post("/v3/orderexecution/orderconfirm", order).await?;
200        Ok(resp.json().await?)
201    }
202
203    /// Cancel an existing order by its order ID.
204    pub async fn cancel_order(&mut self, order_id: &str) -> Result<OrderResponse, Error> {
205        let resp = self
206            .delete(&format!("/v3/orderexecution/orders/{order_id}"))
207            .await?;
208        Ok(resp.json().await?)
209    }
210
211    /// Replace (modify) an existing order with new parameters.
212    pub async fn replace_order(
213        &mut self,
214        order_id: &str,
215        order: &OrderRequest,
216    ) -> Result<OrderResponse, Error> {
217        let resp = self
218            .put(&format!("/v3/orderexecution/orders/{order_id}"), order)
219            .await?;
220        Ok(resp.json().await?)
221    }
222
223    /// Place an OCO or bracket order group.
224    pub async fn place_order_group(
225        &mut self,
226        group: &OrderGroupRequest,
227    ) -> Result<OrderResponse, Error> {
228        let resp = self.post("/v3/orderexecution/ordergroups", group).await?;
229        Ok(resp.json().await?)
230    }
231
232    /// Preview an order group without executing.
233    pub async fn confirm_order_group(
234        &mut self,
235        group: &OrderGroupRequest,
236    ) -> Result<OrderResponse, Error> {
237        let resp = self
238            .post("/v3/orderexecution/ordergroupconfirm", group)
239            .await?;
240        Ok(resp.json().await?)
241    }
242
243    /// Get available activation triggers for conditional orders.
244    pub async fn get_activation_triggers(&mut self) -> Result<Vec<ActivationTrigger>, Error> {
245        let resp = self.get("/v3/orderexecution/activationtriggers").await?;
246        let data: ActivationTriggersResponse = resp.json().await?;
247        Ok(data.activation_triggers)
248    }
249
250    /// Get available order routing destinations.
251    pub async fn get_routes(&mut self) -> Result<Vec<Route>, Error> {
252        let resp = self.get("/v3/orderexecution/routes").await?;
253        let data: RoutesResponse = resp.json().await?;
254        Ok(data.routes)
255    }
256}