use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketMakerPerformance {
pub inventory_turnover: f64,
pub realized_spread_bps: f64,
pub adverse_selection_bps: f64,
pub gross_profit: Decimal,
pub net_profit: Decimal,
pub sharpe_ratio: f64,
}
#[derive(Debug, Clone)]
pub struct MmTrade {
pub side: MmSide,
pub price: Decimal,
pub quantity: Decimal,
pub midpoint_at_trade: Decimal,
pub midpoint_at_exit: Option<Decimal>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MmSide {
Buy,
Sell,
}
#[derive(Debug)]
pub struct MarketMakingAnalytics {
trades: Vec<MmTrade>,
inventory_history: VecDeque<(Decimal, i64)>,
max_history: usize,
}
impl MarketMakingAnalytics {
pub fn new(max_history: usize) -> Self {
Self {
trades: Vec::new(),
inventory_history: VecDeque::with_capacity(max_history),
max_history,
}
}
pub fn record_trade(&mut self, trade: MmTrade) {
self.trades.push(trade);
}
pub fn record_inventory(&mut self, inventory: Decimal, timestamp: i64) {
if self.inventory_history.len() >= self.max_history {
self.inventory_history.pop_front();
}
self.inventory_history.push_back((inventory, timestamp));
}
pub fn inventory_turnover(&self) -> f64 {
if self.inventory_history.is_empty() {
return 0.0;
}
let avg_inventory = self
.inventory_history
.iter()
.map(|(inv, _)| *inv)
.sum::<Decimal>()
/ Decimal::from(self.inventory_history.len());
let total_volume: Decimal = self.trades.iter().map(|t| t.quantity).sum();
if avg_inventory > Decimal::ZERO {
(total_volume / avg_inventory)
.to_string()
.parse()
.unwrap_or(0.0)
} else {
0.0
}
}
pub fn realized_spread_bps(&self) -> f64 {
let mut total_spread = 0.0;
let mut count = 0;
for trade in &self.trades {
if trade.midpoint_at_exit.is_none() {
continue;
}
let midpoint_exit = trade.midpoint_at_exit.unwrap();
let spread = match trade.side {
MmSide::Buy => (midpoint_exit - trade.price) * dec!(2),
MmSide::Sell => (trade.price - midpoint_exit) * dec!(2),
};
if trade.midpoint_at_trade > Decimal::ZERO {
let spread_bps = ((spread / trade.midpoint_at_trade) * dec!(10000))
.to_string()
.parse()
.unwrap_or(0.0);
total_spread += spread_bps;
count += 1;
}
}
if count > 0 {
total_spread / count as f64
} else {
0.0
}
}
pub fn adverse_selection_cost_bps(&self) -> f64 {
let mut total_cost = 0.0;
let mut count = 0;
for trade in &self.trades {
if trade.midpoint_at_exit.is_none() {
continue;
}
let midpoint_exit = trade.midpoint_at_exit.unwrap();
let midpoint_trade = trade.midpoint_at_trade;
let price_move = match trade.side {
MmSide::Buy => midpoint_exit - midpoint_trade,
MmSide::Sell => midpoint_trade - midpoint_exit,
};
if midpoint_trade > Decimal::ZERO {
let cost_bps = ((price_move / midpoint_trade) * dec!(10000))
.to_string()
.parse()
.unwrap_or(0.0);
total_cost += cost_bps;
count += 1;
}
}
if count > 0 {
total_cost / count as f64
} else {
0.0
}
}
pub fn gross_profit(&self) -> Decimal {
let mut profit = Decimal::ZERO;
for trade in &self.trades {
let trade_pnl = match trade.side {
MmSide::Buy => {
(trade.midpoint_at_trade - trade.price) * trade.quantity
}
MmSide::Sell => {
(trade.price - trade.midpoint_at_trade) * trade.quantity
}
};
profit += trade_pnl;
}
profit
}
pub fn sharpe_ratio(&self, risk_free_rate: f64) -> f64 {
if self.trades.len() < 2 {
return 0.0;
}
let mut returns = Vec::new();
for trade in &self.trades {
let ret = match trade.side {
MmSide::Buy => (trade.midpoint_at_trade - trade.price) / trade.price,
MmSide::Sell => (trade.price - trade.midpoint_at_trade) / trade.price,
};
returns.push(ret.to_string().parse::<f64>().unwrap_or(0.0));
}
let mean_return = returns.iter().sum::<f64>() / returns.len() as f64;
let variance = returns
.iter()
.map(|r| (r - mean_return).powi(2))
.sum::<f64>()
/ returns.len() as f64;
let std_dev = variance.sqrt();
if std_dev > 0.0 {
(mean_return - risk_free_rate) / std_dev
} else {
0.0
}
}
pub fn get_performance(&self, transaction_costs: Decimal) -> MarketMakerPerformance {
let gross_profit = self.gross_profit();
let total_trades = Decimal::from(self.trades.len());
let total_costs = transaction_costs * total_trades;
let net_profit = gross_profit - total_costs;
MarketMakerPerformance {
inventory_turnover: self.inventory_turnover(),
realized_spread_bps: self.realized_spread_bps(),
adverse_selection_bps: self.adverse_selection_cost_bps(),
gross_profit,
net_profit,
sharpe_ratio: self.sharpe_ratio(0.0),
}
}
}
#[derive(Debug)]
pub struct SpreadDecomposition;
impl SpreadDecomposition {
pub fn decompose(
quoted_spread: Decimal,
realized_spread: Decimal,
adverse_selection: Decimal,
) -> SpreadComponents {
let order_processing = realized_spread - adverse_selection;
SpreadComponents {
quoted_spread,
realized_spread,
adverse_selection,
order_processing_cost: order_processing,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpreadComponents {
pub quoted_spread: Decimal,
pub realized_spread: Decimal,
pub adverse_selection: Decimal,
pub order_processing_cost: Decimal,
}
#[derive(Debug)]
pub struct InventoryAnalyzer {
target_inventory: Decimal,
}
impl InventoryAnalyzer {
pub fn new(target_inventory: Decimal) -> Self {
Self { target_inventory }
}
pub fn calculate_skew(&self, current_inventory: Decimal) -> f64 {
if self.target_inventory == Decimal::ZERO {
return 0.0;
}
let deviation = current_inventory - self.target_inventory;
let skew: f64 = (deviation / self.target_inventory)
.to_string()
.parse()
.unwrap_or(0.0);
skew.clamp(-0.5, 0.5)
}
pub fn suggest_adjustments(
&self,
current_inventory: Decimal,
base_spread: Decimal,
) -> QuoteAdjustments {
let skew = self.calculate_skew(current_inventory);
let bid_adjustment = Decimal::from_f64_retain(-skew * 0.5).unwrap_or(Decimal::ZERO);
let ask_adjustment = Decimal::from_f64_retain(skew * 0.5).unwrap_or(Decimal::ZERO);
QuoteAdjustments {
bid_adjustment: bid_adjustment * base_spread,
ask_adjustment: ask_adjustment * base_spread,
suggested_spread: base_spread
* (Decimal::ONE
+ Decimal::from_f64_retain(skew.abs() * 0.2).unwrap_or(Decimal::ZERO)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuoteAdjustments {
pub bid_adjustment: Decimal,
pub ask_adjustment: Decimal,
pub suggested_spread: Decimal,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_inventory_turnover() {
let mut analytics = MarketMakingAnalytics::new(100);
analytics.record_inventory(dec!(1000), 0);
analytics.record_inventory(dec!(1000), 1);
for _ in 0..10 {
analytics.record_trade(MmTrade {
side: MmSide::Buy,
price: dec!(100),
quantity: dec!(100),
midpoint_at_trade: dec!(100),
midpoint_at_exit: Some(dec!(100)),
});
}
let turnover = analytics.inventory_turnover();
assert!((turnover - 1.0).abs() < 0.1);
}
#[test]
fn test_realized_spread() {
let mut analytics = MarketMakingAnalytics::new(100);
analytics.record_trade(MmTrade {
side: MmSide::Buy,
price: dec!(99),
quantity: dec!(100),
midpoint_at_trade: dec!(100),
midpoint_at_exit: Some(dec!(100)),
});
analytics.record_trade(MmTrade {
side: MmSide::Sell,
price: dec!(101),
quantity: dec!(100),
midpoint_at_trade: dec!(100),
midpoint_at_exit: Some(dec!(100)),
});
let spread = analytics.realized_spread_bps();
assert!(spread > 0.0);
}
#[test]
fn test_adverse_selection() {
let mut analytics = MarketMakingAnalytics::new(100);
analytics.record_trade(MmTrade {
side: MmSide::Buy,
price: dec!(100),
quantity: dec!(100),
midpoint_at_trade: dec!(100),
midpoint_at_exit: Some(dec!(99)),
});
let cost = analytics.adverse_selection_cost_bps();
assert!(cost < 0.0);
}
#[test]
fn test_gross_profit() {
let mut analytics = MarketMakingAnalytics::new(100);
analytics.record_trade(MmTrade {
side: MmSide::Buy,
price: dec!(99),
quantity: dec!(100),
midpoint_at_trade: dec!(100),
midpoint_at_exit: None,
});
analytics.record_trade(MmTrade {
side: MmSide::Sell,
price: dec!(101),
quantity: dec!(100),
midpoint_at_trade: dec!(100),
midpoint_at_exit: None,
});
let profit = analytics.gross_profit();
assert_eq!(profit, dec!(200));
}
#[test]
fn test_inventory_skew() {
let analyzer = InventoryAnalyzer::new(dec!(1000));
let skew_high = analyzer.calculate_skew(dec!(1500));
assert!(skew_high > 0.0);
let skew_low = analyzer.calculate_skew(dec!(500));
assert!(skew_low < 0.0);
let skew_target = analyzer.calculate_skew(dec!(1000));
assert_eq!(skew_target, 0.0);
}
#[test]
fn test_quote_adjustments() {
let analyzer = InventoryAnalyzer::new(dec!(1000));
let adj = analyzer.suggest_adjustments(dec!(1500), dec!(1.0));
assert!(adj.ask_adjustment > Decimal::ZERO);
assert!(adj.bid_adjustment < Decimal::ZERO);
}
}