alpaca-mock 0.26.0

Executable mock server for alpaca-rust trading and market-data flows
Documentation
use axum::{
    Json,
    extract::{Extension, Path, Query, State},
};
use rust_decimal::Decimal;
use serde::Deserialize;

use alpaca_trade::orders::{
    CancelAllOrderResult, OptionLegRequest, Order, OrderClass, OrderSide, OrderType,
    PositionIntent, QueryOrderStatus, SortDirection, StopLoss, TakeProfit, TimeInForce,
};

use crate::auth::{AuthenticatedAccount, MockHttpError};
use crate::state::{
    CreateOrderInput, ListOrdersFilter, MockServerState, MockStateError, ReplaceOrderInput,
};

type RouteResult<T> = Result<T, MockHttpError>;

impl From<MockStateError> for MockHttpError {
    fn from(error: MockStateError) -> Self {
        match error {
            MockStateError::NotFound(message) => Self::not_found(message),
            MockStateError::Conflict(message) => Self::conflict(message),
            MockStateError::MarketDataUnavailable(message) => Self::internal(message),
        }
    }
}

#[derive(Debug, Deserialize)]
pub(crate) struct ByClientOrderIdQuery {
    client_order_id: String,
}

#[derive(Debug, Deserialize, Default)]
pub(crate) struct ListOrdersQuery {
    status: Option<QueryOrderStatus>,
    limit: Option<u32>,
    after: Option<String>,
    until: Option<String>,
    direction: Option<SortDirection>,
    nested: Option<bool>,
    symbols: Option<String>,
    side: Option<OrderSide>,
    asset_class: Option<String>,
}

#[derive(Debug, Deserialize)]
pub(crate) struct CreateOrderBody {
    symbol: Option<String>,
    #[serde(
        default,
        deserialize_with = "alpaca_core::decimal::deserialize_option_decimal_from_string_or_number"
    )]
    qty: Option<Decimal>,
    #[serde(
        default,
        deserialize_with = "alpaca_core::decimal::deserialize_option_decimal_from_string_or_number"
    )]
    notional: Option<Decimal>,
    side: Option<OrderSide>,
    #[serde(rename = "type")]
    r#type: Option<OrderType>,
    time_in_force: Option<TimeInForce>,
    #[serde(
        default,
        deserialize_with = "alpaca_core::decimal::deserialize_option_decimal_from_string_or_number"
    )]
    limit_price: Option<Decimal>,
    #[serde(
        default,
        deserialize_with = "alpaca_core::decimal::deserialize_option_decimal_from_string_or_number"
    )]
    stop_price: Option<Decimal>,
    #[serde(
        default,
        deserialize_with = "alpaca_core::decimal::deserialize_option_decimal_from_string_or_number"
    )]
    trail_price: Option<Decimal>,
    #[serde(
        default,
        deserialize_with = "alpaca_core::decimal::deserialize_option_decimal_from_string_or_number"
    )]
    trail_percent: Option<Decimal>,
    extended_hours: Option<bool>,
    client_order_id: Option<String>,
    order_class: Option<OrderClass>,
    take_profit: Option<TakeProfit>,
    stop_loss: Option<StopLoss>,
    legs: Option<Vec<OptionLegRequest>>,
    position_intent: Option<PositionIntent>,
}

#[derive(Debug, Deserialize, Default)]
pub(crate) struct ReplaceOrderBody {
    #[serde(
        default,
        deserialize_with = "alpaca_core::decimal::deserialize_option_decimal_from_string_or_number"
    )]
    qty: Option<Decimal>,
    time_in_force: Option<TimeInForce>,
    #[serde(
        default,
        deserialize_with = "alpaca_core::decimal::deserialize_option_decimal_from_string_or_number"
    )]
    limit_price: Option<Decimal>,
    #[serde(
        default,
        deserialize_with = "alpaca_core::decimal::deserialize_option_decimal_from_string_or_number"
    )]
    stop_price: Option<Decimal>,
    #[serde(
        default,
        deserialize_with = "alpaca_core::decimal::deserialize_option_decimal_from_string_or_number"
    )]
    trail: Option<Decimal>,
    client_order_id: Option<String>,
}

