tradestation-api 0.1.0

Complete TradeStation REST API v3 wrapper for Rust
Documentation
mod helpers;

use tradestation_api::{OrderGroupRequest, OrderRequest, TimeInForce};
use wiremock::matchers::{method, path};
use wiremock::{Mock, ResponseTemplate};

fn sample_order() -> OrderRequest {
    OrderRequest {
        account_id: "123456789".to_string(),
        symbol: "AAPL".to_string(),
        quantity: "100".to_string(),
        order_type: "Market".to_string(),
        trade_action: "Buy".to_string(),
        time_in_force: TimeInForce::day(),
        limit_price: None,
        stop_price: None,
    }
}

fn sample_limit_order(price: &str, action: &str) -> OrderRequest {
    OrderRequest {
        account_id: "123456789".to_string(),
        symbol: "AAPL".to_string(),
        quantity: "100".to_string(),
        order_type: "Limit".to_string(),
        trade_action: action.to_string(),
        time_in_force: TimeInForce::gtc(),
        limit_price: Some(price.to_string()),
        stop_price: None,
    }
}

fn order_success_response() -> serde_json::Value {
    serde_json::json!({
        "Orders": [
            {
                "OrderId": "ORD12345",
                "Message": "Order has been accepted"
            }
        ]
    })
}

#[tokio::test]
async fn test_place_order() {
    let server = helpers::setup_mock_server().await;
    let mut client = helpers::mock_client(&server);

    Mock::given(method("POST"))
        .and(path("/v3/orderexecution/orders"))
        .respond_with(ResponseTemplate::new(200).set_body_json(order_success_response()))
        .mount(&server)
        .await;

    let order = sample_order();
    let result = client.place_order(&order).await.unwrap();

    let orders = result.orders.unwrap();
    assert_eq!(orders.len(), 1);
    assert_eq!(orders[0].order_id.as_deref(), Some("ORD12345"));
    assert_eq!(
        orders[0].message.as_deref(),
        Some("Order has been accepted")
    );
}

#[tokio::test]
async fn test_replace_order() {
    let server = helpers::setup_mock_server().await;
    let mut client = helpers::mock_client(&server);

    Mock::given(method("PUT"))
        .and(path("/v3/orderexecution/orders/ORD12345"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "Orders": [
                {
                    "OrderId": "ORD12346",
                    "Message": "Order has been replaced"
                }
            ]
        })))
        .mount(&server)
        .await;

    let mut order = sample_limit_order("190.00", "Buy");
    order.limit_price = Some("192.00".to_string());
    let result = client.replace_order("ORD12345", &order).await.unwrap();

    let orders = result.orders.unwrap();
    assert_eq!(orders[0].order_id.as_deref(), Some("ORD12346"));
    assert_eq!(
        orders[0].message.as_deref(),
        Some("Order has been replaced")
    );
}

#[tokio::test]
async fn test_cancel_order() {
    let server = helpers::setup_mock_server().await;
    let mut client = helpers::mock_client(&server);

    Mock::given(method("DELETE"))
        .and(path("/v3/orderexecution/orders/ORD12345"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "Orders": [
                {
                    "OrderId": "ORD12345",
                    "Message": "Order has been cancelled"
                }
            ]
        })))
        .mount(&server)
        .await;

    let result = client.cancel_order("ORD12345").await.unwrap();

    let orders = result.orders.unwrap();
    assert_eq!(orders[0].order_id.as_deref(), Some("ORD12345"));
    assert_eq!(
        orders[0].message.as_deref(),
        Some("Order has been cancelled")
    );
}

#[tokio::test]
async fn test_confirm_order() {
    let server = helpers::setup_mock_server().await;
    let mut client = helpers::mock_client(&server);

    Mock::given(method("POST"))
        .and(path("/v3/orderexecution/orderconfirm"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "Orders": [
                {
                    "OrderId": null,
                    "Message": "Estimated cost: $18,530.00"
                }
            ]
        })))
        .mount(&server)
        .await;

    let order = sample_order();
    let result = client.confirm_order(&order).await.unwrap();

    let orders = result.orders.unwrap();
    assert_eq!(orders.len(), 1);
    assert!(
        orders[0]
            .message
            .as_deref()
            .unwrap()
            .contains("Estimated cost")
    );
}

