1use super::Strategy;
2use crate::types::{Observation, Order, OrderRequest, Side};
3use solver::lookup::LookupTable;
4use std::sync::Arc;
5
6#[derive(Clone, Debug)]
11pub struct KellyRigorousParameters {
12 pub t_horizon: f64,
14}
15
16#[derive(Clone)]
30pub struct KellyRigorousStrategy {
31 pub params: KellyRigorousParameters,
32 pub bid_spread_table: Arc<LookupTable>,
33 pub ask_spread_table: Arc<LookupTable>,
34 q_min: f64,
35 q_max: f64,
36 start_time: Option<f64>,
37 order_counter: u64,
38}
39
40impl KellyRigorousStrategy {
41 pub fn new(
42 params: KellyRigorousParameters,
43 bid_spread_table: Arc<LookupTable>,
44 ask_spread_table: Arc<LookupTable>,
45 ) -> Self {
46 let q_axis = &bid_spread_table.axes[0];
47 let q_min = q_axis[0];
48 let q_max = q_axis[q_axis.len() - 1];
49 Self {
50 params,
51 bid_spread_table,
52 ask_spread_table,
53 q_min,
54 q_max,
55 start_time: None,
56 order_counter: 0,
57 }
58 }
59}
60
61impl Strategy for KellyRigorousStrategy {
62 fn on_tick(&mut self, obs: &Observation, requests: &mut Vec<OrderRequest>) {
63 if self.start_time.is_none() {
64 self.start_time = Some(obs.timestamp);
65 }
66
67 let elapsed = obs.timestamp - self.start_time.unwrap();
68 let tau = (self.params.t_horizon - elapsed).max(0.0);
69
70 if tau <= 0.0 {
71 requests.push(OrderRequest::CancelAll);
72 return;
73 }
74
75 let mid = obs.mid_price();
76 let q = obs.portfolio.position;
77 let cash = obs.portfolio.cash;
78 let x = if mid > 0.0 { cash / mid } else { 1.0 };
79
80 let delta_bid = self.bid_spread_table.interpolate(&[q, x, tau]).max(1e-9);
81 let delta_ask = self.ask_spread_table.interpolate(&[q, x, tau]).max(1e-9);
82
83 let bid_price = mid - delta_bid;
84 let ask_price = mid + delta_ask;
85
86 let place_bid = q < self.q_max;
87 let place_ask = q > self.q_min;
88
89 requests.push(OrderRequest::CancelAll);
90 if place_bid {
91 self.order_counter += 1;
92 requests.push(OrderRequest::New(Order::new(
93 self.order_counter,
94 Side::Buy,
95 bid_price,
96 1.0,
97 )));
98 }
99 if place_ask {
100 self.order_counter += 1;
101 requests.push(OrderRequest::New(Order::new(
102 self.order_counter,
103 Side::Sell,
104 ask_price,
105 1.0,
106 )));
107 }
108 }
109
110 fn as_any(&self) -> &dyn std::any::Any {
111 self
112 }
113 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
114 self
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::types::PortfolioSnapshot;
122 use ndarray::ArrayD;
123 use ndarray::IxDyn;
124 use std::sync::Arc;
125
126 fn make_obs(mid: f64, position: f64, cash: f64) -> Observation {
127 Observation {
128 timestamp: 0.0,
129 best_bid: mid - 0.05,
130 best_ask: mid + 0.05,
131 last_price: Some(mid),
132 portfolio: PortfolioSnapshot { cash, position },
133 volatility: None,
134 drift: None,
135 parameters: None,
136 }
137 }
138
139 fn make_3d_table(values: &[f64]) -> LookupTable {
140 let axes = vec![
141 vec![-1.0, 0.0, 1.0], vec![1.0, 5.0, 10.0], vec![0.0, 0.5, 1.0], ];
145 let data = ArrayD::from_shape_vec(IxDyn(&[3, 3, 3]), values.to_vec()).unwrap();
146 LookupTable::new(axes, data)
147 }
148
149 #[test]
150 fn happy_path_places_bid_and_ask() {
151 let spreads = vec![0.01; 27];
152 let bid_table = Arc::new(make_3d_table(&spreads));
153 let ask_table = Arc::new(make_3d_table(&spreads));
154 let mut strategy = KellyRigorousStrategy::new(
155 KellyRigorousParameters { t_horizon: 1.0 },
156 bid_table,
157 ask_table,
158 );
159 let obs = make_obs(100.0, 0.0, 10000.0);
160 let mut requests = Vec::new();
161 strategy.on_tick(&obs, &mut requests);
162
163 let bid_count = requests
164 .iter()
165 .filter(|r| matches!(r, OrderRequest::New(o) if o.side == Side::Buy))
166 .count();
167 let ask_count = requests
168 .iter()
169 .filter(|r| matches!(r, OrderRequest::New(o) if o.side == Side::Sell))
170 .count();
171 assert_eq!(bid_count, 1);
172 assert_eq!(ask_count, 1);
173 }
174
175 #[test]
176 fn cancels_all_at_horizon() {
177 let spreads = vec![0.01; 27];
178 let bid_table = Arc::new(make_3d_table(&spreads));
179 let ask_table = Arc::new(make_3d_table(&spreads));
180 let mut strategy = KellyRigorousStrategy::new(
181 KellyRigorousParameters { t_horizon: 0.0 },
182 bid_table,
183 ask_table,
184 );
185 let obs = make_obs(100.0, 0.0, 10000.0);
186 let mut requests = Vec::new();
187 strategy.on_tick(&obs, &mut requests);
188 assert_eq!(requests.len(), 1);
189 assert!(matches!(requests[0], OrderRequest::CancelAll));
190 }
191
192 #[test]
193 fn no_bid_at_q_max() {
194 let spreads = vec![0.01; 27];
195 let bid_table = Arc::new(make_3d_table(&spreads));
196 let ask_table = Arc::new(make_3d_table(&spreads));
197 let mut strategy = KellyRigorousStrategy::new(
198 KellyRigorousParameters { t_horizon: 1.0 },
199 bid_table,
200 ask_table,
201 );
202 let obs = make_obs(100.0, 1.0, 10000.0);
203 let mut requests = Vec::new();
204 strategy.on_tick(&obs, &mut requests);
205 let bid_count = requests
206 .iter()
207 .filter(|r| matches!(r, OrderRequest::New(o) if o.side == Side::Buy))
208 .count();
209 assert_eq!(bid_count, 0, "should not bid at q=q_max");
210 }
211}