1use super::DataSource;
2use crate::types::{FillEvent, MarketState, Side};
3use market_model::process::{
4 StochasticProcess, bates::BatesProcess, gbm::GeometricBrownianMotion, heston::HestonProcess,
5};
6use ndarray::Array1;
7use rand::prelude::*;
8use rand::rng;
9use rand::rngs::StdRng;
10use std::collections::HashMap;
11
12pub trait MarketProcess: StochasticProcess {
13 fn get_price(&self, state: &Self::State) -> f64;
14 fn get_volatility(&self, _state: &Self::State) -> Option<f64> {
15 None
16 }
17 fn get_drift(&self, _state: &Self::State) -> Option<f64> {
18 None
19 }
20 fn get_parameters(&self) -> Option<HashMap<String, f64>> {
21 None
22 }
23 fn add_price_impact(&self, state: &mut Self::State, impact: f64);
24}
25
26impl MarketProcess for GeometricBrownianMotion {
27 fn get_price(&self, state: &f64) -> f64 {
28 *state
29 }
30
31 fn get_volatility(&self, _state: &f64) -> Option<f64> {
32 Some(self.sigma)
33 }
34
35 fn get_drift(&self, _state: &f64) -> Option<f64> {
36 Some(self.mu)
37 }
38
39 fn get_parameters(&self) -> Option<HashMap<String, f64>> {
40 let mut p = HashMap::new();
41 p.insert("mu".to_string(), self.mu);
42 p.insert("sigma".to_string(), self.sigma);
43 Some(p)
44 }
45
46 fn add_price_impact(&self, state: &mut f64, impact: f64) {
47 *state += impact;
48 }
49}
50
51impl MarketProcess for HestonProcess {
52 fn get_price(&self, state: &Array1<f64>) -> f64 {
53 state[0]
54 }
55
56 fn get_volatility(&self, state: &Array1<f64>) -> Option<f64> {
57 Some(state[1].max(0.0).sqrt())
58 }
59
60 fn get_drift(&self, _state: &Array1<f64>) -> Option<f64> {
61 Some(self.mu)
62 }
63
64 fn get_parameters(&self) -> Option<HashMap<String, f64>> {
65 let mut p = HashMap::new();
66 p.insert("mu".to_string(), self.mu);
67 p.insert("kappa".to_string(), self.kappa);
68 p.insert("theta".to_string(), self.theta);
69 p.insert("sigma".to_string(), self.sigma);
70 p.insert("rho".to_string(), self.rho);
71 Some(p)
72 }
73
74 fn add_price_impact(&self, state: &mut Array1<f64>, impact: f64) {
75 state[0] += impact;
76 }
77}
78
79impl MarketProcess for BatesProcess {
80 fn get_price(&self, state: &Array1<f64>) -> f64 {
81 state[0]
82 }
83
84 fn get_volatility(&self, state: &Array1<f64>) -> Option<f64> {
85 Some(state[1].max(0.0).sqrt())
86 }
87
88 fn get_drift(&self, _state: &Array1<f64>) -> Option<f64> {
89 Some(self.mu())
90 }
91
92 fn get_parameters(&self) -> Option<HashMap<String, f64>> {
93 let mut p = HashMap::new();
94 p.insert("mu".to_string(), self.mu());
95 p.insert("kappa".to_string(), self.kappa());
96 p.insert("theta".to_string(), self.theta());
97 p.insert("sigma".to_string(), self.sigma());
98 p.insert("rho".to_string(), self.rho());
99 p.insert("lambda".to_string(), self.lambda);
100 p.insert("mu_jump".to_string(), self.mu_jump);
101 p.insert("sigma_jump".to_string(), self.sigma_jump);
102 Some(p)
103 }
104
105 fn add_price_impact(&self, state: &mut Array1<f64>, impact: f64) {
106 state[0] += impact;
107 }
108}
109
110pub struct SimulatedDataSource<P: MarketProcess> {
111 process: P,
112 current_state: P::State,
113 current_time: f64,
114 dt: f64,
115 spread: f64,
116 max_steps: usize,
117 current_step: usize,
118 rng: StdRng,
119 impact_factor: f64,
120 #[allow(clippy::type_complexity)]
121 precalculated_data: Option<Vec<(f64, Option<f64>, Option<f64>)>>,
122 cached_parameters: Option<HashMap<String, f64>>,
123}
124
125impl<P: MarketProcess> SimulatedDataSource<P>
126where
127 P::State: Clone,
128{
129 pub fn new(
130 process: P,
131 initial_state: P::State,
132 dt: f64,
133 spread: f64,
134 max_steps: usize,
135 impact_factor: f64,
136 ) -> Self {
137 let mut rng = StdRng::from_rng(&mut rng());
138 let cached_parameters = process.get_parameters();
139 let precalculated_data = if impact_factor.abs() < f64::EPSILON {
140 let mut data = Vec::with_capacity(max_steps);
143 let mut state = initial_state.clone();
144 let mut time = 0.0;
145
146 for _ in 0..max_steps {
147 let dw = process.generate_increment(&mut rng, dt);
148 state = process.step(&state, time, dt, &dw, &mut rng);
149 time += dt;
150 data.push((
151 process.get_price(&state),
152 process.get_volatility(&state),
153 process.get_drift(&state),
154 ));
155 }
156 Some(data)
157 } else {
158 None
159 };
160
161 Self {
162 process,
163 current_state: initial_state,
164 current_time: 0.0,
165 dt,
166 spread,
167 max_steps,
168 current_step: 0,
169 rng,
170 impact_factor,
171 precalculated_data,
172 cached_parameters,
173 }
174 }
175
176 pub fn with_rng(mut self, rng: StdRng) -> Self {
178 self.rng = rng;
179 self
180 }
181}
182
183impl<P: MarketProcess> DataSource for SimulatedDataSource<P> {
184 fn next_quote(&mut self) -> Option<MarketState> {
185 if self.current_step >= self.max_steps {
186 return None;
187 }
188
189 let (price, vol, drift) = if let Some(data) = &self.precalculated_data {
191 let (p, v, d) = data[self.current_step];
192 self.current_step += 1;
193 self.current_time += self.dt;
194 (p, v, d)
195 } else {
196 self.current_step += 1;
198
199 let dw = self.process.generate_increment(&mut self.rng, self.dt);
201
202 self.current_state = self.process.step(
204 &self.current_state,
205 self.current_time,
206 self.dt,
207 &dw,
208 &mut self.rng,
209 );
210
211 let drift = self.process.get_drift(&self.current_state);
212 let vol = self.process.get_volatility(&self.current_state);
213 let price = self.process.get_price(&self.current_state);
214
215 self.current_time += self.dt;
216 (price, vol, drift)
217 };
218
219 let timestamp = self.current_step as f64 * self.dt;
222
223 Some(MarketState {
224 timestamp,
225 best_bid: price - self.spread / 2.0,
226 best_ask: price + self.spread / 2.0,
227 last_price: Some(price),
228 true_volatility: vol,
229 true_drift: drift,
230 parameters: self.cached_parameters.clone(),
231 })
232 }
233
234 fn apply_impact(&mut self, fills: &[FillEvent]) {
235 if self.impact_factor.abs() < f64::EPSILON {
236 return;
237 }
238
239 let net_quantity: f64 = fills
240 .iter()
241 .map(|f| match f.side {
242 Side::Buy => f.quantity,
243 Side::Sell => -f.quantity,
244 })
245 .sum();
246
247 let impact = self.impact_factor * net_quantity;
250 self.process
251 .add_price_impact(&mut self.current_state, impact);
252 }
253}