1use super::Strategy;
2use crate::types::{Observation, Order, OrderRequest, Side};
3use solver::lookup::LookupTable;
4use std::sync::Arc;
5
6#[derive(Clone, Debug)]
17pub struct AvellanedaStoikovBilateralHawkesOrderFlowImbalanceParameters {
18 pub t_horizon: f64,
20 pub base_intensity_buy: f64,
22 pub base_intensity_sell: f64,
24}
25
26#[derive(Clone)]
44pub struct AvellanedaStoikovBilateralHawkesOrderFlowImbalanceStrategy {
45 pub params: AvellanedaStoikovBilateralHawkesOrderFlowImbalanceParameters,
46 pub bid_spread_table: Arc<LookupTable>,
48 pub ask_spread_table: Arc<LookupTable>,
50 q_min: f64,
51 q_max: f64,
52 start_time: Option<f64>,
53 order_counter: u64,
54}
55
56impl AvellanedaStoikovBilateralHawkesOrderFlowImbalanceStrategy {
57 pub fn new(
58 params: AvellanedaStoikovBilateralHawkesOrderFlowImbalanceParameters,
59 bid_spread_table: Arc<LookupTable>,
60 ask_spread_table: Arc<LookupTable>,
61 ) -> Self {
62 assert_eq!(
63 bid_spread_table.axes.len(),
64 4,
65 "bid table must be 4D [q, lambda+, lambda-, tau]"
66 );
67 assert_eq!(
68 ask_spread_table.axes.len(),
69 4,
70 "ask table must be 4D [q, lambda+, lambda-, tau]"
71 );
72 let q_axis = &bid_spread_table.axes[0];
73 assert!(!q_axis.is_empty(), "q axis must not be empty");
74 let q_min = q_axis[0];
75 let q_max = q_axis[q_axis.len() - 1];
76 Self {
77 params,
78 bid_spread_table,
79 ask_spread_table,
80 q_min,
81 q_max,
82 start_time: None,
83 order_counter: 0,
84 }
85 }
86}
87
88impl Strategy for AvellanedaStoikovBilateralHawkesOrderFlowImbalanceStrategy {
89 fn on_tick(&mut self, obs: &Observation, requests: &mut Vec<OrderRequest>) {
90 if self.start_time.is_none() {
91 self.start_time = Some(obs.timestamp);
92 }
93
94 let elapsed = obs.timestamp - self.start_time.unwrap();
95 let tau = (self.params.t_horizon - elapsed).max(0.0);
96
97 if tau <= 0.0 {
98 requests.push(OrderRequest::CancelAll);
99 return;
100 }
101
102 let mid = obs.mid_price();
103 let q = obs.portfolio.position;
104
105 let params = obs.parameters.as_ref();
106
107 let lambda_plus = params
109 .and_then(|p| p.get("hawkes_buy_intensity").copied())
110 .unwrap_or(self.params.base_intensity_buy);
111
112 let lambda_minus = params
114 .and_then(|p| p.get("hawkes_sell_intensity").copied())
115 .unwrap_or(self.params.base_intensity_sell);
116
117 let delta_bid = self
119 .bid_spread_table
120 .interpolate(&[q, lambda_plus, lambda_minus, tau])
121 .max(1e-9);
122 let delta_ask = self
123 .ask_spread_table
124 .interpolate(&[q, lambda_plus, lambda_minus, tau])
125 .max(1e-9);
126
127 let bid_price = mid - delta_bid;
129 let ask_price = mid + delta_ask;
130 let place_bid = q < self.q_max;
134 let place_ask = q > self.q_min;
135
136 requests.push(OrderRequest::CancelAll);
137 if place_bid {
138 self.order_counter += 1;
139 let bid_id = self.order_counter;
140 requests.push(OrderRequest::New(Order::new(
141 bid_id,
142 Side::Buy,
143 bid_price,
144 1.0,
145 )));
146 }
147 if place_ask {
148 self.order_counter += 1;
149 let ask_id = self.order_counter;
150 requests.push(OrderRequest::New(Order::new(
151 ask_id,
152 Side::Sell,
153 ask_price,
154 1.0,
155 )));
156 }
157 }
158
159 fn as_any(&self) -> &dyn std::any::Any {
160 self
161 }
162 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
163 self
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::types::PortfolioSnapshot;
171 use ndarray::{ArrayD, IxDyn};
172 use solver::lookup::LookupTable;
173
174 fn flat_table_4d(axes: Vec<Vec<f64>>, value: f64) -> LookupTable {
175 let shape: Vec<usize> = axes.iter().map(|a| a.len()).collect();
176 let n: usize = shape.iter().product();
177 let data = ArrayD::from_shape_vec(IxDyn(&shape), vec![value; n]).unwrap();
178 LookupTable::new(axes, data)
179 }
180
181 #[allow(dead_code)]
182 fn make_obs_ofi(position: f64, lambda_plus: f64, lambda_minus: f64) -> Observation {
183 let mut params = std::collections::HashMap::new();
184 params.insert("hawkes_buy_intensity".to_string(), lambda_plus);
185 params.insert("hawkes_sell_intensity".to_string(), lambda_minus);
186 Observation {
187 timestamp: 0.0,
188 best_bid: 99.95,
189 best_ask: 100.05,
190 last_price: Some(100.0),
191 portfolio: PortfolioSnapshot {
192 cash: 10_000.0,
193 position: position,
194 },
195 volatility: None,
196 drift: None,
197 parameters: Some(params),
198 }
199 }
200
201 #[test]
202 fn test_ofi_strategy_creation() {
203 let axes = vec![
204 vec![-10.0, 0.0, 10.0],
205 vec![0.0, 1.0, 2.0],
206 vec![0.0, 1.0, 2.0],
207 vec![0.0, 500.0, 1000.0],
208 ];
209 let bid_tbl = flat_table_4d(axes.clone(), 0.05);
210 let ask_tbl = flat_table_4d(axes, 0.05);
211
212 let params = AvellanedaStoikovBilateralHawkesOrderFlowImbalanceParameters {
213 t_horizon: 1000.0,
214 base_intensity_buy: 1.0,
215 base_intensity_sell: 1.0,
216 };
217
218 let strategy = AvellanedaStoikovBilateralHawkesOrderFlowImbalanceStrategy::new(
219 params,
220 Arc::new(bid_tbl),
221 Arc::new(ask_tbl),
222 );
223
224 assert_eq!(strategy.params.t_horizon, 1000.0);
225 }
226}