use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
use super::order_book::{LimitOrder, OrderSide};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketMakerConfig {
pub target_spread_pct: Decimal,
pub depth_levels: usize,
pub level_size_pct: Decimal,
pub level_spacing_pct: Decimal,
pub rebalance_threshold: Decimal,
pub max_position: Decimal,
pub min_order_size: Decimal,
pub max_slippage_pct: Decimal,
pub dynamic_spread: bool,
pub inventory_skew: bool,
}
impl Default for MarketMakerConfig {
fn default() -> Self {
Self {
target_spread_pct: dec!(0.02), depth_levels: 5, level_size_pct: dec!(0.10), level_spacing_pct: dec!(0.005), rebalance_threshold: dec!(0.30), max_position: dec!(1000000), min_order_size: dec!(1), max_slippage_pct: dec!(0.05), dynamic_spread: true,
inventory_skew: true,
}
}
}
impl MarketMakerConfig {
pub fn tight() -> Self {
Self {
target_spread_pct: dec!(0.005), depth_levels: 10,
level_size_pct: dec!(0.05),
level_spacing_pct: dec!(0.002),
..Default::default()
}
}
pub fn wide() -> Self {
Self {
target_spread_pct: dec!(0.05), depth_levels: 3,
level_size_pct: dec!(0.15),
level_spacing_pct: dec!(0.01),
..Default::default()
}
}
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct InventoryState {
pub token_balance: Decimal,
pub quote_balance: Decimal,
pub target_token_balance: Decimal,
pub skew: Decimal,
pub total_value: Decimal,
}
impl InventoryState {
pub fn new(token_balance: Decimal, quote_balance: Decimal, token_price: Decimal) -> Self {
let token_value = token_balance * token_price;
let total_value = token_value + quote_balance;
let target_token_balance = if token_price > Decimal::ZERO {
(total_value / dec!(2)) / token_price
} else {
Decimal::ZERO
};
let skew = if target_token_balance > Decimal::ZERO {
(token_balance - target_token_balance) / target_token_balance
} else {
Decimal::ZERO
};
Self {
token_balance,
quote_balance,
target_token_balance,
skew,
total_value,
}
}
pub fn needs_rebalance(&self, threshold: Decimal) -> bool {
self.skew.abs() > threshold
}
pub fn rebalance_amount(&self) -> Decimal {
self.token_balance - self.target_token_balance
}
}
#[derive(Debug, Clone, Serialize)]
pub struct MarketMakerQuote {
pub token_id: Uuid,
pub bids: Vec<QuoteLevel>,
pub asks: Vec<QuoteLevel>,
pub mid_price: Decimal,
pub spread: Decimal,
pub spread_pct: Decimal,
}
#[derive(Debug, Clone, Serialize)]
pub struct QuoteLevel {
pub price: Decimal,
pub amount: Decimal,
pub total_value: Decimal,
}
pub struct TokenMarketMaker {
pub token_id: Uuid,
pub config: MarketMakerConfig,
inventory: InventoryState,
active_bid_orders: Vec<Uuid>,
active_ask_orders: Vec<Uuid>,
last_mid_price: Option<Decimal>,
volatility_estimate: Decimal,
}
impl TokenMarketMaker {
pub fn new(token_id: Uuid, config: MarketMakerConfig) -> Self {
Self {
token_id,
config,
inventory: InventoryState::default(),
active_bid_orders: Vec::new(),
active_ask_orders: Vec::new(),
last_mid_price: None,
volatility_estimate: dec!(0.02), }
}
pub fn update_inventory(
&mut self,
token_balance: Decimal,
quote_balance: Decimal,
token_price: Decimal,
) {
self.inventory = InventoryState::new(token_balance, quote_balance, token_price);
}
pub fn calculate_spread(&self) -> Decimal {
let mut spread = self.config.target_spread_pct;
if self.config.dynamic_spread {
spread = spread.max(self.volatility_estimate * dec!(1.5));
}
if self.config.inventory_skew {
let skew_adjustment = self.inventory.skew.abs() * dec!(0.5);
spread += spread * skew_adjustment;
}
spread
}
pub fn calculate_prices(&self, mid_price: Decimal) -> (Decimal, Decimal) {
let spread = self.calculate_spread();
let half_spread = spread / dec!(2);
let mut bid_price = mid_price * (Decimal::ONE - half_spread);
let mut ask_price = mid_price * (Decimal::ONE + half_spread);
if self.config.inventory_skew && self.inventory.skew.abs() > dec!(0.1) {
let skew_adjustment = self.inventory.skew * self.config.target_spread_pct;
if self.inventory.skew > Decimal::ZERO {
ask_price -= mid_price * skew_adjustment;
} else {
bid_price += mid_price * skew_adjustment.abs();
}
}
(bid_price, ask_price)
}
pub fn generate_quote(&self, mid_price: Decimal) -> MarketMakerQuote {
let (base_bid, base_ask) = self.calculate_prices(mid_price);
let spread = base_ask - base_bid;
let spread_pct = if mid_price > Decimal::ZERO {
spread / mid_price
} else {
Decimal::ZERO
};
let level_size = self.inventory.token_balance * self.config.level_size_pct;
let mut bids = Vec::new();
let mut asks = Vec::new();
for i in 0..self.config.depth_levels {
let level_offset = Decimal::from(i as u32) * self.config.level_spacing_pct;
let bid_price = base_bid * (Decimal::ONE - level_offset);
if bid_price > Decimal::ZERO {
let bid_amount = level_size.min(self.inventory.quote_balance / bid_price);
if bid_amount >= self.config.min_order_size {
bids.push(QuoteLevel {
price: bid_price,
amount: bid_amount,
total_value: bid_price * bid_amount,
});
}
}
let ask_price = base_ask * (Decimal::ONE + level_offset);
if ask_price > Decimal::ZERO {
let ask_amount = level_size.min(self.inventory.token_balance);
if ask_amount >= self.config.min_order_size {
asks.push(QuoteLevel {
price: ask_price,
amount: ask_amount,
total_value: ask_price * ask_amount,
});
}
}
}
MarketMakerQuote {
token_id: self.token_id,
bids,
asks,
mid_price,
spread,
spread_pct,
}
}
pub fn generate_orders(&mut self, mid_price: Decimal, mm_user_id: Uuid) -> Vec<LimitOrder> {
let quote = self.generate_quote(mid_price);
let mut orders = Vec::new();
for level in quote.bids {
orders.push(LimitOrder::new(
mm_user_id,
self.token_id,
OrderSide::Buy,
level.price,
level.amount,
));
}
for level in quote.asks {
orders.push(LimitOrder::new(
mm_user_id,
self.token_id,
OrderSide::Sell,
level.price,
level.amount,
));
}
self.active_bid_orders = orders
.iter()
.filter(|o| o.side == OrderSide::Buy)
.map(|o| o.order_id)
.collect();
self.active_ask_orders = orders
.iter()
.filter(|o| o.side == OrderSide::Sell)
.map(|o| o.order_id)
.collect();
self.last_mid_price = Some(mid_price);
orders
}
pub fn update_volatility(&mut self, new_price: Decimal) {
if let Some(last_price) = self.last_mid_price {
if last_price > Decimal::ZERO {
let return_pct = (new_price - last_price).abs() / last_price;
self.volatility_estimate =
self.volatility_estimate * dec!(0.9) + return_pct * dec!(0.1);
}
}
self.last_mid_price = Some(new_price);
}
pub fn needs_refresh(&self, current_price: Decimal, threshold_pct: Decimal) -> bool {
if let Some(last_price) = self.last_mid_price {
if last_price > Decimal::ZERO {
let price_change = (current_price - last_price).abs() / last_price;
return price_change > threshold_pct;
}
}
true }
pub fn active_order_ids(&self) -> Vec<Uuid> {
let mut ids = self.active_bid_orders.clone();
ids.extend(self.active_ask_orders.clone());
ids
}
pub fn inventory(&self) -> &InventoryState {
&self.inventory
}
pub fn needs_rebalance(&self) -> bool {
self.inventory
.needs_rebalance(self.config.rebalance_threshold)
}
pub fn rebalance_recommendation(&self) -> RebalanceAction {
if !self.needs_rebalance() {
return RebalanceAction::None;
}
let amount = self.inventory.rebalance_amount();
if amount > Decimal::ZERO {
RebalanceAction::Sell(amount)
} else {
RebalanceAction::Buy(amount.abs())
}
}
}
#[derive(Debug, Clone, Serialize)]
pub enum RebalanceAction {
None,
Buy(Decimal),
Sell(Decimal),
}
pub struct MarketMakerManager {
makers: HashMap<Uuid, TokenMarketMaker>,
mm_user_id: Uuid,
default_config: MarketMakerConfig,
}
impl MarketMakerManager {
pub fn new(mm_user_id: Uuid) -> Self {
Self {
makers: HashMap::new(),
mm_user_id,
default_config: MarketMakerConfig::default(),
}
}
pub fn set_default_config(&mut self, config: MarketMakerConfig) {
self.default_config = config;
}
pub fn add_maker(&mut self, token_id: Uuid, config: Option<MarketMakerConfig>) {
let config = config.unwrap_or_else(|| self.default_config.clone());
self.makers
.insert(token_id, TokenMarketMaker::new(token_id, config));
}
pub fn remove_maker(&mut self, token_id: Uuid) -> Option<TokenMarketMaker> {
self.makers.remove(&token_id)
}
pub fn get_maker(&self, token_id: &Uuid) -> Option<&TokenMarketMaker> {
self.makers.get(token_id)
}
pub fn get_maker_mut(&mut self, token_id: &Uuid) -> Option<&mut TokenMarketMaker> {
self.makers.get_mut(token_id)
}
pub fn update_inventory(
&mut self,
token_id: &Uuid,
token_balance: Decimal,
quote_balance: Decimal,
token_price: Decimal,
) {
if let Some(maker) = self.makers.get_mut(token_id) {
maker.update_inventory(token_balance, quote_balance, token_price);
}
}
pub fn generate_orders(&mut self, token_id: &Uuid, mid_price: Decimal) -> Vec<LimitOrder> {
if let Some(maker) = self.makers.get_mut(token_id) {
maker.generate_orders(mid_price, self.mm_user_id)
} else {
Vec::new()
}
}
pub fn get_quote(&self, token_id: &Uuid, mid_price: Decimal) -> Option<MarketMakerQuote> {
self.makers
.get(token_id)
.map(|m| m.generate_quote(mid_price))
}
pub fn tokens_needing_refresh(
&self,
prices: &HashMap<Uuid, Decimal>,
threshold: Decimal,
) -> Vec<Uuid> {
self.makers
.iter()
.filter(|(id, maker)| {
if let Some(price) = prices.get(id) {
maker.needs_refresh(*price, threshold)
} else {
false
}
})
.map(|(id, _)| *id)
.collect()
}
pub fn tokens_needing_rebalance(&self) -> Vec<(Uuid, RebalanceAction)> {
self.makers
.iter()
.filter_map(|(id, maker)| {
let action = maker.rebalance_recommendation();
match action {
RebalanceAction::None => None,
_ => Some((*id, action)),
}
})
.collect()
}
pub fn summary(&self) -> Vec<MarketMakerSummary> {
self.makers
.iter()
.map(|(id, maker)| MarketMakerSummary {
token_id: *id,
inventory: maker.inventory.clone(),
volatility: maker.volatility_estimate,
needs_rebalance: maker.needs_rebalance(),
active_orders: maker.active_order_ids().len(),
})
.collect()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct MarketMakerSummary {
pub token_id: Uuid,
pub inventory: InventoryState,
pub volatility: Decimal,
pub needs_rebalance: bool,
pub active_orders: usize,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct MarketMakerStats {
pub total_buy_volume: Decimal,
pub total_sell_volume: Decimal,
pub total_profit: Decimal,
pub trades_count: u64,
pub avg_spread_earned: Decimal,
pub rebalance_count: u64,
}
impl MarketMakerStats {
pub fn record_trade(
&mut self,
side: OrderSide,
amount: Decimal,
price: Decimal,
spread_earned: Decimal,
) {
match side {
OrderSide::Buy => self.total_buy_volume += amount * price,
OrderSide::Sell => self.total_sell_volume += amount * price,
}
self.total_profit += spread_earned;
self.trades_count += 1;
let n = Decimal::from(self.trades_count);
self.avg_spread_earned = (self.avg_spread_earned * (n - Decimal::ONE) + spread_earned) / n;
}
pub fn record_rebalance(&mut self) {
self.rebalance_count += 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn make_token_id() -> Uuid {
Uuid::new_v4()
}
fn make_user_id() -> Uuid {
Uuid::new_v4()
}
#[test]
fn test_inventory_state_balanced() {
let price = dec!(2.0);
let inv = InventoryState::new(dec!(50), dec!(100), price);
assert_eq!(
inv.total_value,
dec!(200),
"total_value = token_balance * price + quote_balance"
);
assert_eq!(inv.target_token_balance, dec!(50));
assert_eq!(
inv.skew,
dec!(0),
"Perfectly balanced inventory must have zero skew"
);
}
#[test]
fn test_inventory_state_token_heavy() {
let price = dec!(1.0);
let inv = InventoryState::new(dec!(80), dec!(20), price);
assert!(
inv.skew > dec!(0),
"Excess token holdings must produce positive skew"
);
assert_eq!(
inv.rebalance_amount(),
inv.token_balance - inv.target_token_balance,
"rebalance_amount must equal token_balance minus target"
);
}
#[test]
fn test_inventory_state_zero_price() {
let inv = InventoryState::new(dec!(100), dec!(100), dec!(0));
assert_eq!(inv.target_token_balance, dec!(0));
assert_eq!(inv.skew, dec!(0));
}
#[test]
fn test_needs_rebalance_above_threshold() {
let price = dec!(1.0);
let inv = InventoryState::new(dec!(80), dec!(20), price);
assert!(
inv.needs_rebalance(dec!(0.30)),
"Skew of 0.6 must trigger rebalance at threshold 0.30"
);
}
#[test]
fn test_needs_rebalance_below_threshold() {
let price = dec!(1.0);
let inv = InventoryState::new(dec!(52), dec!(48), price);
assert!(
!inv.needs_rebalance(dec!(0.30)),
"Skew of 0.04 must not trigger rebalance at threshold 0.30"
);
}
#[test]
fn test_spread_at_least_target_when_no_skew() {
let token_id = make_token_id();
let config = MarketMakerConfig {
dynamic_spread: false,
inventory_skew: false,
target_spread_pct: dec!(0.02),
..MarketMakerConfig::default()
};
let maker = TokenMarketMaker::new(token_id, config.clone());
let spread = maker.calculate_spread();
assert_eq!(
spread, config.target_spread_pct,
"With dynamic and skew disabled, spread must equal target_spread_pct"
);
}
#[test]
fn test_spread_widens_with_high_volatility() {
let token_id = make_token_id();
let config = MarketMakerConfig {
dynamic_spread: true,
inventory_skew: false,
target_spread_pct: dec!(0.02),
..MarketMakerConfig::default()
};
let mut maker = TokenMarketMaker::new(token_id, config);
maker.volatility_estimate = dec!(0.10);
let spread = maker.calculate_spread();
assert!(
spread > dec!(0.02),
"High volatility must cause spread to exceed target_spread_pct"
);
}
#[test]
fn test_bid_below_mid_and_ask_above_mid() {
let token_id = make_token_id();
let config = MarketMakerConfig {
dynamic_spread: false,
inventory_skew: false,
..MarketMakerConfig::default()
};
let maker = TokenMarketMaker::new(token_id, config);
let mid = dec!(100.0);
let (bid, ask) = maker.calculate_prices(mid);
assert!(bid < mid, "Bid must be below mid price");
assert!(ask > mid, "Ask must be above mid price");
assert!(bid < ask, "Bid must be strictly less than ask");
}
#[test]
fn test_generate_quote_positive_spread() {
let token_id = make_token_id();
let config = MarketMakerConfig {
dynamic_spread: false,
inventory_skew: false,
..MarketMakerConfig::default()
};
let mut maker = TokenMarketMaker::new(token_id, config);
maker.update_inventory(dec!(1000), dec!(1000), dec!(1.0));
let mid = dec!(1.0);
let quote = maker.generate_quote(mid);
assert_eq!(quote.token_id, token_id);
assert!(
quote.spread > dec!(0),
"Quote spread must be strictly positive"
);
assert!(
quote.spread_pct > dec!(0),
"Quote spread_pct must be strictly positive"
);
assert_eq!(quote.mid_price, mid);
}
#[test]
fn test_generate_quote_empty_inventory_produces_no_asks() {
let token_id = make_token_id();
let config = MarketMakerConfig {
dynamic_spread: false,
inventory_skew: false,
min_order_size: dec!(1),
..MarketMakerConfig::default()
};
let mut maker = TokenMarketMaker::new(token_id, config);
maker.update_inventory(dec!(0), dec!(1000), dec!(1.0));
let quote = maker.generate_quote(dec!(1.0));
assert!(
quote.asks.is_empty(),
"Zero token inventory must produce no ask levels"
);
}
#[test]
fn test_needs_refresh_when_no_last_price() {
let token_id = make_token_id();
let maker = TokenMarketMaker::new(token_id, MarketMakerConfig::default());
assert!(
maker.needs_refresh(dec!(1.0), dec!(0.01)),
"needs_refresh must be true when there is no last mid price"
);
}
#[test]
fn test_needs_refresh_when_price_moved_beyond_threshold() {
let token_id = make_token_id();
let mut maker = TokenMarketMaker::new(token_id, MarketMakerConfig::default());
maker.update_volatility(dec!(100.0));
assert!(
maker.needs_refresh(dec!(110.0), dec!(0.05)),
"A 10% price move must exceed the 5% refresh threshold"
);
}
#[test]
fn test_does_not_need_refresh_for_tiny_move() {
let token_id = make_token_id();
let mut maker = TokenMarketMaker::new(token_id, MarketMakerConfig::default());
maker.update_volatility(dec!(100.0));
assert!(
!maker.needs_refresh(dec!(100.5), dec!(0.01)),
"A 0.5% price move must not exceed the 1% refresh threshold"
);
}
#[test]
fn test_volatility_estimate_moves_toward_returns() {
let token_id = make_token_id();
let mut maker = TokenMarketMaker::new(token_id, MarketMakerConfig::default());
let initial_vol = maker.volatility_estimate;
maker.update_volatility(dec!(100.0));
maker.update_volatility(dec!(200.0));
assert!(
maker.volatility_estimate > initial_vol,
"Volatility estimate must increase after a large price move"
);
}
#[test]
fn test_rebalance_recommendation_sell_when_token_heavy() {
let token_id = make_token_id();
let mut maker = TokenMarketMaker::new(token_id, MarketMakerConfig::default());
maker.update_inventory(dec!(80), dec!(20), dec!(1.0));
let rec = maker.rebalance_recommendation();
assert!(
matches!(rec, RebalanceAction::Sell(_)),
"Excess token inventory must recommend Sell action"
);
}
#[test]
fn test_rebalance_recommendation_none_when_balanced() {
let token_id = make_token_id();
let mut maker = TokenMarketMaker::new(token_id, MarketMakerConfig::default());
maker.update_inventory(dec!(50), dec!(50), dec!(1.0));
let rec = maker.rebalance_recommendation();
assert!(
matches!(rec, RebalanceAction::None),
"Balanced inventory must recommend no rebalance action"
);
}
#[test]
fn test_rebalance_recommendation_buy_when_quote_heavy() {
let token_id = make_token_id();
let mut maker = TokenMarketMaker::new(token_id, MarketMakerConfig::default());
maker.update_inventory(dec!(20), dec!(80), dec!(1.0));
let rec = maker.rebalance_recommendation();
assert!(
matches!(rec, RebalanceAction::Buy(_)),
"Excess quote inventory must recommend Buy action"
);
}
#[test]
fn test_stats_buy_volume_accumulated() {
let mut stats = MarketMakerStats::default();
stats.record_trade(OrderSide::Buy, dec!(10), dec!(2.0), dec!(0.04));
assert_eq!(
stats.total_buy_volume,
dec!(20),
"Buy volume must equal amount * price"
);
assert_eq!(stats.trades_count, 1);
assert_eq!(stats.total_profit, dec!(0.04));
}
#[test]
fn test_stats_sell_volume_accumulated() {
let mut stats = MarketMakerStats::default();
stats.record_trade(OrderSide::Sell, dec!(5), dec!(3.0), dec!(0.05));
assert_eq!(
stats.total_sell_volume,
dec!(15),
"Sell volume must equal amount * price"
);
}
#[test]
fn test_stats_average_spread_running_mean() {
let mut stats = MarketMakerStats::default();
stats.record_trade(OrderSide::Buy, dec!(1), dec!(1.0), dec!(0.02));
stats.record_trade(OrderSide::Buy, dec!(1), dec!(1.0), dec!(0.04));
assert_eq!(
stats.avg_spread_earned,
dec!(0.03),
"Average spread must be the running mean of spread_earned values"
);
}
#[test]
fn test_stats_rebalance_count_increments() {
let mut stats = MarketMakerStats::default();
stats.record_rebalance();
stats.record_rebalance();
assert_eq!(stats.rebalance_count, 2);
}
#[test]
fn test_manager_add_and_remove_maker() {
let mm_user = make_user_id();
let mut manager = MarketMakerManager::new(mm_user);
let token_id = make_token_id();
manager.add_maker(token_id, None);
assert!(
manager.get_maker(&token_id).is_some(),
"Maker must be accessible after add_maker"
);
let removed = manager.remove_maker(token_id);
assert!(
removed.is_some(),
"remove_maker must return the removed maker"
);
assert!(
manager.get_maker(&token_id).is_none(),
"Maker must be gone after remove_maker"
);
}
#[test]
fn test_manager_get_quote_returns_none_for_unknown_token() {
let mm_user = make_user_id();
let manager = MarketMakerManager::new(mm_user);
let unknown = make_token_id();
assert!(
manager.get_quote(&unknown, dec!(1.0)).is_none(),
"get_quote must return None for tokens with no maker"
);
}
#[test]
fn test_manager_tokens_needing_rebalance() {
let mm_user = make_user_id();
let mut manager = MarketMakerManager::new(mm_user);
let token_id = make_token_id();
manager.add_maker(token_id, None);
manager.update_inventory(&token_id, dec!(80), dec!(20), dec!(1.0));
let needs_rebalance = manager.tokens_needing_rebalance();
assert!(
!needs_rebalance.is_empty(),
"Manager must report a token that needs rebalancing"
);
assert_eq!(
needs_rebalance[0].0, token_id,
"The reported token must be the one with skewed inventory"
);
}
}