alpaca-mock 0.26.0

Executable mock server for alpaca-rust trading and market-data flows
Documentation
use rust_decimal::Decimal;

use alpaca_trade::{
    activities::Activity,
    orders::{Order, OrderSide, OrderStatus},
};

#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ActivityEventKind {
    New,
    Filled,
    Canceled,
    Replaced,
    PositionClosed,
    Exercised,
    DoNotExercise,
}

impl ActivityEventKind {
    pub(crate) fn public_activity_type(&self) -> Option<&'static str> {
        match self {
            Self::Filled => Some("FILL"),
            Self::New
            | Self::Canceled
            | Self::Replaced
            | Self::PositionClosed
            | Self::Exercised
            | Self::DoNotExercise => None,
        }
    }
}

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ActivityEvent {
    pub(crate) sequence: u64,
    pub(crate) kind: ActivityEventKind,
    pub(crate) order_id: String,
    pub(crate) client_order_id: String,
    pub(crate) related_order_id: Option<String>,
    pub(crate) status: Option<OrderStatus>,
    pub(crate) symbol: String,
    pub(crate) asset_class: String,
    pub(crate) occurred_at: String,
    pub(crate) cash_delta: Decimal,
    pub(crate) r#type: Option<String>,
    pub(crate) price: Option<Decimal>,
    pub(crate) qty: Option<Decimal>,
    pub(crate) side: Option<OrderSide>,
    pub(crate) leaves_qty: Option<Decimal>,
    pub(crate) cum_qty: Option<Decimal>,
}

impl ActivityEvent {
    #[allow(clippy::too_many_arguments)]
    pub(crate) fn new(
        sequence: u64,
        kind: ActivityEventKind,
        order_id: String,
        client_order_id: String,
        related_order_id: Option<String>,
        status: Option<OrderStatus>,
        symbol: String,
        asset_class: String,
        occurred_at: String,
        cash_delta: Decimal,
    ) -> Self {
        Self {
            sequence,
            kind,
            order_id,
            client_order_id,
            related_order_id,
            status,
            symbol,
            asset_class,
            occurred_at,
            cash_delta,
            r#type: None,
            price: None,
            qty: None,
            side: None,
            leaves_qty: None,
            cum_qty: None,
        }
    }

    pub(crate) fn with_fill_order(mut self, order: &Order, side: &OrderSide) -> Self {
        self.r#type = Some("fill".to_owned());
        self.price = order.filled_avg_price;
        self.qty = Some(order.filled_qty);
        self.side = Some(side.clone());
        self.leaves_qty = Some(Decimal::ZERO);
        self.cum_qty = Some(order.filled_qty);
        self
    }
}

pub(crate) fn project_activity(event: &ActivityEvent) -> Option<Activity> {
    Some(Activity {
        id: format!("mock-activity-{}", event.sequence),
        activity_type: event.kind.public_activity_type()?.to_owned(),
        transaction_time: Some(event.occurred_at.clone()),
        r#type: event.r#type.clone(),
        price: event.price,
        qty: event.qty,
        side: event.side.as_ref().map(order_side_name),
        symbol: Some(event.symbol.clone()),
        leaves_qty: event.leaves_qty,
        order_id: Some(event.order_id.clone()),
        cum_qty: event.cum_qty,
        order_status: event.status.as_ref().map(order_status_name),
        extra: Default::default(),
    })
}

pub(crate) fn matches_activity_type(event: &ActivityEvent, filter: &str) -> bool {
    event
        .kind
        .public_activity_type()
        .is_some_and(|activity_type| activity_type.eq_ignore_ascii_case(filter))
}

pub(crate) fn is_public_activity(event: &ActivityEvent) -> bool {
    event.kind.public_activity_type().is_some()
}

fn order_side_name(side: &OrderSide) -> String {
    serde_json::to_value(side)
        .ok()
        .and_then(|value| value.as_str().map(str::to_owned))
        .unwrap_or_else(|| format!("{side:?}"))
}

fn order_status_name(status: &OrderStatus) -> String {
    serde_json::to_value(status)
        .ok()
        .and_then(|value| value.as_str().map(str::to_owned))
        .unwrap_or_else(|| format!("{status:?}"))
}