#![forbid(unsafe_code)]
#![deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
#![warn(
unused,
clippy::cognitive_complexity,
unused_crate_dependencies,
unused_extern_crates,
clippy::unused_self,
clippy::useless_let_if_seq,
missing_debug_implementations,
rust_2018_idioms
)]
#![allow(clippy::type_complexity, clippy::too_many_arguments, type_alias_bounds)]
#[cfg(test)]
use serial_test as _;
#[cfg(test)]
use temp_env as _;
#[cfg(test)]
use tracing_subscriber as _;
#[cfg(test)]
use wiremock as _;
use crate::{
balance::{AssetBalance, AssetBalanceUpdate},
error::StreamTerminationReason,
order::{Order, OrderSnapshot, request::OrderResponseCancel},
position::Position,
trade::Trade,
};
use chrono::{DateTime, Utc};
use derive_more::{Constructor, From};
use order::state::OrderState;
use rust_decimal::Decimal;
use rustrade_instrument::{
asset::{AssetIndex, name::AssetNameExchange},
exchange::{ExchangeId, ExchangeIndex},
instrument::{InstrumentIndex, name::InstrumentNameExchange},
};
use rustrade_integration::collection::snapshot::Snapshot;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
pub mod balance;
pub mod client;
pub mod error;
pub mod exchange;
pub mod fee;
pub use fee::{FeeModel, FeeModelConfig, PerContractFeeModel, PercentageFeeModel, ZeroFeeModel};
pub mod fill;
pub use fill::{BidAskFillModel, FillModel, LastPriceFillModel, MidpointFillModel, SimFillConfig};
pub mod indexer;
pub mod map;
pub mod order;
pub mod position;
pub mod trade;
pub type UnindexedAccountEvent =
AccountEvent<ExchangeId, AssetNameExchange, InstrumentNameExchange>;
pub type UnindexedAccountSnapshot =
AccountSnapshot<ExchangeId, AssetNameExchange, InstrumentNameExchange>;
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct AccountEvent<
ExchangeKey = ExchangeIndex,
AssetKey = AssetIndex,
InstrumentKey = InstrumentIndex,
> {
pub exchange: ExchangeKey,
pub kind: AccountEventKind<ExchangeKey, AssetKey, InstrumentKey>,
}
impl<ExchangeKey, AssetKey, InstrumentKey> AccountEvent<ExchangeKey, AssetKey, InstrumentKey> {
pub fn new<K>(exchange: ExchangeKey, kind: K) -> Self
where
K: Into<AccountEventKind<ExchangeKey, AssetKey, InstrumentKey>>,
{
Self {
exchange,
kind: kind.into(),
}
}
pub(crate) fn stream_terminated(
exchange: ExchangeKey,
reason: StreamTerminationReason,
) -> Self {
Self {
exchange,
kind: AccountEventKind::StreamTerminated(reason),
}
}
}
pub(crate) fn emit_stream_terminated(
tx: &mpsc::UnboundedSender<UnindexedAccountEvent>,
exchange: ExchangeId,
reason: StreamTerminationReason,
) {
let _ = tx.send(UnindexedAccountEvent::stream_terminated(exchange, reason));
}
#[cfg(any(feature = "alpaca", feature = "binance", feature = "hyperliquid"))]
pub(crate) fn parse_env_bool(value: &str) -> Option<bool> {
let trimmed = value.trim();
if trimmed.eq_ignore_ascii_case("true") {
Some(true)
} else if trimmed.eq_ignore_ascii_case("false") {
Some(false)
} else {
None
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, From)]
#[non_exhaustive]
pub enum AccountEventKind<ExchangeKey, AssetKey, InstrumentKey> {
Snapshot(AccountSnapshot<ExchangeKey, AssetKey, InstrumentKey>),
BalanceSnapshot(Snapshot<AssetBalance<AssetKey>>),
BalanceStreamUpdate(Snapshot<AssetBalanceUpdate<AssetKey>>),
InstrumentBalanceUpdate(InstrumentBalanceUpdate<AssetKey, InstrumentKey>),
OrderSnapshot(Snapshot<Order<ExchangeKey, InstrumentKey, OrderState<AssetKey, InstrumentKey>>>),
OrderCancelled(OrderResponseCancel<ExchangeKey, AssetKey, InstrumentKey>),
Trade(Trade<AssetKey, InstrumentKey>),
StreamTerminated(StreamTerminationReason),
}
impl<ExchangeKey, AssetKey, InstrumentKey> AccountEvent<ExchangeKey, AssetKey, InstrumentKey>
where
AssetKey: Eq,
InstrumentKey: Eq,
{
pub fn snapshot(self) -> Option<AccountSnapshot<ExchangeKey, AssetKey, InstrumentKey>> {
match self.kind {
AccountEventKind::Snapshot(snapshot) => Some(snapshot),
_ => None,
}
}
}
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, Constructor,
)]
pub struct AccountSnapshot<
ExchangeKey = ExchangeIndex,
AssetKey = AssetIndex,
InstrumentKey = InstrumentIndex,
> {
pub exchange: ExchangeKey,
pub balances: Vec<AssetBalance<AssetKey>>,
pub instruments: Vec<InstrumentAccountSnapshot<ExchangeKey, AssetKey, InstrumentKey>>,
}
fn none_option<T>() -> Option<T> {
None
}
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, Constructor,
)]
pub struct InstrumentAccountSnapshot<
ExchangeKey = ExchangeIndex,
AssetKey = AssetIndex,
InstrumentKey = InstrumentIndex,
> {
pub instrument: InstrumentKey,
#[serde(default = "Vec::new")]
pub orders: Vec<OrderSnapshot<ExchangeKey, AssetKey, InstrumentKey>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub position: Option<Position>,
#[serde(default = "none_option", skip_serializing_if = "Option::is_none")]
pub isolated: Option<IsolatedInstrumentState<AssetKey>>,
}
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, Constructor,
)]
pub struct IsolatedInstrumentState<AssetKey = AssetIndex> {
pub base: AssetBalance<AssetKey>,
pub quote: AssetBalance<AssetKey>,
pub risk: IsolatedMarginRisk,
}
#[derive(
Debug,
Copy,
Clone,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Default,
Deserialize,
Serialize,
Constructor,
)]
pub struct IsolatedMarginRisk {
pub margin_level: Option<Decimal>,
pub margin_ratio: Option<Decimal>,
pub liquidation_price: Option<Decimal>,
}
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, Constructor,
)]
pub struct InstrumentBalanceUpdate<AssetKey = AssetIndex, InstrumentKey = InstrumentIndex> {
pub instrument: InstrumentKey,
pub base: AssetBalanceUpdate<AssetKey>,
pub quote: AssetBalanceUpdate<AssetKey>,
}
impl<ExchangeKey, AssetKey, InstrumentKey> AccountSnapshot<ExchangeKey, AssetKey, InstrumentKey> {
pub fn time_most_recent(&self) -> Option<DateTime<Utc>> {
let order_times = self.instruments.iter().flat_map(|instrument| {
instrument
.orders
.iter()
.filter_map(|order| order.state.time_exchange())
});
let balance_times = self.balances.iter().map(|balance| balance.time_exchange);
order_times.chain(balance_times).max()
}
pub fn assets(&self) -> impl Iterator<Item = &AssetKey> {
self.balances.iter().map(|balance| &balance.asset)
}
pub fn instruments(&self) -> impl Iterator<Item = &InstrumentKey> {
self.instruments.iter().map(|snapshot| &snapshot.instrument)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
use super::*;
#[test]
fn emit_stream_terminated_delivers_event_in_band() {
let (tx, mut rx) = mpsc::unbounded_channel::<UnindexedAccountEvent>();
let reason = StreamTerminationReason::ReconnectBudgetExhausted {
attempts: 7,
last_error: "socket reset".to_string(),
};
emit_stream_terminated(&tx, ExchangeId::BinanceSpot, reason.clone());
let event = rx.try_recv().expect("expected a terminal event");
assert_eq!(event.exchange, ExchangeId::BinanceSpot);
assert!(
matches!(event.kind, AccountEventKind::StreamTerminated(r) if r == reason),
"expected StreamTerminated carrying the supplied reason",
);
}
#[test]
fn emit_stream_terminated_is_noop_when_receiver_dropped() {
let (tx, rx) = mpsc::unbounded_channel::<UnindexedAccountEvent>();
drop(rx);
emit_stream_terminated(
&tx,
ExchangeId::HyperliquidPerp,
StreamTerminationReason::Error("stream closed".to_string()),
);
}
}