alpaca-sdk 0.1.0

Unofficial Rust SDK for the Alpaca trading, market data, and broker APIs
Documentation
//! The market data API's [corporate actions event stream](https://docs.alpaca.markets/us/reference/subscribetocorporateactionseventssse).
//!
//! One long-lived `text/event-stream` connection carrying every corporate-action
//! mutation — `insert`, `update` and `delete` — across all fifteen action types.
//! The same data is available on demand from
//! [`CorporateActionsClient::get_corporate_actions`](crate::data::CorporateActionsClient::get_corporate_actions);
//! this is the push side of it.
//!
//! No SDK ports it, and there is no captured payload, so the event body stays
//! JSON for the caller to deserialize: every event shares an envelope
//! (`event_id`, `at`, `action`, `region`, `event_type`) and `event_type` selects
//! which of fifteen shapes the `ca` field takes. Claiming a Rust type for all
//! fifteen from the spec alone would be a guess with a struct around it.

use serde::{Deserialize, Serialize};

use crate::sse::EventStreamRequest;
use crate::types::wire::wire_enum;

wire_enum! {
    /// Which markets a corporate actions stream should carry.
    pub enum CorporateActionRegion {
        /// Every event, whichever market.
        All => "all",
        /// US-listed or US-regulated actions only.
        Us => "us",
        /// Everything else.
        NonUs => "non_us",
    }
}

wire_enum! {
    /// The `event_type` discriminator on a corporate actions event.
    ///
    /// Also the filter: naming a subset here narrows the stream to those types.
    /// The values are not the same strings as
    /// [`CorporateActionsType`](crate::data::CorporateActionsType), which the
    /// polled route uses — these carry a `_corporateaction_event` suffix, and
    /// the two lists do not even hold the same members.
    pub enum CorporateActionEventType {
        /// A cash dividend.
        CashDividend => "cash_dividend_corporateaction_event",
        /// A cash merger.
        CashMerger => "cash_merger_corporateaction_event",
        /// A partial call of an equity.
        EquityPartialCall => "equity_partial_call_corporateaction_event",
        /// A forward split.
        ForwardSplit => "forward_split_corporateaction_event",
        /// A name change.
        NameChange => "name_change_corporateaction_event",
        /// A redemption.
        Redemption => "redemption_corporateaction_event",
        /// A reorganization.
        Reorganization => "reorganization_corporateaction_event",
        /// A reverse split.
        ReverseSplit => "reverse_split_corporateaction_event",
        /// A rights distribution.
        RightsDistribution => "rights_distribution_corporateaction_event",
        /// A spin off.
        SpinOff => "spin_off_corporateaction_event",
        /// A merger paid in stock and cash.
        StockAndCashMerger => "stock_and_cash_merger_corporateaction_event",
        /// A stock dividend.
        StockDividend => "stock_dividend_corporateaction_event",
        /// A stock merger.
        StockMerger => "stock_merger_corporateaction_event",
        /// A unit split.
        UnitSplit => "unit_split_corporateaction_event",
        /// A worthless removal.
        WorthlessRemoval => "worthless_removal_corporateaction_event",
    }
}

/// Filters for the corporate actions event stream.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CorporateActionEventsRequest {
    /// The replay window and cursor, shared with every other Alpaca stream
    /// found in the reference sweep.
    #[serde(flatten)]
    pub window: EventStreamRequest,
    /// Only these event types. Sent as one comma-separated `type` parameter.
    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
    pub types: Option<Vec<CorporateActionEventType>>,
    /// Which markets to receive events for. Alpaca defaults this to `all`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub region: Option<CorporateActionRegion>,
}

impl CorporateActionEventsRequest {
    /// A stream of everything, live from now.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Replays from `window` before going live.
    #[must_use]
    pub fn window(mut self, window: EventStreamRequest) -> Self {
        self.window = window;
        self
    }

    /// Only these event types.
    #[must_use]
    pub fn types(mut self, types: Vec<CorporateActionEventType>) -> Self {
        self.types = Some(types);
        self
    }

    /// Only this region.
    #[must_use]
    pub fn region(mut self, region: CorporateActionRegion) -> Self {
        self.region = Some(region);
        self
    }

    /// The filter as query parameters.
    pub(crate) fn query(&self) -> Vec<(&'static str, String)> {
        let mut query = self.window.query();
        if let Some(types) = &self.types {
            query.push((
                "type",
                types
                    .iter()
                    .map(CorporateActionEventType::as_str)
                    .collect::<Vec<_>>()
                    .join(","),
            ));
        }
        if let Some(region) = &self.region {
            query.push(("region", region.as_str().to_owned()));
        }
        query
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn types_render_as_one_comma_separated_parameter() {
        let request = CorporateActionEventsRequest::new()
            .types(vec![
                CorporateActionEventType::CashDividend,
                CorporateActionEventType::StockMerger,
            ])
            .region(CorporateActionRegion::Us);

        assert_eq!(
            request.query(),
            vec![
                (
                    "type",
                    "cash_dividend_corporateaction_event,stock_merger_corporateaction_event"
                        .to_owned()
                ),
                ("region", "us".to_owned()),
            ]
        );
    }

    #[test]
    fn the_event_types_are_not_the_polled_routes_types() {
        // Two lists, two spellings, and not even the same members — mapping one
        // onto the other would send a filter the stream ignores.
        assert_eq!(
            CorporateActionEventType::CashDividend.as_str(),
            "cash_dividend_corporateaction_event"
        );
        assert_eq!(
            crate::data::CorporateActionsType::CashDividend.as_str(),
            "cash_dividend"
        );
    }

    #[test]
    fn an_empty_filter_sends_nothing() {
        assert!(CorporateActionEventsRequest::new().query().is_empty());
    }
}