use crate::error::Result;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AvellanedaStoikovConfig {
pub risk_aversion: Decimal,
pub time_horizon_secs: u64,
pub order_intensity: Decimal,
pub max_inventory: Decimal,
pub min_spread_bps: Decimal,
pub max_spread_bps: Decimal,
}
impl Default for AvellanedaStoikovConfig {
fn default() -> Self {
Self {
risk_aversion: dec!(0.1),
time_horizon_secs: 3600, order_intensity: dec!(1.5),
max_inventory: dec!(1000),
min_spread_bps: dec!(10), max_spread_bps: dec!(500), }
}
}
#[derive(Debug, Clone)]
pub struct MarketState {
pub mid_price: Decimal,
pub volatility: Decimal,
pub inventory: Decimal,
pub time_remaining_secs: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ASQuote {
pub bid_price: Decimal,
pub ask_price: Decimal,
pub bid_size: Decimal,
pub ask_size: Decimal,
pub reservation_price: Decimal,
pub spread: Decimal,
pub timestamp: std::time::SystemTime,
}
pub struct AvellanedaStoikovMarketMaker {
pub config: AvellanedaStoikovConfig,
pub token_id: Uuid,
}
impl AvellanedaStoikovMarketMaker {
pub fn new(config: AvellanedaStoikovConfig, token_id: Uuid) -> Self {
Self { config, token_id }
}
pub fn calculate_reservation_price(&self, state: &MarketState) -> Result<Decimal> {
let mid_price = state.mid_price;
let inventory = state.inventory;
let gamma = self.config.risk_aversion;
let sigma = state.volatility;
let time_remaining_years = Decimal::from(state.time_remaining_secs) / dec!(31536000);
let variance = sigma * sigma;
let inventory_adjustment = inventory * gamma * variance * time_remaining_years;
let reservation_price = mid_price - inventory_adjustment;
Ok(reservation_price)
}
pub fn calculate_optimal_spread(&self, state: &MarketState) -> Result<Decimal> {
let gamma = self.config.risk_aversion;
let sigma = state.volatility;
let kappa = self.config.order_intensity;
let time_remaining_years = Decimal::from(state.time_remaining_secs) / dec!(31536000);
let variance = sigma * sigma;
let first_term = gamma * variance * time_remaining_years;
let ratio = gamma / kappa;
let ln_approx = self.ln_approximation(dec!(1) + ratio);
let second_term = (dec!(2) / gamma) * ln_approx;
let optimal_spread = first_term + second_term;
let spread_bps = optimal_spread * dec!(10000); let constrained_bps = spread_bps
.max(self.config.min_spread_bps)
.min(self.config.max_spread_bps);
Ok(constrained_bps / dec!(10000)) }
pub fn calculate_asymmetric_spread(
&self,
reservation_price: Decimal,
optimal_spread: Decimal,
state: &MarketState,
) -> Result<(Decimal, Decimal)> {
let inventory_ratio = if self.config.max_inventory > Decimal::ZERO {
(state.inventory / self.config.max_inventory)
.min(dec!(1))
.max(dec!(-1))
} else {
Decimal::ZERO
};
let asymmetry_factor = inventory_ratio.abs() * dec!(0.5);
let bid_adjustment = if inventory_ratio > Decimal::ZERO {
dec!(1) - asymmetry_factor
} else {
dec!(1) + asymmetry_factor
};
let ask_adjustment = if inventory_ratio > Decimal::ZERO {
dec!(1) + asymmetry_factor
} else {
dec!(1) - asymmetry_factor
};
let half_spread = optimal_spread / dec!(2);
let bid_price = reservation_price - (half_spread * bid_adjustment);
let ask_price = reservation_price + (half_spread * ask_adjustment);
Ok((bid_price, ask_price))
}
pub fn generate_quote(&self, state: &MarketState, base_size: Decimal) -> Result<ASQuote> {
let reservation_price = self.calculate_reservation_price(state)?;
let optimal_spread = self.calculate_optimal_spread(state)?;
let (bid_price, ask_price) =
self.calculate_asymmetric_spread(reservation_price, optimal_spread, state)?;
let (bid_size, ask_size) = self.calculate_optimal_sizes(state, base_size)?;
Ok(ASQuote {
bid_price,
ask_price,
bid_size,
ask_size,
reservation_price,
spread: optimal_spread,
timestamp: std::time::SystemTime::now(),
})
}
fn calculate_optimal_sizes(
&self,
state: &MarketState,
base_size: Decimal,
) -> Result<(Decimal, Decimal)> {
let inventory_ratio = if self.config.max_inventory > Decimal::ZERO {
(state.inventory / self.config.max_inventory)
.min(dec!(1))
.max(dec!(-1))
} else {
Decimal::ZERO
};
let bid_multiplier = if inventory_ratio > Decimal::ZERO {
dec!(1) - (inventory_ratio * dec!(0.5))
} else {
dec!(1) + (inventory_ratio.abs() * dec!(0.5))
};
let ask_multiplier = if inventory_ratio < Decimal::ZERO {
dec!(1) - (inventory_ratio.abs() * dec!(0.5))
} else {
dec!(1) + (inventory_ratio * dec!(0.5))
};
let bid_size = base_size * bid_multiplier;
let ask_size = base_size * ask_multiplier;
Ok((bid_size, ask_size))
}
fn ln_approximation(&self, x: Decimal) -> Decimal {
if x <= Decimal::ZERO {
return Decimal::MIN;
}
let y = (x - dec!(1)) / (x + dec!(1));
let y2 = y * y;
let mut result = y;
let mut term = y;
for n in 1..=4 {
term *= y2;
let divisor = Decimal::from(2 * n + 1);
result += term / divisor;
}
result * dec!(2)
}
pub fn calculate_expected_profit(
&self,
_state: &MarketState,
quote: &ASQuote,
) -> Result<Decimal> {
let spread = quote.ask_price - quote.bid_price;
let kappa = self.config.order_intensity;
let expected_profit = kappa * spread / dec!(2);
Ok(expected_profit)
}
pub fn calculate_inventory_risk(&self, state: &MarketState) -> Result<Decimal> {
let gamma = self.config.risk_aversion;
let sigma = state.volatility;
let inventory = state.inventory;
let variance = sigma * sigma;
let risk = dec!(0.5) * gamma * variance * inventory * inventory;
Ok(risk)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_reservation_price_neutral_inventory() {
let config = AvellanedaStoikovConfig::default();
let mm = AvellanedaStoikovMarketMaker::new(config, Uuid::new_v4());
let state = MarketState {
mid_price: dec!(100),
volatility: dec!(0.20), inventory: Decimal::ZERO,
time_remaining_secs: 3600,
};
let reservation_price = mm.calculate_reservation_price(&state).unwrap();
assert_eq!(reservation_price, dec!(100));
}
#[test]
fn test_reservation_price_long_inventory() {
let config = AvellanedaStoikovConfig::default();
let mm = AvellanedaStoikovMarketMaker::new(config, Uuid::new_v4());
let state = MarketState {
mid_price: dec!(100),
volatility: dec!(0.20),
inventory: dec!(50), time_remaining_secs: 3600,
};
let reservation_price = mm.calculate_reservation_price(&state).unwrap();
assert!(reservation_price < dec!(100));
}
#[test]
fn test_reservation_price_short_inventory() {
let config = AvellanedaStoikovConfig::default();
let mm = AvellanedaStoikovMarketMaker::new(config, Uuid::new_v4());
let state = MarketState {
mid_price: dec!(100),
volatility: dec!(0.20),
inventory: dec!(-50), time_remaining_secs: 3600,
};
let reservation_price = mm.calculate_reservation_price(&state).unwrap();
assert!(reservation_price > dec!(100));
}
#[test]
fn test_optimal_spread_calculation() {
let config = AvellanedaStoikovConfig::default();
let min_spread_bps = config.min_spread_bps;
let max_spread_bps = config.max_spread_bps;
let mm = AvellanedaStoikovMarketMaker::new(config, Uuid::new_v4());
let state = MarketState {
mid_price: dec!(100),
volatility: dec!(0.20),
inventory: Decimal::ZERO,
time_remaining_secs: 3600,
};
let spread = mm.calculate_optimal_spread(&state).unwrap();
assert!(spread > Decimal::ZERO);
let spread_bps = spread * dec!(10000);
assert!(spread_bps >= min_spread_bps);
assert!(spread_bps <= max_spread_bps);
}
#[test]
fn test_generate_quote() {
let config = AvellanedaStoikovConfig::default();
let mm = AvellanedaStoikovMarketMaker::new(config, Uuid::new_v4());
let state = MarketState {
mid_price: dec!(100),
volatility: dec!(0.20),
inventory: Decimal::ZERO,
time_remaining_secs: 3600,
};
let quote = mm.generate_quote(&state, dec!(10)).unwrap();
assert!(quote.bid_price > Decimal::ZERO);
assert!(quote.ask_price > quote.bid_price);
assert!(quote.bid_size > Decimal::ZERO);
assert!(quote.ask_size > Decimal::ZERO);
assert_eq!(quote.spread, quote.ask_price - quote.bid_price);
}
#[test]
fn test_asymmetric_spread_long_position() {
let config = AvellanedaStoikovConfig::default();
let mm = AvellanedaStoikovMarketMaker::new(config, Uuid::new_v4());
let state = MarketState {
mid_price: dec!(100),
volatility: dec!(0.20),
inventory: dec!(500), time_remaining_secs: 3600,
};
let reservation_price = mm.calculate_reservation_price(&state).unwrap();
let optimal_spread = mm.calculate_optimal_spread(&state).unwrap();
let (bid_price, ask_price) = mm
.calculate_asymmetric_spread(reservation_price, optimal_spread, &state)
.unwrap();
let bid_distance = reservation_price - bid_price;
let ask_distance = ask_price - reservation_price;
assert!(ask_distance > bid_distance);
}
#[test]
fn test_ln_approximation() {
let config = AvellanedaStoikovConfig::default();
let mm = AvellanedaStoikovMarketMaker::new(config, Uuid::new_v4());
let result = mm.ln_approximation(dec!(1));
assert!(result.abs() < dec!(0.01));
let result = mm.ln_approximation(dec!(2.718));
assert!((result - dec!(1)).abs() < dec!(0.1));
}
#[test]
fn test_expected_profit() {
let config = AvellanedaStoikovConfig::default();
let mm = AvellanedaStoikovMarketMaker::new(config, Uuid::new_v4());
let state = MarketState {
mid_price: dec!(100),
volatility: dec!(0.20),
inventory: Decimal::ZERO,
time_remaining_secs: 3600,
};
let quote = mm.generate_quote(&state, dec!(10)).unwrap();
let expected_profit = mm.calculate_expected_profit(&state, "e).unwrap();
assert!(expected_profit > Decimal::ZERO);
}
#[test]
fn test_inventory_risk() {
let config = AvellanedaStoikovConfig::default();
let mm = AvellanedaStoikovMarketMaker::new(config, Uuid::new_v4());
let state_neutral = MarketState {
mid_price: dec!(100),
volatility: dec!(0.20),
inventory: Decimal::ZERO,
time_remaining_secs: 3600,
};
let state_long = MarketState {
mid_price: dec!(100),
volatility: dec!(0.20),
inventory: dec!(100),
time_remaining_secs: 3600,
};
let risk_neutral = mm.calculate_inventory_risk(&state_neutral).unwrap();
let risk_long = mm.calculate_inventory_risk(&state_long).unwrap();
assert_eq!(risk_neutral, Decimal::ZERO);
assert!(risk_long > Decimal::ZERO);
}
#[test]
fn test_optimal_sizes_inventory_adjustment() {
let config = AvellanedaStoikovConfig::default();
let mm = AvellanedaStoikovMarketMaker::new(config, Uuid::new_v4());
let state = MarketState {
mid_price: dec!(100),
volatility: dec!(0.20),
inventory: dec!(500), time_remaining_secs: 3600,
};
let (bid_size, ask_size) = mm.calculate_optimal_sizes(&state, dec!(10)).unwrap();
assert!(bid_size > Decimal::ZERO);
assert!(ask_size > Decimal::ZERO);
}
}