use crate::error::FinError;
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub struct AlmgrenChrissParams {
pub total_shares: f64,
pub time_steps: usize,
pub volatility: f64,
pub permanent_impact: f64,
pub temporary_impact: f64,
pub risk_aversion: f64,
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub struct TrajectoryStep {
pub step: usize,
pub inventory: f64,
pub trade_size: f64,
pub impact_cost: f64,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct OptimalTrajectory {
pub steps: Vec<TrajectoryStep>,
pub total_permanent_cost: f64,
pub total_temporary_cost: f64,
pub total_expected_cost: f64,
pub cost_variance: f64,
pub objective: f64,
}
pub struct AlmgrenChriss;
impl AlmgrenChriss {
pub fn compute(params: &AlmgrenChrissParams) -> Result<OptimalTrajectory, FinError> {
Self::validate(params)?;
let n = params.time_steps;
let x = params.total_shares;
let sigma = params.volatility;
let gamma = params.permanent_impact;
let eta = params.temporary_impact;
let lambda = params.risk_aversion;
let dt = 1.0_f64;
let eta_tilde = eta - 0.5 * gamma * dt;
let eta_tilde = if eta_tilde <= 0.0 { eta } else { eta_tilde };
let kappa_sq = lambda * sigma * sigma / eta_tilde;
if !kappa_sq.is_finite() || kappa_sq < 0.0 {
return Err(FinError::ArithmeticOverflow);
}
let kappa = kappa_sq.sqrt();
let big_t = n as f64 * dt;
let sinh_kt = (kappa * big_t).sinh();
if sinh_kt.abs() < f64::EPSILON {
return Self::twap_fallback(params);
}
let mut inventories = Vec::with_capacity(n + 1);
for j in 0..=n {
let tau = j as f64 * dt;
let inv = x * (kappa * (big_t - tau)).sinh() / sinh_kt;
inventories.push(inv);
}
let mut steps = Vec::with_capacity(n);
let mut total_perm = 0.0_f64;
let mut total_temp = 0.0_f64;
let mut cost_var_sum = 0.0_f64;
for j in 0..n {
let inv_now = inventories[j];
let inv_next = inventories[j + 1];
let trade = inv_now - inv_next; let trade_rate = trade / dt;
let temp_cost = eta * trade_rate * trade_rate * dt;
let perm_cost = 0.5 * gamma * trade * trade;
let impact_cost = temp_cost + perm_cost;
cost_var_sum += sigma * sigma * inv_now * inv_now * dt;
total_perm += perm_cost;
total_temp += temp_cost;
steps.push(TrajectoryStep {
step: j,
inventory: inv_now,
trade_size: trade,
impact_cost,
});
}
let total_cost = total_perm + total_temp;
let objective = total_cost + lambda * cost_var_sum;
Ok(OptimalTrajectory {
steps,
total_permanent_cost: total_perm,
total_temporary_cost: total_temp,
total_expected_cost: total_cost,
cost_variance: cost_var_sum,
objective,
})
}
fn twap_fallback(params: &AlmgrenChrissParams) -> Result<OptimalTrajectory, FinError> {
let n = params.time_steps;
let x = params.total_shares;
let trade_per_step = x / n as f64;
let mut steps = Vec::with_capacity(n);
let mut total_perm = 0.0;
let mut total_temp = 0.0;
for j in 0..n {
let inv = x - j as f64 * trade_per_step;
let temp_cost = params.temporary_impact * trade_per_step * trade_per_step;
let perm_cost = 0.5 * params.permanent_impact * trade_per_step * trade_per_step;
total_perm += perm_cost;
total_temp += temp_cost;
steps.push(TrajectoryStep {
step: j,
inventory: inv,
trade_size: trade_per_step,
impact_cost: temp_cost + perm_cost,
});
}
let total_cost = total_perm + total_temp;
let cost_var = params.volatility * params.volatility * x * x;
Ok(OptimalTrajectory {
steps,
total_permanent_cost: total_perm,
total_temporary_cost: total_temp,
total_expected_cost: total_cost,
cost_variance: cost_var,
objective: total_cost + params.risk_aversion * cost_var,
})
}
fn validate(p: &AlmgrenChrissParams) -> Result<(), FinError> {
if p.time_steps == 0 {
return Err(FinError::InvalidInput("time_steps must be at least 1".to_owned()));
}
if p.volatility <= 0.0 {
return Err(FinError::InvalidInput("volatility must be positive".to_owned()));
}
if p.permanent_impact < 0.0 {
return Err(FinError::InvalidInput(
"permanent_impact must be non-negative".to_owned(),
));
}
if p.temporary_impact < 0.0 {
return Err(FinError::InvalidInput(
"temporary_impact must be non-negative".to_owned(),
));
}
if p.risk_aversion < 0.0 {
return Err(FinError::InvalidInput("risk_aversion must be non-negative".to_owned()));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_params() -> AlmgrenChrissParams {
AlmgrenChrissParams {
total_shares: 10_000.0,
time_steps: 10,
volatility: 0.02,
permanent_impact: 1e-7,
temporary_impact: 1e-6,
risk_aversion: 1e-5,
}
}
#[test]
fn test_trajectory_has_correct_step_count() {
let traj = AlmgrenChriss::compute(&default_params()).unwrap();
assert_eq!(traj.steps.len(), 10);
}
#[test]
fn test_first_step_inventory_is_total_shares() {
let p = default_params();
let traj = AlmgrenChriss::compute(&p).unwrap();
let first_inv = traj.steps[0].inventory;
assert!((first_inv - p.total_shares).abs() < 1.0, "first inventory: {first_inv}");
}
#[test]
fn test_total_shares_roughly_traded() {
let p = default_params();
let traj = AlmgrenChriss::compute(&p).unwrap();
let total_traded: f64 = traj.steps.iter().map(|s| s.trade_size).sum();
assert!(
(total_traded - p.total_shares).abs() < 1.0,
"total traded {total_traded} vs {}", p.total_shares
);
}
#[test]
fn test_costs_positive() {
let traj = AlmgrenChriss::compute(&default_params()).unwrap();
assert!(traj.total_expected_cost > 0.0);
assert!(traj.cost_variance > 0.0);
}
#[test]
fn test_objective_equals_cost_plus_risk() {
let p = default_params();
let traj = AlmgrenChriss::compute(&p).unwrap();
let expected_obj = traj.total_expected_cost + p.risk_aversion * traj.cost_variance;
assert!((traj.objective - expected_obj).abs() < 1e-6);
}
#[test]
fn test_invalid_params() {
let mut p = default_params();
p.time_steps = 0;
assert!(AlmgrenChriss::compute(&p).is_err());
p = default_params();
p.volatility = -1.0;
assert!(AlmgrenChriss::compute(&p).is_err());
p = default_params();
p.risk_aversion = -1.0;
assert!(AlmgrenChriss::compute(&p).is_err());
}
#[test]
fn test_twap_fallback_zero_risk_aversion() {
let p = AlmgrenChrissParams {
total_shares: 1000.0,
time_steps: 5,
volatility: 0.01,
permanent_impact: 0.0,
temporary_impact: 1e-6,
risk_aversion: 0.0, };
let traj = AlmgrenChriss::compute(&p).unwrap();
assert_eq!(traj.steps.len(), 5);
}
}