use std::cmp::Reverse;
use std::collections::{BTreeMap, HashMap};
use std::fmt::Write;
use std::str::FromStr;
use chrono::{DateTime, Duration, Utc};
use crc32fast::Hasher;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
mod identifiers;
pub use identifiers::{AssetId, ExchangeId, IdentifierParseError, Symbol};
pub type Price = Decimal;
pub type Quantity = Decimal;
pub type OrderId = String;
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InstrumentKind {
Spot,
LinearPerpetual,
InversePerpetual,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Instrument {
pub symbol: Symbol,
pub base: AssetId,
pub quote: AssetId,
pub kind: InstrumentKind,
pub settlement_currency: AssetId,
pub tick_size: Price,
pub lot_size: Quantity,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Cash {
pub currency: AssetId,
pub quantity: Quantity,
pub conversion_rate: Price,
}
impl Cash {
#[must_use]
pub fn value(&self) -> Price {
self.quantity * self.conversion_rate
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct CashBook(pub HashMap<AssetId, Cash>);
impl CashBook {
#[must_use]
pub fn new() -> Self {
Self(HashMap::new())
}
pub fn upsert(&mut self, cash: Cash) {
self.0.insert(cash.currency, cash);
}
pub fn adjust(&mut self, currency: impl Into<AssetId>, delta: Quantity) -> Quantity {
let currency = currency.into();
let entry = self.0.entry(currency).or_insert(Cash {
currency,
quantity: Decimal::ZERO,
conversion_rate: Decimal::ZERO,
});
entry.quantity += delta;
entry.quantity
}
pub fn update_conversion_rate(&mut self, currency: impl Into<AssetId>, rate: Price) {
let currency = currency.into();
let entry = self.0.entry(currency).or_insert(Cash {
currency,
quantity: Decimal::ZERO,
conversion_rate: Decimal::ZERO,
});
entry.conversion_rate = rate;
}
#[must_use]
pub fn total_value(&self) -> Price {
self.0.values().map(Cash::value).sum()
}
#[must_use]
pub fn get(&self, currency: impl Into<AssetId>) -> Option<&Cash> {
let currency = currency.into();
self.0.get(¤cy)
}
#[must_use]
pub fn get_mut(&mut self, currency: impl Into<AssetId>) -> Option<&mut Cash> {
let currency = currency.into();
self.0.get_mut(¤cy)
}
pub fn iter(&self) -> impl Iterator<Item = (&AssetId, &Cash)> {
self.0.iter()
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum ExecutionHint {
Twap { duration: Duration },
Vwap {
duration: Duration,
#[serde(default)]
participation_rate: Option<Decimal>,
},
IcebergSimulated {
display_size: Quantity,
#[serde(default)]
limit_offset_bps: Option<Decimal>,
},
PeggedBest {
offset_bps: Decimal,
#[serde(default)]
clip_size: Option<Quantity>,
#[serde(default)]
refresh_secs: Option<u64>,
#[serde(default)]
min_chase_distance: Option<Price>,
},
Sniper {
trigger_price: Price,
#[serde(default)]
timeout: Option<Duration>,
},
TrailingStop {
activation_price: Price,
callback_rate: Decimal,
},
Plugin {
name: String,
#[serde(default)]
params: Value,
},
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ExitStrategy {
StandardZScore { exit_z: Decimal },
HardTimeStop { max_duration_secs: u64 },
HalfLifeTimeStop {
half_life_candles: u32,
multiplier: Decimal,
},
DecayingThreshold {
initial_exit_z: Decimal,
decay_rate_per_hour: Decimal,
},
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum Side {
Buy,
Sell,
}
impl Side {
#[must_use]
pub fn inverse(self) -> Self {
match self {
Self::Buy => Self::Sell,
Self::Sell => Self::Buy,
}
}
#[must_use]
pub fn as_i8(self) -> i8 {
match self {
Self::Buy => 1,
Self::Sell => -1,
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum OrderType {
Market,
Limit,
StopMarket,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum TimeInForce {
GoodTilCanceled,
ImmediateOrCancel,
FillOrKill,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum Interval {
OneSecond,
OneMinute,
FiveMinutes,
FifteenMinutes,
OneHour,
FourHours,
OneDay,
}
impl Interval {
#[must_use]
pub fn as_duration(self) -> Duration {
match self {
Self::OneSecond => Duration::seconds(1),
Self::OneMinute => Duration::minutes(1),
Self::FiveMinutes => Duration::minutes(5),
Self::FifteenMinutes => Duration::minutes(15),
Self::OneHour => Duration::hours(1),
Self::FourHours => Duration::hours(4),
Self::OneDay => Duration::days(1),
}
}
#[must_use]
pub fn to_bybit(self) -> &'static str {
match self {
Self::OneSecond => "1",
Self::OneMinute => "1",
Self::FiveMinutes => "5",
Self::FifteenMinutes => "15",
Self::OneHour => "60",
Self::FourHours => "240",
Self::OneDay => "D",
}
}
pub fn to_binance(self) -> &'static str {
match self {
Self::OneSecond => "1s",
Self::OneMinute => "1m",
Self::FiveMinutes => "5m",
Self::FifteenMinutes => "15m",
Self::OneHour => "1h",
Self::FourHours => "4h",
Self::OneDay => "1d",
}
}
}
impl FromStr for Interval {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.to_lowercase().as_str() {
"1s" | "1sec" | "1second" | "1" => Ok(Self::OneSecond),
"1m" | "1min" | "1minute" => Ok(Self::OneMinute),
"5m" | "5min" | "5minutes" => Ok(Self::FiveMinutes),
"15m" | "15min" | "15minutes" => Ok(Self::FifteenMinutes),
"1h" | "60m" | "1hour" | "60" => Ok(Self::OneHour),
"4h" | "240m" | "4hours" | "240" => Ok(Self::FourHours),
"1d" | "day" | "d" => Ok(Self::OneDay),
other => Err(format!("unsupported interval '{other}'")),
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Tick {
pub symbol: Symbol,
pub price: Price,
pub size: Quantity,
pub side: Side,
pub exchange_timestamp: DateTime<Utc>,
pub received_at: DateTime<Utc>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Candle {
pub symbol: Symbol,
pub interval: Interval,
pub open: Price,
pub high: Price,
pub low: Price,
pub close: Price,
pub volume: Quantity,
pub timestamp: DateTime<Utc>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct OrderBookLevel {
pub price: Price,
pub size: Quantity,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct OrderBook {
pub symbol: Symbol,
pub bids: Vec<OrderBookLevel>,
pub asks: Vec<OrderBookLevel>,
pub timestamp: DateTime<Utc>,
#[serde(default)]
pub exchange_checksum: Option<u32>,
#[serde(default)]
pub local_checksum: Option<u32>,
}
impl OrderBook {
#[must_use]
pub fn best_bid(&self) -> Option<&OrderBookLevel> {
self.bids.first()
}
#[must_use]
pub fn best_ask(&self) -> Option<&OrderBookLevel> {
self.asks.first()
}
#[must_use]
pub fn imbalance(&self, depth: usize) -> Option<Decimal> {
let depth = depth.max(1);
let bid_vol: Decimal = self.bids.iter().take(depth).map(|level| level.size).sum();
let ask_vol: Decimal = self.asks.iter().take(depth).map(|level| level.size).sum();
let denom = bid_vol + ask_vol;
if denom.is_zero() {
None
} else {
Some((bid_vol - ask_vol) / denom)
}
}
#[must_use]
pub fn computed_checksum(&self, depth: Option<usize>) -> u32 {
let mut lob = LocalOrderBook::new();
let bids = self
.bids
.iter()
.map(|level| (level.price, level.size))
.collect::<Vec<_>>();
let asks = self
.asks
.iter()
.map(|level| (level.price, level.size))
.collect::<Vec<_>>();
lob.load_snapshot(&bids, &asks);
let depth = depth.unwrap_or_else(|| bids.len().max(asks.len()).max(1));
lob.checksum(depth)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct DepthUpdate {
pub symbol: Symbol,
pub bids: Vec<OrderBookLevel>,
pub asks: Vec<OrderBookLevel>,
pub timestamp: DateTime<Utc>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct LocalOrderBook {
bids: BTreeMap<Reverse<Price>, Quantity>,
asks: BTreeMap<Price, Quantity>,
}
impl LocalOrderBook {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn load_snapshot(&mut self, bids: &[(Price, Quantity)], asks: &[(Price, Quantity)]) {
self.bids.clear();
self.asks.clear();
for &(price, qty) in bids {
self.add_order(Side::Buy, price, qty);
}
for &(price, qty) in asks {
self.add_order(Side::Sell, price, qty);
}
}
pub fn add_order(&mut self, side: Side, price: Price, quantity: Quantity) {
if quantity <= Decimal::ZERO {
return;
}
match side {
Side::Buy => {
let key = Reverse(price);
let entry = self.bids.entry(key).or_insert(Decimal::ZERO);
*entry += quantity;
}
Side::Sell => {
let entry = self.asks.entry(price).or_insert(Decimal::ZERO);
*entry += quantity;
}
}
}
pub fn apply_delta(&mut self, side: Side, price: Price, quantity: Quantity) {
if quantity <= Decimal::ZERO {
self.clear_level(side, price);
return;
}
match side {
Side::Buy => {
self.bids.insert(Reverse(price), quantity);
}
Side::Sell => {
self.asks.insert(price, quantity);
}
}
}
pub fn apply_deltas(&mut self, bids: &[(Price, Quantity)], asks: &[(Price, Quantity)]) {
for &(price, qty) in bids {
self.apply_delta(Side::Buy, price, qty);
}
for &(price, qty) in asks {
self.apply_delta(Side::Sell, price, qty);
}
}
pub fn remove_order(&mut self, side: Side, price: Price, quantity: Quantity) {
if quantity <= Decimal::ZERO {
return;
}
match side {
Side::Buy => {
let key = Reverse(price);
if let Some(level) = self.bids.get_mut(&key) {
*level -= quantity;
if *level <= Decimal::ZERO {
self.bids.remove(&key);
}
}
}
Side::Sell => {
if let Some(level) = self.asks.get_mut(&price) {
*level -= quantity;
if *level <= Decimal::ZERO {
self.asks.remove(&price);
}
}
}
}
}
pub fn clear_level(&mut self, side: Side, price: Price) {
match side {
Side::Buy => {
self.bids.remove(&Reverse(price));
}
Side::Sell => {
self.asks.remove(&price);
}
}
}
#[must_use]
pub fn best_bid(&self) -> Option<(Price, Quantity)> {
self.bids.iter().next().map(|(price, qty)| (price.0, *qty))
}
#[must_use]
pub fn best_ask(&self) -> Option<(Price, Quantity)> {
self.asks.iter().next().map(|(price, qty)| (*price, *qty))
}
pub fn bids(&self) -> impl Iterator<Item = (Price, Quantity)> + '_ {
self.bids.iter().map(|(price, qty)| (price.0, *qty))
}
pub fn asks(&self) -> impl Iterator<Item = (Price, Quantity)> + '_ {
self.asks.iter().map(|(price, qty)| (*price, *qty))
}
pub fn take_liquidity(
&mut self,
aggressive_side: Side,
mut quantity: Quantity,
) -> Vec<(Price, Quantity)> {
let mut fills = Vec::new();
while quantity > Decimal::ZERO {
let (price, available) = match aggressive_side {
Side::Buy => match self.best_ask() {
Some(level) => level,
None => break,
},
Side::Sell => match self.best_bid() {
Some(level) => level,
None => break,
},
};
let traded = quantity.min(available);
let contra_side = aggressive_side.inverse();
self.remove_order(contra_side, price, traded);
fills.push((price, traded));
quantity -= traded;
}
fills
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.bids.is_empty() && self.asks.is_empty()
}
#[must_use]
pub fn checksum(&self, depth: usize) -> u32 {
if depth == 0 {
return 0;
}
let mut buffer = String::new();
let mut first = true;
for (price, size) in self.bids().take(depth) {
if !first {
buffer.push(':');
}
first = false;
write!(buffer, "{}:{}", price.normalize(), size.normalize()).ok();
}
for (price, size) in self.asks().take(depth) {
if !first {
buffer.push(':');
}
first = false;
write!(buffer, "{}:{}", price.normalize(), size.normalize()).ok();
}
let mut hasher = Hasher::new();
hasher.update(buffer.as_bytes());
hasher.finalize()
}
pub fn bid_levels(&self, depth: usize) -> Vec<(Price, Quantity)> {
self.bids().take(depth).collect()
}
pub fn ask_levels(&self, depth: usize) -> Vec<(Price, Quantity)> {
self.asks().take(depth).collect()
}
#[must_use]
pub fn volume_at_level(&self, side: Side, price: Price) -> Quantity {
match side {
Side::Buy => self
.bids
.get(&Reverse(price))
.copied()
.unwrap_or(Decimal::ZERO),
Side::Sell => self.asks.get(&price).copied().unwrap_or(Decimal::ZERO),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct OrderRequest {
pub symbol: Symbol,
pub side: Side,
pub order_type: OrderType,
pub quantity: Quantity,
pub price: Option<Price>,
pub trigger_price: Option<Price>,
pub time_in_force: Option<TimeInForce>,
pub client_order_id: Option<String>,
pub take_profit: Option<Price>,
pub stop_loss: Option<Price>,
pub display_quantity: Option<Quantity>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct OrderUpdateRequest {
pub order_id: OrderId,
pub symbol: Symbol,
pub side: Side,
pub new_price: Option<Price>,
pub new_quantity: Option<Quantity>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum OrderStatus {
PendingNew,
Accepted,
PartiallyFilled,
Filled,
Canceled,
Rejected,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Order {
pub id: OrderId,
pub request: OrderRequest,
pub status: OrderStatus,
pub filled_quantity: Quantity,
pub avg_fill_price: Option<Price>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Fill {
pub order_id: OrderId,
pub symbol: Symbol,
pub side: Side,
pub fill_price: Price,
pub fill_quantity: Quantity,
pub fee: Option<Price>,
#[serde(default)]
pub fee_asset: Option<AssetId>,
pub timestamp: DateTime<Utc>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Trade {
pub id: Uuid,
pub fill: Fill,
pub realized_pnl: Price,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Position {
pub symbol: Symbol,
pub side: Option<Side>,
pub quantity: Quantity,
pub entry_price: Option<Price>,
pub unrealized_pnl: Price,
pub updated_at: DateTime<Utc>,
}
impl Position {
pub fn mark_price(&mut self, price: Price) {
if let (Some(entry), Some(side)) = (self.entry_price, self.side) {
let delta = match side {
Side::Buy => price - entry,
Side::Sell => entry - price,
};
self.unrealized_pnl = delta * self.quantity;
}
self.updated_at = Utc::now();
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct AccountBalance {
#[serde(default)]
pub exchange: ExchangeId,
#[serde(alias = "currency")]
pub asset: AssetId,
pub total: Price,
pub available: Price,
pub updated_at: DateTime<Utc>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Signal {
pub id: Uuid,
pub symbol: Symbol,
pub kind: SignalKind,
pub confidence: f64,
#[serde(default)]
pub quantity: Option<Quantity>,
#[serde(default)]
pub group_id: Option<Uuid>,
#[serde(default)]
pub panic_behavior: Option<SignalPanicBehavior>,
pub generated_at: DateTime<Utc>,
pub note: Option<String>,
pub stop_loss: Option<Price>,
pub take_profit: Option<Price>,
pub execution_hint: Option<ExecutionHint>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum SignalKind {
EnterLong,
ExitLong,
EnterShort,
ExitShort,
Flatten,
}
impl SignalKind {
#[must_use]
pub fn side(self) -> Side {
match self {
Self::EnterLong | Self::ExitShort => Side::Buy,
Self::EnterShort | Self::ExitLong => Side::Sell,
Self::Flatten => {
Side::Sell
}
}
}
}
impl Signal {
#[must_use]
pub fn new(symbol: impl Into<Symbol>, kind: SignalKind, confidence: f64) -> Self {
Self {
id: Uuid::new_v4(),
symbol: symbol.into(),
kind,
confidence,
quantity: None,
group_id: None,
panic_behavior: None,
generated_at: Utc::now(),
note: None,
stop_loss: None,
take_profit: None,
execution_hint: None,
}
}
#[must_use]
pub fn with_hint(mut self, hint: ExecutionHint) -> Self {
self.execution_hint = Some(hint);
self
}
#[must_use]
pub fn with_quantity(mut self, quantity: Quantity) -> Self {
self.quantity = Some(quantity);
self
}
#[must_use]
pub fn with_group(mut self, group_id: Uuid) -> Self {
self.group_id = Some(group_id);
self
}
#[must_use]
pub fn with_panic_behavior(mut self, behavior: SignalPanicBehavior) -> Self {
self.panic_behavior = Some(behavior);
self
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub enum SignalPanicBehavior {
Market,
AggressiveLimit { offset_bps: Decimal },
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::prelude::FromPrimitive;
#[test]
fn interval_duration_matches_definition() {
assert_eq!(Interval::OneMinute.as_duration(), Duration::minutes(1));
assert_eq!(Interval::FourHours.as_duration(), Duration::hours(4));
}
#[test]
fn position_mark_price_updates_unrealized_pnl() {
let mut position = Position {
symbol: Symbol::from("BTCUSDT"),
side: Some(Side::Buy),
quantity: Decimal::from_f64(0.5).unwrap(),
entry_price: Some(Decimal::from(60_000)),
unrealized_pnl: Decimal::ZERO,
updated_at: Utc::now(),
};
position.mark_price(Decimal::from(60_500));
assert_eq!(position.unrealized_pnl, Decimal::from(250));
}
#[test]
fn local_order_book_tracks_best_levels() {
let mut lob = LocalOrderBook::new();
lob.add_order(Side::Buy, Decimal::from(10), Decimal::from(2));
lob.add_order(Side::Buy, Decimal::from(11), Decimal::from(1));
lob.add_order(
Side::Sell,
Decimal::from(12),
Decimal::from_f64(1.5).unwrap(),
);
lob.add_order(Side::Sell, Decimal::from(13), Decimal::from(3));
assert_eq!(lob.best_bid(), Some((Decimal::from(11), Decimal::from(1))));
assert_eq!(
lob.best_ask(),
Some((Decimal::from(12), Decimal::from_f64(1.5).unwrap()))
);
let fills = lob.take_liquidity(Side::Buy, Decimal::from(2));
assert_eq!(
fills,
vec![
(Decimal::from(12), Decimal::from_f64(1.5).unwrap()),
(Decimal::from(13), Decimal::from_f64(0.5).unwrap())
]
);
assert_eq!(
lob.best_ask(),
Some((Decimal::from(13), Decimal::from_f64(2.5).unwrap()))
);
}
#[test]
fn local_order_book_apply_delta_overwrites_level() {
let mut lob = LocalOrderBook::new();
lob.apply_delta(Side::Buy, Decimal::from(100), Decimal::from(1));
lob.apply_delta(Side::Buy, Decimal::from(100), Decimal::from(3));
assert_eq!(lob.best_bid(), Some((Decimal::from(100), Decimal::from(3))));
lob.apply_delta(Side::Buy, Decimal::from(100), Decimal::ZERO);
assert!(lob.best_bid().is_none());
}
#[test]
fn local_order_book_checksum_reflects_depth() {
let mut lob = LocalOrderBook::new();
lob.apply_delta(Side::Buy, Decimal::from(10), Decimal::from(1));
lob.apply_delta(Side::Buy, Decimal::from(9), Decimal::from(2));
lob.apply_delta(Side::Sell, Decimal::from(11), Decimal::from(1));
lob.apply_delta(Side::Sell, Decimal::from(12), Decimal::from(2));
let checksum_full = lob.checksum(2);
let checksum_partial = lob.checksum(1);
assert_ne!(checksum_full, checksum_partial);
}
#[test]
fn local_order_book_reports_volume_at_level() {
let mut lob = LocalOrderBook::new();
lob.apply_delta(Side::Buy, Decimal::from(100), Decimal::from(3));
lob.apply_delta(Side::Sell, Decimal::from(105), Decimal::from(2));
assert_eq!(
lob.volume_at_level(Side::Buy, Decimal::from(100)),
Decimal::from(3)
);
assert_eq!(
lob.volume_at_level(Side::Sell, Decimal::from(105)),
Decimal::from(2)
);
assert_eq!(
lob.volume_at_level(Side::Buy, Decimal::from(101)),
Decimal::ZERO
);
}
}