tradestation-api 0.1.0

Complete TradeStation REST API v3 wrapper for Rust
Documentation
//! Order execution endpoints for TradeStation v3.
//!
//! Covers:
//! - `POST /v3/orderexecution/orders` (place)
//! - `PUT /v3/orderexecution/orders/{id}` (replace)
//! - `DELETE /v3/orderexecution/orders/{id}` (cancel)
//! - `POST /v3/orderexecution/ordergroups` (OCO/bracket)
//! - `POST /v3/orderexecution/ordergroupconfirm` (preview group)
//! - `POST /v3/orderexecution/orderconfirm` (preview)
//! - `GET /v3/orderexecution/activationtriggers`
//! - `GET /v3/orderexecution/routes`

use serde::{Deserialize, Serialize};

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

/// Request body for placing a new order.
///
/// # Example
///
/// ```
/// use tradestation_api::{OrderRequest, TimeInForce};
///
/// let order = OrderRequest {
///     account_id: "123456".into(),
///     symbol: "AAPL".into(),
///     quantity: "10".into(),
///     order_type: "Market".into(),
///     trade_action: "BUY".into(),
///     time_in_force: TimeInForce::day(),
///     limit_price: None,
///     stop_price: None,
/// };
/// ```
#[derive(Debug, Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct OrderRequest {
    /// Target account ID.
    pub account_id: String,
    /// Ticker symbol to trade.
    pub symbol: String,
    /// Number of shares/contracts.
    pub quantity: String,
    /// Order type: "Market", "Limit", "StopMarket", or "StopLimit".
    pub order_type: String,
    /// Trade action: "BUY", "SELL", "BUYTOCOVER", or "SELLSHORT".
    pub trade_action: String,
    /// Order duration.
    pub time_in_force: TimeInForce,
    /// Limit price (required for Limit and StopLimit orders).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub limit_price: Option<String>,
    /// Stop price (required for StopMarket and StopLimit orders).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stop_price: Option<String>,
}

/// Order duration / time-in-force specification.
///
/// Use [`TimeInForce::day`] for day orders or [`TimeInForce::gtc`] for
/// good-til-cancelled.
#[derive(Debug, Serialize, Deserialize)]
pub struct TimeInForce {
    /// Duration string: "DAY", "GTC", "GTD", "IOC", "FOK", "OPG", "CLO".
    #[serde(rename = "Duration")]
    pub duration: String,
}

impl TimeInForce {
    /// Day order -- expires at end of trading session.
    pub fn day() -> Self {
        Self {
            duration: "DAY".to_string(),
        }
    }

    /// Good-til-cancelled -- remains active until filled or cancelled.
    pub fn gtc() -> Self {
        Self {
            duration: "GTC".to_string(),
        }
    }
}

/// Request body for placing OCO (one-cancels-other) or bracket order groups.
///
/// # Example
///
/// ```
/// use tradestation_api::{OrderGroupRequest, OrderRequest, TimeInForce};
///
/// let group = OrderGroupRequest {
///     group_type: "OCO".into(),
///     orders: vec![
///         // ... two OrderRequest legs
///     ],
/// };
/// ```
#[derive(Debug, Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct OrderGroupRequest {
    /// Group type: "OCO" or "BRK" (bracket).
    #[serde(rename = "Type")]
    pub group_type: String,
    /// The order legs in this group.
    pub orders: Vec<OrderRequest>,
}

/// Response from order placement, replacement, or cancellation.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct OrderResponse {
    /// Successful order confirmations.
    pub orders: Option<Vec<OrderConfirmation>>,
    /// Errors that occurred during order processing.
    pub errors: Option<Vec<OrderError>>,
}

/// Confirmation details for a successfully placed/modified order.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct OrderConfirmation {
    /// The assigned order ID.
    pub order_id: Option<String>,
    /// Confirmation message.
    pub message: Option<String>,
}

/// Error details for a failed order operation.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct OrderError {
    /// Error code or type.
    pub error: Option<String>,
    /// Human-readable error message.
    pub message: Option<String>,
}

/// Activation trigger definition for conditional orders.
///
/// Returned by [`Client::get_activation_triggers`].
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ActivationTrigger {
    /// Trigger key identifier.
    pub key: String,
    /// Display name.
    pub name: String,
    /// Human-readable description.
    #[serde(default)]
    pub description: Option<String>,
}

/// Response from activation triggers endpoint.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct ActivationTriggersResponse {
    activation_triggers: Vec<ActivationTrigger>,
}

/// Order routing destination.
///
/// Returned by [`Client::get_routes`].
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Route {
    /// Route identifier.
    pub id: String,
    /// Route display name.
    pub name: String,
    /// Asset types this route supports (e.g., "Stock", "Option").
    #[serde(default)]
    pub asset_types: Vec<String>,
}

/// Response from routes endpoint.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct RoutesResponse {
    routes: Vec<Route>,
}

impl Client {
    /// Place a new order.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Api`] if the order is rejected by TradeStation.
    pub async fn place_order(&mut self, order: &OrderRequest) -> Result<OrderResponse, Error> {
        let resp = self.post("/v3/orderexecution/orders", order).await?;
        Ok(resp.json().await?)
    }

    /// Preview an order without executing it.
    ///
    /// Returns estimated fill details and any validation errors.
    pub async fn confirm_order(&mut self, order: &OrderRequest) -> Result<OrderResponse, Error> {
        let resp = self.post("/v3/orderexecution/orderconfirm", order).await?;
        Ok(resp.json().await?)
    }

    /// Cancel an existing order by its order ID.
    pub async fn cancel_order(&mut self, order_id: &str) -> Result<OrderResponse, Error> {
        let resp = self
            .delete(&format!("/v3/orderexecution/orders/{order_id}"))
            .await?;
        Ok(resp.json().await?)
    }

    /// Replace (modify) an existing order with new parameters.
    pub async fn replace_order(
        &mut self,
        order_id: &str,
        order: &OrderRequest,
    ) -> Result<OrderResponse, Error> {
        let resp = self
            .put(&format!("/v3/orderexecution/orders/{order_id}"), order)
            .await?;
        Ok(resp.json().await?)
    }

    /// Place an OCO or bracket order group.
    pub async fn place_order_group(
        &mut self,
        group: &OrderGroupRequest,
    ) -> Result<OrderResponse, Error> {
        let resp = self.post("/v3/orderexecution/ordergroups", group).await?;
        Ok(resp.json().await?)
    }

    /// Preview an order group without executing.
    pub async fn confirm_order_group(
        &mut self,
        group: &OrderGroupRequest,
    ) -> Result<OrderResponse, Error> {
        let resp = self
            .post("/v3/orderexecution/ordergroupconfirm", group)
            .await?;
        Ok(resp.json().await?)
    }

    /// Get available activation triggers for conditional orders.
    pub async fn get_activation_triggers(&mut self) -> Result<Vec<ActivationTrigger>, Error> {
        let resp = self.get("/v3/orderexecution/activationtriggers").await?;
        let data: ActivationTriggersResponse = resp.json().await?;
        Ok(data.activation_triggers)
    }

    /// Get available order routing destinations.
    pub async fn get_routes(&mut self) -> Result<Vec<Route>, Error> {
        let resp = self.get("/v3/orderexecution/routes").await?;
        let data: RoutesResponse = resp.json().await?;
        Ok(data.routes)
    }
}