#[tokio::test]
async fn test_confirm_order_group() {
    let server = helpers::setup_mock_server().await;
    let mut client = helpers::mock_client(&server);

    Mock::given(method("POST"))
        .and(path("/v3/orderexecution/ordergroupconfirm"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "Orders": [
                {
                    "OrderId": null,
                    "Message": "Estimated cost for OCO group"
                }
            ]
        })))
        .mount(&server)
        .await;

    let group = OrderGroupRequest {
        group_type: "OCO".to_string(),
        orders: vec![
            sample_limit_order("190.00", "Sell"),
            sample_limit_order("175.00", "Sell"),
        ],
    };

    let result = client.confirm_order_group(&group).await.unwrap();

    let orders = result.orders.unwrap();
    assert_eq!(orders.len(), 1);
    assert!(
        orders[0]
            .message
            .as_deref()
            .unwrap()
            .contains("Estimated cost")
    );
}

#[tokio::test]
async fn test_place_order_group() {
    let server = helpers::setup_mock_server().await;
    let mut client = helpers::mock_client(&server);

    Mock::given(method("POST"))
        .and(path("/v3/orderexecution/ordergroups"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "Orders": [
                {
                    "OrderId": "ORD10001",
                    "Message": "OCO order accepted"
                },
                {
                    "OrderId": "ORD10002",
                    "Message": "OCO order accepted"
                }
            ]
        })))
        .mount(&server)
        .await;

    let group = OrderGroupRequest {
        group_type: "OCO".to_string(),
        orders: vec![
            sample_limit_order("190.00", "Sell"),
            OrderRequest {
                account_id: "123456789".to_string(),
                symbol: "AAPL".to_string(),
                quantity: "100".to_string(),
                order_type: "StopMarket".to_string(),
                trade_action: "Sell".to_string(),
                time_in_force: TimeInForce::gtc(),
                limit_price: None,
                stop_price: Some("175.00".to_string()),
            },
        ],
    };

    let result = client.place_order_group(&group).await.unwrap();

    let orders = result.orders.unwrap();
    assert_eq!(orders.len(), 2);
    assert_eq!(orders[0].order_id.as_deref(), Some("ORD10001"));
    assert_eq!(orders[1].order_id.as_deref(), Some("ORD10002"));
}

#[tokio::test]
async fn test_get_activation_triggers() {
    let server = helpers::setup_mock_server().await;
    let mut client = helpers::mock_client(&server);

    Mock::given(method("GET"))
        .and(path("/v3/orderexecution/activationtriggers"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "ActivationTriggers": [
                {
                    "Key": "SGL",
                    "Name": "Single",
                    "Description": "Single trade trigger"
                },
                {
                    "Key": "DBL",
                    "Name": "Double",
                    "Description": "Double trade trigger"
                }
            ]
        })))
        .mount(&server)
        .await;

    let triggers = client.get_activation_triggers().await.unwrap();

    assert_eq!(triggers.len(), 2);
    assert_eq!(triggers[0].key, "SGL");
    assert_eq!(triggers[0].name, "Single");
    assert_eq!(
        triggers[0].description.as_deref(),
        Some("Single trade trigger")
    );
    assert_eq!(triggers[1].key, "DBL");
}

#[tokio::test]
async fn test_get_routes() {
    let server = helpers::setup_mock_server().await;
    let mut client = helpers::mock_client(&server);

    Mock::given(method("GET"))
        .and(path("/v3/orderexecution/routes"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "Routes": [
                {
                    "Id": "Intelligent",
                    "Name": "Intelligent",
                    "AssetTypes": ["EQ", "OP"]
                },
                {
                    "Id": "AMEX",
                    "Name": "AMEX",
                    "AssetTypes": ["EQ"]
                }
            ]
        })))
        .mount(&server)
        .await;

    let routes = client.get_routes().await.unwrap();

    assert_eq!(routes.len(), 2);
    assert_eq!(routes[0].id, "Intelligent");
    assert_eq!(routes[0].name, "Intelligent");
    assert_eq!(routes[0].asset_types, vec!["EQ", "OP"]);
    assert_eq!(routes[1].id, "AMEX");
    assert_eq!(routes[1].asset_types, vec!["EQ"]);
}