Skip to main content

solver/models/
stationary_avellaneda.rs

1//! Infinite-horizon Avellaneda-Stoikov market making in inventory space.
2//!
3//! This is a validating instance of the ergodic stationary path
4//! ([`crate::numeric::ergodic`]). It is deliberately not an
5//! [`crate::numeric::finite_difference::elliptic::EllipticControlProblem`]:
6//! the undiscounted market-making HJB is translation invariant and has no
7//! unique solution of `sup_u { f + L^u V } = 0`. Its exact
8//! `T -> infinity` limit is the principal eigenvector of the GLT operator.
9//!
10//! # Problem and conditions
11//!
12//! State: inventory `q in Z`. Control: bid/ask half-spreads
13//! `(delta_b, delta_a)`. Fill intensities are
14//!
15//! ```text
16//! lambda_b = a exp(-kappa delta_b)
17//! lambda_a = a exp(-kappa delta_a)
18//! ```
19//!
20//! The finite-horizon CARA value reduces to `theta(t, q)`; see
21//! `docs/src/reference/soc_exact.md`.
22//!
23//! # Exact solution
24//!
25//! Let `v_q = exp(kappa theta_q)`. The GLT substitution gives the linear
26//! system
27//!
28//! ```text
29//! v_q_t = alpha q^2 v_q - eta (v_{q-1} + v_{q+1})
30//! alpha = (kappa / 2) gamma sigma^2
31//! eta   = a (1 + gamma/kappa)^-(1 + kappa/gamma)
32//! ```
33//!
34//! As `T -> infinity` the solution concentrates on the principal (Perron)
35//! eigenvector of the negated operator, assembled here as
36//!
37//! ```text
38//! A_qq = -alpha q^2,   A_{q,q+1} = A_{q,q-1} = +eta
39//! ```
40//!
41//! The eigenvector is normalized by `theta(0) = 0`:
42//!
43//! ```text
44//! theta(q) = (1/kappa) ln(v_q / v_0)
45//! ```
46//!
47//! Optimal stationary spreads are recovered as
48//!
49//! ```text
50//! delta_b = base + theta(q) - theta(q+1)
51//! delta_a = base + theta(q) - theta(q-1)
52//! base    = (1/gamma) ln(1 + gamma/kappa)
53//! ```
54//!
55//! See [`crate::analytical::avellaneda::approximations::gueant::AvellanedaGueant`]
56//! for the reference implementation used in tests.
57use crate::core::grid::Grid;
58use crate::linalg::csr::CsrMatrix;
59use crate::numeric::ergodic::StationaryEigenProblem;
60
61/// Infinite-horizon Avellaneda-Stoikov market making model in inventory space.
62///
63/// See the module-level documentation for the formulation and exact solution.
64#[derive(Clone, Debug)]
65pub struct StationaryAvellaneda {
66    /// Risk aversion for inventory holding.
67    pub gamma: f64,
68    /// Order filling intensity decay (`kappa`).
69    pub kappa: f64,
70    /// Base order arrival intensity (`A`).
71    pub a: f64,
72    /// Mid-price volatility.
73    pub sigma: f64,
74}
75
76impl StationaryAvellaneda {
77    /// Creates a stationary Avellaneda model.
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use solver::models::stationary_avellaneda::StationaryAvellaneda;
83    /// let model = StationaryAvellaneda::new(0.1, 1.5, 140.0, 0.1);
84    /// assert_eq!(model.gamma, 0.1);
85    /// ```
86    pub fn new(gamma: f64, kappa: f64, a: f64, sigma: f64) -> Self {
87        Self {
88            gamma,
89            kappa,
90            a,
91            sigma,
92        }
93    }
94
95    /// The exact base half-spread at zero inventory.
96    pub fn base_spread(&self) -> f64 {
97        (1.0 / self.gamma) * (1.0 + self.gamma / self.kappa).ln()
98    }
99
100    /// The `alpha` diagonal coefficient of the GLT operator.
101    pub fn alpha(&self) -> f64 {
102        (self.kappa / 2.0) * self.gamma * self.sigma.powi(2)
103    }
104
105    /// The `eta` off-diagonal coefficient of the GLT operator.
106    pub fn eta(&self) -> f64 {
107        self.a * (1.0 + self.gamma / self.kappa).powf(-(1.0 + self.kappa / self.gamma))
108    }
109
110    /// Builds the symmetric GLT operator with the Perron sign convention.
111    fn glt_operator(&self, grid: &Grid<1>) -> CsrMatrix {
112        let n = grid.total_size();
113        let mut mat = CsrMatrix::new(n, n * 3);
114        let q0 = grid.min[0].round() as i32;
115        let alpha = self.alpha();
116        let eta = self.eta();
117
118        for i in 0..n {
119            let q = q0 + i as i32;
120            if i > 0 {
121                mat.add_entry(i - 1, eta);
122            }
123            mat.add_entry(i, -alpha * (q as f64).powi(2));
124            if i + 1 < n {
125                mat.add_entry(i + 1, eta);
126            }
127            mat.finish_row();
128        }
129
130        mat
131    }
132}
133
134impl StationaryEigenProblem<1> for StationaryAvellaneda {
135    fn operator(&self, grid: &Grid<1>) -> CsrMatrix {
136        self.glt_operator(grid)
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    fn default_model() -> StationaryAvellaneda {
145        StationaryAvellaneda::new(0.1, 1.5, 140.0, 0.1)
146    }
147
148    #[test]
149    fn base_spread_matches_reference() {
150        let model = default_model();
151        let base = (1.0 / model.gamma) * (1.0 + model.gamma / model.kappa).ln();
152        assert!((model.base_spread() - base).abs() < 1e-12);
153    }
154
155    #[test]
156    fn glt_coefficients_match_reference() {
157        let model = default_model();
158        let alpha = (model.kappa / 2.0) * model.gamma * model.sigma.powi(2);
159        let eta =
160            model.a * (1.0 + model.gamma / model.kappa).powf(-(1.0 + model.kappa / model.gamma));
161        assert!((model.alpha() - alpha).abs() < 1e-12);
162        assert!((model.eta() - eta).abs() < 1e-12);
163    }
164
165    #[test]
166    fn operator_is_symmetric_tridiagonal() {
167        let model = default_model();
168        let grid = Grid::<1>::new([9], [-4.0], [4.0]);
169        let mat = model.operator(&grid);
170
171        assert_eq!(mat.size, 9);
172        for i in 0..mat.size {
173            let start = mat.row_ptr[i];
174            let end = mat.row_ptr[i + 1];
175            assert!(end - start <= 3, "row {i} has too many entries");
176            for idx in start..end {
177                let col = mat.col_indices[idx];
178                assert!(
179                    (col as i32 - i as i32).abs() <= 1,
180                    "row {i} has non-tridiagonal column {col}"
181                );
182            }
183        }
184    }
185}