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}