use self::{
channel::AlpacaChannel,
connection::{AlpacaAttachment, AlpacaConnections, AttachRequest, Slot},
market::AlpacaMarket,
quote::AlpacaQuoteTransformer,
stream::AlpacaStream,
subscription::AlpacaSubResponse,
trade::AlpacaTradeTransformer,
};
use crate::{
Identifier, NoInitialSnapshots,
exchange::{Connector, ExchangeServer, ExchangeSub, StreamSelector},
instrument::InstrumentData,
subscriber::{
Subscribed, Subscriber, mapper::SubscriptionMapper, validator::WebSocketSubValidator,
},
subscription::{
Subscription, SubscriptionKind, SubscriptionMeta, book::OrderBooksL1, quote::Quotes,
trade::PublicTrades,
},
};
use fnv::FnvHashSet;
use futures::{SinkExt, StreamExt};
use rustrade_instrument::exchange::ExchangeId;
use rustrade_integration::{
error::SocketError,
protocol::websocket::{WebSocket, WsMessage},
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use smol_str::SmolStr;
use std::{env, fmt, fmt::Debug, marker::PhantomData, sync::Arc, time::Duration};
use tracing::debug;
use url::Url;
pub mod channel;
pub mod connection;
pub mod market;
pub mod options;
pub mod quote;
pub mod reference;
pub mod rest;
pub mod stream;
pub mod subscription;
pub mod trade;
mod corporate_action;
mod pagination;
pub use reference::{AlpacaStockSplit, CorporateActionsQuery};
pub use rest::{AlpacaRestClient, AlpacaRestError};
pub const WEBSOCKET_URL_IEX: &str = "wss://stream.data.alpaca.markets/v2/iex";
pub const WEBSOCKET_URL_SIP: &str = "wss://stream.data.alpaca.markets/v2/sip";
pub const WEBSOCKET_URL_CRYPTO: &str = "wss://stream.data.alpaca.markets/v1beta3/crypto/us";
pub type AlpacaIex = Alpaca<AlpacaServerIex>;
pub type AlpacaSip = Alpaca<AlpacaServerSip>;
pub type AlpacaCrypto = Alpaca<AlpacaServerCrypto>;
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct Alpaca<Server>(PhantomData<Server>);
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct AlpacaServerIex;
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct AlpacaServerSip;
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct AlpacaServerCrypto;
impl ExchangeServer for AlpacaServerIex {
const ID: ExchangeId = ExchangeId::AlpacaIex;
fn websocket_url() -> &'static str {
WEBSOCKET_URL_IEX
}
}
impl ExchangeServer for AlpacaServerSip {
const ID: ExchangeId = ExchangeId::AlpacaSip;
fn websocket_url() -> &'static str {
WEBSOCKET_URL_SIP
}
}
impl ExchangeServer for AlpacaServerCrypto {
const ID: ExchangeId = ExchangeId::AlpacaCrypto;
fn websocket_url() -> &'static str {
WEBSOCKET_URL_CRYPTO
}
}
impl<Server> Connector for Alpaca<Server>
where
Server: ExchangeServer,
{
const ID: ExchangeId = Server::ID;
type Channel = AlpacaChannel;
type Market = AlpacaMarket;
type Subscriber = AlpacaSubscriber;
type SubValidator = WebSocketSubValidator;
type SubResponse = AlpacaSubResponse;
fn url() -> Result<Url, url::ParseError> {
Url::parse(Server::websocket_url())
}
fn requests(exchange_subs: Vec<ExchangeSub<Self::Channel, Self::Market>>) -> Vec<WsMessage> {
vec![channel_message(
"subscribe",
exchange_subs
.iter()
.map(|sub| (sub.channel, sub.market.as_ref())),
)]
}
}
impl<Instrument, Server> StreamSelector<Instrument, PublicTrades> for Alpaca<Server>
where
Instrument: InstrumentData,
Server: ExchangeServer + Debug + Send + Sync,
{
type SnapFetcher = NoInitialSnapshots;
type Stream = AlpacaStream<AlpacaTradeTransformer<Self, Instrument::Key>>;
}
impl<Instrument, Server> StreamSelector<Instrument, Quotes> for Alpaca<Server>
where
Instrument: InstrumentData,
Server: ExchangeServer + Debug + Send + Sync,
{
type SnapFetcher = NoInitialSnapshots;
type Stream = AlpacaStream<AlpacaQuoteTransformer<Self, Instrument::Key>>;
}
impl<Instrument, Server> StreamSelector<Instrument, OrderBooksL1> for Alpaca<Server>
where
Instrument: InstrumentData,
Server: ExchangeServer + Debug + Send + Sync,
{
type SnapFetcher = NoInitialSnapshots;
type Stream = AlpacaStream<AlpacaQuoteTransformer<Self, Instrument::Key, OrderBooksL1>>;
}
impl<'de, Server> Deserialize<'de> for Alpaca<Server>
where
Server: ExchangeServer,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let input = <String as Deserialize>::deserialize(deserializer)?;
if input.as_str() == Self::ID.as_str() {
Ok(Self::default())
} else {
Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Str(input.as_str()),
&Self::ID.as_str(),
))
}
}
}
impl<Server> Serialize for Alpaca<Server>
where
Server: ExchangeServer,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
serializer.serialize_str(Self::ID.as_str())
}
}
#[derive(Clone)]
pub struct AlpacaCredentials {
api_key: String,
api_secret: String,
}
impl fmt::Debug for AlpacaCredentials {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AlpacaCredentials")
.field("api_key", &self.api_key)
.field("api_secret", &"[REDACTED]")
.finish()
}
}
impl AlpacaCredentials {
pub fn new(api_key: impl Into<String>, api_secret: impl Into<String>) -> Self {
Self {
api_key: api_key.into(),
api_secret: api_secret.into(),
}
}
pub fn from_env() -> Result<Self, SocketError> {
let api_key = env::var("ALPACA_API_KEY")
.map_err(|e| SocketError::Subscribe(format!("ALPACA_API_KEY: {e}")))?;
let api_secret = env::var("ALPACA_SECRET_KEY")
.map_err(|e| SocketError::Subscribe(format!("ALPACA_SECRET_KEY: {e}")))?;
Ok(Self {
api_key,
api_secret,
})
}
}
#[derive(Clone, Debug)]
pub struct AlpacaSubscriber {
connections: Arc<AlpacaConnections>,
}
impl AlpacaSubscriber {
pub fn new(credentials: AlpacaCredentials) -> Self {
Self {
connections: Arc::new(AlpacaConnections::new(credentials)),
}
}
pub fn from_env() -> Result<Self, SocketError> {
Ok(Self::new(AlpacaCredentials::from_env()?))
}
#[cfg(test)]
pub(crate) fn connection_handles(&self) -> usize {
Arc::strong_count(&self.connections)
}
}
impl Subscriber for AlpacaSubscriber {
type SubMapper = crate::subscriber::mapper::WebSocketSubMapper;
type Transport = AlpacaAttachment;
async fn subscribe<Exchange, Instrument, Kind>(
&self,
subscriptions: &[Subscription<Exchange, Instrument, Kind>],
) -> Result<Subscribed<Instrument::Key, Self::Transport>, SocketError>
where
Exchange: Connector + Send + Sync,
Kind: SubscriptionKind + Send + Sync,
Instrument: InstrumentData,
Subscription<Exchange, Instrument, Kind>:
Identifier<Exchange::Channel> + Identifier<Exchange::Market>,
{
let exchange = Exchange::ID;
let url = Exchange::url()?;
debug!(%exchange, %url, ?subscriptions, "subscribing to Alpaca WebSocket");
let slots = requested_slots(exchange, subscriptions)?;
let Some(kind) = subscriptions
.first()
.map(|subscription| subscription.kind.as_str())
else {
return Err(SocketError::Subscribe(format!(
"no subscriptions were given to subscribe to on {exchange}"
)));
};
let SubscriptionMeta {
instrument_map,
ws_subscriptions: _,
} = Self::SubMapper::map::<Exchange, Instrument, Kind>(subscriptions);
let transport = self
.connections
.attach(AttachRequest {
exchange,
url,
kind,
slots,
timeout: Exchange::subscription_timeout(),
})
.await?;
debug!(%exchange, "attached to the Alpaca connection");
Ok(Subscribed {
transport,
map: instrument_map,
buffered_websocket_events: Vec::new(),
})
}
}
fn requested_slots<Exchange, Instrument, Kind>(
exchange: ExchangeId,
subscriptions: &[Subscription<Exchange, Instrument, Kind>],
) -> Result<Vec<Slot>, SocketError>
where
Exchange: Connector,
Subscription<Exchange, Instrument, Kind>:
Identifier<Exchange::Channel> + Identifier<Exchange::Market>,
{
let mut seen = FnvHashSet::default();
let mut slots = Vec::with_capacity(subscriptions.len());
for subscription in subscriptions {
let sub = ExchangeSub::<Exchange::Channel, Exchange::Market>::new(subscription);
let Some(channel) = AlpacaChannel::from_name(sub.channel.as_ref()) else {
return Err(SocketError::Subscribe(format!(
"{exchange} has no Alpaca channel named {}",
sub.channel.as_ref()
)));
};
let slot = Slot {
channel,
symbol: SmolStr::new(sub.market.as_ref()),
};
if seen.insert(slot.clone()) {
slots.push(slot);
}
}
Ok(slots)
}
fn channel_message<'a>(
action: &str,
pairs: impl IntoIterator<Item = (AlpacaChannel, &'a str)>,
) -> WsMessage {
let mut trades: Vec<&str> = Vec::new();
let mut quotes: Vec<&str> = Vec::new();
for (channel, symbol) in pairs {
match channel {
AlpacaChannel::Trades => trades.push(symbol),
AlpacaChannel::Quotes => quotes.push(symbol),
}
}
let mut payload = json!({"action": action});
if !trades.is_empty() {
payload["trades"] = json!(trades);
}
if !quotes.is_empty() {
payload["quotes"] = json!(quotes);
}
WsMessage::text(payload.to_string())
}
async fn alpaca_authenticate(
ws: &mut WebSocket,
credentials: &AlpacaCredentials,
) -> Result<(), SocketError> {
let auth_msg = json!({
"action": "auth",
"key": credentials.api_key,
"secret": credentials.api_secret,
})
.to_string();
ws.send(WsMessage::text(auth_msg))
.await
.map_err(|e| SocketError::WebSocket(Box::new(e)))?;
tokio::time::timeout(Duration::from_secs(10), async {
loop {
match ws.next().await {
Some(Ok(WsMessage::Text(text))) => {
if let Some(result) = check_alpaca_auth_response(text.as_str()) {
return result;
}
}
Some(Ok(WsMessage::Binary(bytes))) => {
if let Ok(text) = std::str::from_utf8(&bytes)
&& let Some(result) = check_alpaca_auth_response(text)
{
return result;
}
}
Some(Err(e)) => {
return Err(SocketError::WebSocket(Box::new(e)));
}
None => {
return Err(SocketError::Subscribe(
"WebSocket closed before auth response".to_owned(),
));
}
Some(Ok(WsMessage::Close(frame))) => {
return Err(SocketError::Subscribe(format!(
"WebSocket closed during auth: {frame:?}"
)));
}
_ => {}
}
}
})
.await
.map_err(|_| SocketError::Subscribe("Alpaca auth timeout (10s)".to_owned()))?
}
fn check_alpaca_auth_response(text: &str) -> Option<Result<(), SocketError>> {
#[derive(Deserialize)]
struct AuthMsg<'a> {
#[serde(rename = "T")]
msg_type: &'a str,
#[serde(default)]
msg: Option<&'a str>,
}
let messages: Vec<AuthMsg<'_>> = serde_json::from_str(text).ok()?;
for msg in &messages {
match (msg.msg_type, msg.msg) {
("success", Some("authenticated")) => return Some(Ok(())),
("error", _) => {
return Some(Err(SocketError::Subscribe(format!(
"Alpaca auth failed: {}",
msg.msg.unwrap_or("unknown error")
))));
}
_ => {}
}
}
None
}