use crate::Decimal;
use crate::types::decimal::{decimal_ln, decimal_powi};
use crate::types::error::{MMError, MMResult};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
const SECONDS_PER_MILLISECOND: Decimal = Decimal::from_parts(1, 0, 0, false, 3); const SECONDS_PER_YEAR: Decimal = Decimal::from_parts(31_536_000, 0, 0, false, 0);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum PenaltyFunction {
#[default]
Linear,
Exponential,
Quadratic,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GLFTConfig {
pub risk_aversion: Decimal,
pub order_intensity: Decimal,
pub terminal_penalty: Decimal,
pub terminal_time: u64,
pub min_spread: Decimal,
pub dynamic_gamma: bool,
pub gamma_scaling_factor: Decimal,
pub penalty_function: PenaltyFunction,
}
impl GLFTConfig {
pub fn new(
risk_aversion: Decimal,
order_intensity: Decimal,
terminal_penalty: Decimal,
terminal_time: u64,
min_spread: Decimal,
) -> MMResult<Self> {
if risk_aversion <= Decimal::ZERO {
return Err(MMError::InvalidConfiguration(
"risk_aversion must be positive".to_string(),
));
}
if order_intensity <= Decimal::ZERO {
return Err(MMError::InvalidConfiguration(
"order_intensity must be positive".to_string(),
));
}
if terminal_penalty < Decimal::ZERO {
return Err(MMError::InvalidConfiguration(
"terminal_penalty must be non-negative".to_string(),
));
}
if terminal_time == 0 {
return Err(MMError::InvalidConfiguration(
"terminal_time must be positive".to_string(),
));
}
if min_spread < Decimal::ZERO {
return Err(MMError::InvalidConfiguration(
"min_spread must be non-negative".to_string(),
));
}
Ok(Self {
risk_aversion,
order_intensity,
terminal_penalty,
terminal_time,
min_spread,
dynamic_gamma: false,
gamma_scaling_factor: Decimal::ONE,
penalty_function: PenaltyFunction::default(),
})
}
#[must_use]
pub fn with_dynamic_gamma(mut self, scaling_factor: Decimal) -> Self {
self.dynamic_gamma = true;
self.gamma_scaling_factor = scaling_factor;
self
}
#[must_use]
pub fn with_penalty_function(mut self, penalty_function: PenaltyFunction) -> Self {
self.penalty_function = penalty_function;
self
}
}
pub struct GLFTStrategy;
impl GLFTStrategy {
pub fn calculate_reservation_price(
mid_price: Decimal,
inventory: Decimal,
config: &GLFTConfig,
volatility: Decimal,
current_time: u64,
) -> MMResult<Decimal> {
if mid_price <= Decimal::ZERO {
return Err(MMError::InvalidMarketState(
"mid_price must be positive".to_string(),
));
}
if volatility <= Decimal::ZERO {
return Err(MMError::InvalidMarketState(
"volatility must be positive".to_string(),
));
}
let time_to_terminal_ms = config.terminal_time.saturating_sub(current_time);
let effective_gamma = Self::calculate_dynamic_gamma(
config.risk_aversion,
time_to_terminal_ms,
config.terminal_time,
config.dynamic_gamma,
config.gamma_scaling_factor,
);
let time_to_terminal_years = Self::ms_to_years(time_to_terminal_ms);
let volatility_squared = decimal_powi(volatility, 2)?;
let as_adjustment =
inventory * effective_gamma * volatility_squared * time_to_terminal_years;
let penalty_value = Self::calculate_penalty_function(
time_to_terminal_ms,
config.terminal_time,
config.penalty_function,
);
let terminal_adjustment = inventory * config.terminal_penalty * penalty_value;
let reservation_price = mid_price - as_adjustment - terminal_adjustment;
Ok(reservation_price)
}
pub fn calculate_optimal_spread(
config: &GLFTConfig,
volatility: Decimal,
current_time: u64,
) -> MMResult<Decimal> {
if volatility <= Decimal::ZERO {
return Err(MMError::InvalidMarketState(
"volatility must be positive".to_string(),
));
}
let time_to_terminal_ms = config.terminal_time.saturating_sub(current_time);
let effective_gamma = Self::calculate_dynamic_gamma(
config.risk_aversion,
time_to_terminal_ms,
config.terminal_time,
config.dynamic_gamma,
config.gamma_scaling_factor,
);
let time_to_terminal_years = Self::ms_to_years(time_to_terminal_ms);
let volatility_squared = decimal_powi(volatility, 2)?;
let inventory_risk_term = effective_gamma * volatility_squared * time_to_terminal_years;
let adverse_selection_inner = Decimal::ONE + effective_gamma / config.order_intensity;
let adverse_selection_ln = decimal_ln(adverse_selection_inner)?;
let adverse_selection_term = (Decimal::TWO / effective_gamma) * adverse_selection_ln;
let spread = inventory_risk_term + adverse_selection_term;
Ok(spread.max(config.min_spread))
}
pub fn calculate_optimal_quotes(
mid_price: Decimal,
inventory: Decimal,
config: &GLFTConfig,
volatility: Decimal,
current_time: u64,
) -> MMResult<(Decimal, Decimal)> {
let reservation_price = Self::calculate_reservation_price(
mid_price,
inventory,
config,
volatility,
current_time,
)?;
let spread = Self::calculate_optimal_spread(config, volatility, current_time)?;
let half_spread = spread / Decimal::TWO;
let bid_price = reservation_price - half_spread;
let ask_price = reservation_price + half_spread;
if bid_price >= ask_price {
return Err(MMError::InvalidQuoteGeneration(
"bid price must be less than ask price".to_string(),
));
}
if bid_price <= Decimal::ZERO {
return Err(MMError::InvalidQuoteGeneration(
"bid price must be positive".to_string(),
));
}
Ok((bid_price, ask_price))
}
#[must_use]
pub fn calculate_dynamic_gamma(
base_gamma: Decimal,
time_to_terminal_ms: u64,
total_session_ms: u64,
dynamic_enabled: bool,
scaling_factor: Decimal,
) -> Decimal {
if !dynamic_enabled || total_session_ms == 0 {
return base_gamma;
}
let time_ratio = Decimal::from(time_to_terminal_ms) / Decimal::from(total_session_ms);
let time_factor = Decimal::ONE - time_ratio;
base_gamma * (Decimal::ONE + scaling_factor * time_factor)
}
fn calculate_penalty_function(
time_to_terminal_ms: u64,
total_session_ms: u64,
penalty_type: PenaltyFunction,
) -> Decimal {
if total_session_ms == 0 {
return Decimal::ONE;
}
let time_ratio = Decimal::from(time_to_terminal_ms) / Decimal::from(total_session_ms);
match penalty_type {
PenaltyFunction::Linear => {
Decimal::ONE - time_ratio
}
PenaltyFunction::Exponential => {
let neg_ratio = -time_ratio;
Decimal::ONE + neg_ratio + (neg_ratio * neg_ratio) / Decimal::TWO
}
PenaltyFunction::Quadratic => {
let factor = Decimal::ONE - time_ratio;
factor * factor
}
}
}
fn ms_to_years(ms: u64) -> Decimal {
let ms_dec = Decimal::from(ms);
(ms_dec * SECONDS_PER_MILLISECOND) / SECONDS_PER_YEAR
}
pub fn compare_with_avellaneda_stoikov(
mid_price: Decimal,
inventory: Decimal,
config: &GLFTConfig,
volatility: Decimal,
current_time: u64,
) -> MMResult<((Decimal, Decimal), (Decimal, Decimal))> {
let glft_quotes =
Self::calculate_optimal_quotes(mid_price, inventory, config, volatility, current_time)?;
let as_config = GLFTConfig {
terminal_penalty: Decimal::ZERO,
dynamic_gamma: false,
..config.clone()
};
let as_quotes = Self::calculate_optimal_quotes(
mid_price,
inventory,
&as_config,
volatility,
current_time,
)?;
Ok((glft_quotes, as_quotes))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dec;
#[test]
fn test_config_valid() {
let config = GLFTConfig::new(dec!(0.1), dec!(1.5), dec!(0.05), 3_600_000, dec!(0.0001));
assert!(config.is_ok());
}
#[test]
fn test_config_invalid_risk_aversion() {
let config = GLFTConfig::new(dec!(0.0), dec!(1.5), dec!(0.05), 3_600_000, dec!(0.0001));
assert!(config.is_err());
let config = GLFTConfig::new(dec!(-0.1), dec!(1.5), dec!(0.05), 3_600_000, dec!(0.0001));
assert!(config.is_err());
}
#[test]
fn test_config_invalid_order_intensity() {
let config = GLFTConfig::new(dec!(0.1), dec!(0.0), dec!(0.05), 3_600_000, dec!(0.0001));
assert!(config.is_err());
}
#[test]
fn test_config_invalid_terminal_penalty() {
let config = GLFTConfig::new(dec!(0.1), dec!(1.5), dec!(-0.05), 3_600_000, dec!(0.0001));
assert!(config.is_err());
}
#[test]
fn test_config_invalid_terminal_time() {
let config = GLFTConfig::new(dec!(0.1), dec!(1.5), dec!(0.05), 0, dec!(0.0001));
assert!(config.is_err());
}
#[test]
fn test_config_with_dynamic_gamma() {
let config = GLFTConfig::new(dec!(0.1), dec!(1.5), dec!(0.05), 3_600_000, dec!(0.0001))
.unwrap()
.with_dynamic_gamma(dec!(1.5));
assert!(config.dynamic_gamma);
assert_eq!(config.gamma_scaling_factor, dec!(1.5));
}
#[test]
fn test_config_with_penalty_function() {
let config = GLFTConfig::new(dec!(0.1), dec!(1.5), dec!(0.05), 3_600_000, dec!(0.0001))
.unwrap()
.with_penalty_function(PenaltyFunction::Exponential);
assert_eq!(config.penalty_function, PenaltyFunction::Exponential);
}
#[test]
fn test_reservation_price_flat_inventory() {
let config =
GLFTConfig::new(dec!(0.1), dec!(1.5), dec!(0.05), 3_600_000, dec!(0.0001)).unwrap();
let reservation = GLFTStrategy::calculate_reservation_price(
dec!(100.0),
Decimal::ZERO,
&config,
dec!(0.2),
0,
)
.unwrap();
assert!((reservation - dec!(100.0)).abs() < dec!(0.0001));
}
#[test]
fn test_reservation_price_long_inventory() {
let config =
GLFTConfig::new(dec!(0.1), dec!(1.5), dec!(0.05), 3_600_000, dec!(0.0001)).unwrap();
let reservation = GLFTStrategy::calculate_reservation_price(
dec!(100.0),
dec!(10.0),
&config,
dec!(0.2),
0,
)
.unwrap();
assert!(reservation < dec!(100.0));
}
#[test]
fn test_reservation_price_short_inventory() {
let config =
GLFTConfig::new(dec!(0.1), dec!(1.5), dec!(0.05), 3_600_000, dec!(0.0001)).unwrap();
let reservation = GLFTStrategy::calculate_reservation_price(
dec!(100.0),
dec!(-10.0),
&config,
dec!(0.2),
0,
)
.unwrap();
assert!(reservation > dec!(100.0));
}
#[test]
fn test_terminal_penalty_effect() {
let config_with_penalty =
GLFTConfig::new(dec!(0.1), dec!(1.5), dec!(0.1), 3_600_000, dec!(0.0001)).unwrap();
let config_no_penalty =
GLFTConfig::new(dec!(0.1), dec!(1.5), dec!(0.0), 3_600_000, dec!(0.0001)).unwrap();
let reservation_with = GLFTStrategy::calculate_reservation_price(
dec!(100.0),
dec!(10.0),
&config_with_penalty,
dec!(0.2),
1_800_000, )
.unwrap();
let reservation_without = GLFTStrategy::calculate_reservation_price(
dec!(100.0),
dec!(10.0),
&config_no_penalty,
dec!(0.2),
1_800_000,
)
.unwrap();
assert!(reservation_with < reservation_without);
}
#[test]
fn test_terminal_penalty_increases_near_end() {
let config =
GLFTConfig::new(dec!(0.1), dec!(1.5), dec!(0.1), 3_600_000, dec!(0.0001)).unwrap();
let reservation_early = GLFTStrategy::calculate_reservation_price(
dec!(100.0),
dec!(10.0),
&config,
dec!(0.2),
0, )
.unwrap();
let reservation_late = GLFTStrategy::calculate_reservation_price(
dec!(100.0),
dec!(10.0),
&config,
dec!(0.2),
3_500_000, )
.unwrap();
assert!(reservation_late < reservation_early);
}
#[test]
fn test_dynamic_gamma_at_start() {
let gamma = GLFTStrategy::calculate_dynamic_gamma(
dec!(0.1),
3_600_000, 3_600_000,
true,
dec!(1.0),
);
assert_eq!(gamma, dec!(0.1));
}
#[test]
fn test_dynamic_gamma_at_end() {
let gamma = GLFTStrategy::calculate_dynamic_gamma(
dec!(0.1),
0, 3_600_000,
true,
dec!(1.0),
);
assert_eq!(gamma, dec!(0.2));
}
#[test]
fn test_dynamic_gamma_halfway() {
let gamma = GLFTStrategy::calculate_dynamic_gamma(
dec!(0.1),
1_800_000, 3_600_000,
true,
dec!(1.0),
);
assert_eq!(gamma, dec!(0.15));
}
#[test]
fn test_dynamic_gamma_disabled() {
let gamma = GLFTStrategy::calculate_dynamic_gamma(
dec!(0.1),
0, 3_600_000,
false, dec!(1.0),
);
assert_eq!(gamma, dec!(0.1));
}
#[test]
fn test_optimal_spread_positive() {
let config =
GLFTConfig::new(dec!(0.1), dec!(1.5), dec!(0.05), 3_600_000, dec!(0.0001)).unwrap();
let spread = GLFTStrategy::calculate_optimal_spread(&config, dec!(0.2), 0).unwrap();
assert!(spread > Decimal::ZERO);
}
#[test]
fn test_optimal_spread_min_constraint() {
let config =
GLFTConfig::new(dec!(0.1), dec!(1.5), dec!(0.05), 3_600_000, dec!(1.0)).unwrap();
let spread =
GLFTStrategy::calculate_optimal_spread(&config, dec!(0.001), 3_599_999).unwrap();
assert!(spread >= dec!(1.0));
}
#[test]
fn test_optimal_quotes_valid() {
let config =
GLFTConfig::new(dec!(0.1), dec!(1.5), dec!(0.05), 3_600_000, dec!(0.0001)).unwrap();
let (bid, ask) = GLFTStrategy::calculate_optimal_quotes(
dec!(100.0),
Decimal::ZERO,
&config,
dec!(0.2),
0,
)
.unwrap();
assert!(bid < ask);
assert!(bid > Decimal::ZERO);
}
#[test]
fn test_optimal_quotes_with_inventory() {
let config =
GLFTConfig::new(dec!(0.1), dec!(1.5), dec!(0.05), 3_600_000, dec!(0.0001)).unwrap();
let (bid_flat, ask_flat) = GLFTStrategy::calculate_optimal_quotes(
dec!(100.0),
Decimal::ZERO,
&config,
dec!(0.2),
0,
)
.unwrap();
let (bid_long, ask_long) =
GLFTStrategy::calculate_optimal_quotes(dec!(100.0), dec!(10.0), &config, dec!(0.2), 0)
.unwrap();
assert!(bid_long < bid_flat);
assert!(ask_long < ask_flat);
}
#[test]
fn test_compare_with_as() {
let config = GLFTConfig::new(dec!(0.1), dec!(1.5), dec!(0.1), 3_600_000, dec!(0.0001))
.unwrap()
.with_dynamic_gamma(dec!(1.0));
let ((glft_bid, glft_ask), (as_bid, as_ask)) =
GLFTStrategy::compare_with_avellaneda_stoikov(
dec!(100.0),
dec!(10.0),
&config,
dec!(0.2),
1_800_000,
)
.unwrap();
assert!(glft_bid < as_bid);
assert!(glft_ask < as_ask);
}
#[test]
fn test_penalty_function_linear() {
let penalty_start =
GLFTStrategy::calculate_penalty_function(3_600_000, 3_600_000, PenaltyFunction::Linear);
assert_eq!(penalty_start, dec!(0.0));
let penalty_end =
GLFTStrategy::calculate_penalty_function(0, 3_600_000, PenaltyFunction::Linear);
assert_eq!(penalty_end, dec!(1.0));
let penalty_half =
GLFTStrategy::calculate_penalty_function(1_800_000, 3_600_000, PenaltyFunction::Linear);
assert_eq!(penalty_half, dec!(0.5));
}
#[test]
fn test_penalty_function_quadratic() {
let penalty_start = GLFTStrategy::calculate_penalty_function(
3_600_000,
3_600_000,
PenaltyFunction::Quadratic,
);
assert_eq!(penalty_start, dec!(0.0));
let penalty_end =
GLFTStrategy::calculate_penalty_function(0, 3_600_000, PenaltyFunction::Quadratic);
assert_eq!(penalty_end, dec!(1.0));
let penalty_half = GLFTStrategy::calculate_penalty_function(
1_800_000,
3_600_000,
PenaltyFunction::Quadratic,
);
assert_eq!(penalty_half, dec!(0.25));
}
#[test]
fn test_invalid_mid_price() {
let config =
GLFTConfig::new(dec!(0.1), dec!(1.5), dec!(0.05), 3_600_000, dec!(0.0001)).unwrap();
let result = GLFTStrategy::calculate_reservation_price(
dec!(0.0),
Decimal::ZERO,
&config,
dec!(0.2),
0,
);
assert!(result.is_err());
}
#[test]
fn test_invalid_volatility() {
let config =
GLFTConfig::new(dec!(0.1), dec!(1.5), dec!(0.05), 3_600_000, dec!(0.0001)).unwrap();
let result = GLFTStrategy::calculate_reservation_price(
dec!(100.0),
Decimal::ZERO,
&config,
dec!(0.0),
0,
);
assert!(result.is_err());
}
#[cfg(feature = "serde")]
#[test]
fn test_serialization() {
let config =
GLFTConfig::new(dec!(0.1), dec!(1.5), dec!(0.05), 3_600_000, dec!(0.0001)).unwrap();
let json = serde_json::to_string(&config).unwrap();
let deserialized: GLFTConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config, deserialized);
}
}