Skip to main content

solver/models/
market_making.rs

1//! Market-making control and spread conversion.
2//!
3//! The generic [`crate::models::control::ControlProblem`] abstraction is
4//! agnostic to what a control is. This module supplies the market-making
5//! instantiation: the control is a pair of optimal bid/ask fill intensities,
6//! and spreads are recovered from those intensities through the exponential
7//! fill-rate formula.
8
9/// Optimal market-making control: a pair of fill intensities.
10///
11/// `bid_intensity` is the rate at which the market maker's bid quote is filled
12/// (sell market orders hitting the bid); `ask_intensity` is the rate at which
13/// its ask quote is filled (buy market orders hitting the ask).
14#[derive(Clone, Copy, Debug, PartialEq)]
15pub struct MarketMakingControl {
16    /// Optimal bid fill intensity.
17    pub bid_intensity: f64,
18    /// Optimal ask fill intensity.
19    pub ask_intensity: f64,
20}
21
22impl MarketMakingControl {
23    /// Creates a market-making control from bid and ask intensities.
24    ///
25    /// # Examples
26    ///
27    /// ```
28    /// use solver::models::market_making::MarketMakingControl;
29    /// let c = MarketMakingControl::new(1.5, 2.0);
30    /// assert_eq!(c.bid_intensity, 1.5);
31    /// assert_eq!(c.ask_intensity, 2.0);
32    /// ```
33    pub fn new(bid_intensity: f64, ask_intensity: f64) -> Self {
34        Self {
35            bid_intensity,
36            ask_intensity,
37        }
38    }
39
40    /// Recovers the bid/ask half-spreads from the fill intensities.
41    ///
42    /// Inverts the exponential fill model `lambda = a * exp(-kappa * delta)`.
43    ///
44    /// # Panics
45    ///
46    /// Panics if `a` or `kappa` is not positive.
47    ///
48    /// # Examples
49    ///
50    /// ```
51    /// use solver::models::market_making::MarketMakingControl;
52    /// let a: f64 = 140.0;
53    /// let kappa: f64 = 1.5;
54    /// let c = MarketMakingControl::new(a * (-1.5_f64).exp(), a * (-2.0_f64).exp());
55    /// let spreads = c.to_spreads(a, kappa);
56    /// assert!((spreads.bid_spread - 1.0).abs() < 1e-12);
57    /// assert!((spreads.ask_spread - 2.0 / 1.5).abs() < 1e-12);
58    /// ```
59    pub fn to_spreads(&self, a: f64, kappa: f64) -> SpreadResult {
60        assert!(a > 0.0, "base arrival rate must be positive");
61        assert!(kappa > 0.0, "fill-rate decay must be positive");
62        SpreadResult::from_intensities(self.bid_intensity, self.ask_intensity, a, kappa)
63    }
64}
65
66/// Market-making spreads derived from fill intensities.
67///
68/// Computed by inverting the exponential fill-rate formula
69/// `delta = -ln(lambda / a) / kappa`.
70#[derive(Debug, Clone, PartialEq)]
71pub struct SpreadResult {
72    /// Optimal bid spread (distance from mid to bid).
73    pub bid_spread: f64,
74
75    /// Optimal ask spread (distance from mid to ask).
76    pub ask_spread: f64,
77
78    /// Optimal bid intensity: `a * exp(-kappa * bid_spread)`.
79    pub bid_intensity: f64,
80
81    /// Optimal ask intensity: `a * exp(-kappa * ask_spread)`.
82    pub ask_intensity: f64,
83}
84
85impl SpreadResult {
86    /// Builds a [`SpreadResult`] from raw fill intensities.
87    pub fn from_intensities(bid_intensity: f64, ask_intensity: f64, a: f64, kappa: f64) -> Self {
88        let bid_spread = -(bid_intensity / a).ln() / kappa;
89        let ask_spread = -(ask_intensity / a).ln() / kappa;
90        Self {
91            bid_spread,
92            ask_spread,
93            bid_intensity,
94            ask_intensity,
95        }
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn spread_conversion_inverts_exponential_fill() {
105        let a: f64 = 140.0;
106        let kappa: f64 = 1.5;
107        let c = MarketMakingControl::new(a * (-kappa * 1.0).exp(), a * (-kappa * 1.5).exp());
108        let spreads = c.to_spreads(a, kappa);
109        assert!((spreads.bid_spread - 1.0).abs() < 1e-12);
110        assert!((spreads.ask_spread - 1.5).abs() < 1e-12);
111    }
112
113    #[test]
114    fn intensities_round_trip() {
115        let a = 100.0;
116        let kappa = 2.0;
117        let spreads = SpreadResult::from_intensities(20.0, 30.0, a, kappa);
118        assert!((spreads.bid_intensity - 20.0).abs() < 1e-12);
119        assert!((spreads.ask_intensity - 30.0).abs() < 1e-12);
120    }
121}