pub(crate) async fn orders_create(
    State(state): State<MockServerState>,
    Extension(account): Extension<AuthenticatedAccount>,
    Json(body): Json<CreateOrderBody>,
) -> RouteResult<Json<Order>> {
    let order = state
        .create_order(
            &account.api_key,
            CreateOrderInput {
                symbol: body.symbol,
                qty: body.qty,
                notional: body.notional,
                side: body.side,
                order_type: body.r#type,
                time_in_force: body.time_in_force,
                limit_price: body.limit_price,
                stop_price: body.stop_price,
                trail_price: body.trail_price,
                trail_percent: body.trail_percent,
                extended_hours: body.extended_hours,
                client_order_id: body.client_order_id,
                order_class: body.order_class,
                position_intent: body.position_intent,
                take_profit: body.take_profit,
                stop_loss: body.stop_loss,
                legs: body.legs,
            },
        )
        .await?;
    Ok(Json(order))
}

pub(crate) async fn orders_list(
    State(state): State<MockServerState>,
    Extension(account): Extension<AuthenticatedAccount>,
    Query(query): Query<ListOrdersQuery>,
) -> RouteResult<Json<Vec<Order>>> {
    let _ = (
        query.limit,
        query.after,
        query.until,
        query.direction,
        query.nested,
    );
    let symbols = query.symbols.map(|symbols| {
        symbols
            .split(',')
            .map(|symbol| symbol.trim().to_owned())
            .filter(|symbol| !symbol.is_empty())
            .collect::<Vec<_>>()
    });

    Ok(Json(state.list_orders(
        &account.api_key,
        ListOrdersFilter {
            status: query.status.map(|status| status.to_string()),
            symbols,
            side: query.side,
            asset_class: query.asset_class,
            nested: query.nested,
        },
    )))
}

pub(crate) async fn orders_get(
    State(state): State<MockServerState>,
    Extension(account): Extension<AuthenticatedAccount>,
    Path(order_id): Path<String>,
) -> RouteResult<Json<Order>> {
    let order = state
        .get_order(&account.api_key, &order_id)
        .ok_or_else(|| MockHttpError::not_found(format!("order {order_id} was not found")))?;
    Ok(Json(order))
}

pub(crate) async fn orders_get_by_client_order_id(
    State(state): State<MockServerState>,
    Extension(account): Extension<AuthenticatedAccount>,
    Query(query): Query<ByClientOrderIdQuery>,
) -> RouteResult<Json<Order>> {
    let order = state
        .get_by_client_order_id(&account.api_key, &query.client_order_id)
        .ok_or_else(|| {
            MockHttpError::not_found(format!(
                "client_order_id {} was not found",
                query.client_order_id
            ))
        })?;
    Ok(Json(order))
}

pub(crate) async fn orders_replace(
    State(state): State<MockServerState>,
    Extension(account): Extension<AuthenticatedAccount>,
    Path(order_id): Path<String>,
    Json(body): Json<ReplaceOrderBody>,
) -> RouteResult<Json<Order>> {
    let order = state
        .replace_order(
            &account.api_key,
            &order_id,
            ReplaceOrderInput {
                qty: body.qty,
                time_in_force: body.time_in_force,
                limit_price: body.limit_price,
                stop_price: body.stop_price,
                trail: body.trail,
                client_order_id: body.client_order_id,
            },
        )
        .await?;
    Ok(Json(order))
}

pub(crate) async fn orders_cancel(
    State(state): State<MockServerState>,
    Extension(account): Extension<AuthenticatedAccount>,
    Path(order_id): Path<String>,
) -> RouteResult<axum::http::StatusCode> {
    state.cancel_order(&account.api_key, &order_id)?;
    Ok(axum::http::StatusCode::NO_CONTENT)
}

pub(crate) async fn orders_cancel_all(
    State(state): State<MockServerState>,
    Extension(account): Extension<AuthenticatedAccount>,
) -> RouteResult<Json<Vec<CancelAllOrderResult>>> {
    Ok(Json(state.cancel_all_orders(&account.api_key)))
}