rusty_model/data/
simd_trade.rs1use crate::enums::OrderSide;
7use crate::instruments::InstrumentId;
8use rust_decimal::prelude::ToPrimitive;
9use simd_aligned::VecSimd;
10use wide::f64x4;
11
12use super::market_trade::MarketTrade;
13
14#[derive(Debug, Clone)]
49pub struct SimdTradeBatch {
50 pub prices: VecSimd<f64x4>,
53
54 pub quantities: VecSimd<f64x4>,
57
58 pub exchange_timestamps_ns: Vec<u64>,
61
62 pub sides: Vec<OrderSide>,
64
65 pub count: usize,
67
68 pub instrument_id: InstrumentId,
70}
71
72impl SimdTradeBatch {
73 #[inline]
75 #[must_use]
76 pub fn with_capacity(capacity: usize, instrument_id: InstrumentId) -> Self {
77 let aligned_capacity = (capacity + 3) & !3;
79
80 Self {
81 prices: VecSimd::with(0.0, aligned_capacity),
83 quantities: VecSimd::with(0.0, aligned_capacity),
84 exchange_timestamps_ns: Vec::with_capacity(capacity),
85 sides: Vec::with_capacity(capacity),
86 count: 0,
87 instrument_id,
88 }
89 }
90
91 #[inline]
93 pub fn add_trade(&mut self, trade: &MarketTrade) -> bool {
94 if self.count >= self.exchange_timestamps_ns.capacity() {
95 return false;
96 }
97
98 let idx = self.count;
99 self.prices.flat_mut()[idx] = trade.price.to_f64().unwrap_or_else(|| {
100 #[cfg(debug_assertions)]
101 eprintln!(
102 "Warning: Trade price conversion failed for trade {}: {}",
103 idx, trade.price
104 );
105 f64::NAN
106 });
107 self.quantities.flat_mut()[idx] = trade.quantity.to_f64().unwrap_or_else(|| {
108 #[cfg(debug_assertions)]
109 eprintln!(
110 "Warning: Trade quantity conversion failed for trade {}: {}",
111 idx, trade.quantity
112 );
113 f64::NAN
114 });
115 self.exchange_timestamps_ns.push(trade.exchange_time_ns);
116 self.sides.push(trade.direction);
117 self.count += 1;
118
119 true
120 }
121
122 #[inline]
124 #[must_use]
125 pub fn total_volume(&self) -> f64 {
126 if self.count == 0 {
127 return 0.0;
128 }
129
130 let chunks = self.count.div_ceil(4);
131 let mut sum = f64x4::splat(0.0);
132
133 for i in 0..chunks {
134 sum += self.quantities[i];
135 }
136
137 let total = sum.reduce_add();
139
140 let padding = chunks * 4 - self.count;
142 if padding > 0 {
143 let quantities_flat = self.quantities.flat();
144 let padding_sum: f64 = quantities_flat[self.count..chunks * 4].iter().sum();
145 total - padding_sum
146 } else {
147 total
148 }
149 }
150
151 #[inline]
153 #[must_use]
154 pub fn vwap(&self) -> Option<f64> {
155 if self.count == 0 {
156 return None;
157 }
158
159 let chunks = self.count.div_ceil(4);
160 let mut weighted_sum = f64x4::splat(0.0);
161 let mut volume_sum = f64x4::splat(0.0);
162
163 for i in 0..chunks {
164 weighted_sum += self.prices[i] * self.quantities[i];
165 volume_sum += self.quantities[i];
166 }
167
168 let total_weighted = weighted_sum.reduce_add();
169 let total_volume = volume_sum.reduce_add();
170
171 let padding = chunks * 4 - self.count;
173 if padding > 0 {
174 let prices_flat = self.prices.flat();
175 let quantities_flat = self.quantities.flat();
176 let mut padding_weighted = 0.0;
177 let mut padding_volume = 0.0;
178
179 for i in self.count..chunks * 4 {
180 padding_weighted += prices_flat[i] * quantities_flat[i];
181 padding_volume += quantities_flat[i];
182 }
183
184 let adjusted_weighted = total_weighted - padding_weighted;
185 let adjusted_volume = total_volume - padding_volume;
186
187 if adjusted_volume > 0.0 {
188 Some(adjusted_weighted / adjusted_volume)
189 } else {
190 None
191 }
192 } else if total_volume > 0.0 {
193 Some(total_weighted / total_volume)
194 } else {
195 None
196 }
197 }
198
199 #[inline]
201 #[must_use]
202 pub fn directional_volume(&self) -> f64 {
203 if self.count == 0 {
204 return 0.0;
205 }
206
207 let quantities_flat = self.quantities.flat();
208 let mut buy_volume = 0.0;
209 let mut sell_volume = 0.0;
210
211 for (i, &quantity) in quantities_flat[..self.count].iter().enumerate() {
212 match self.sides[i] {
213 OrderSide::Buy => buy_volume += quantity,
214 OrderSide::Sell => sell_volume += quantity,
215 }
216 }
217
218 buy_volume - sell_volume
219 }
220
221 #[inline]
223 #[must_use]
224 pub fn price_range(&self) -> Option<(f64, f64)> {
225 if self.count == 0 {
226 return None;
227 }
228
229 let chunks = self.count.div_ceil(4);
230 let mut max_val = f64x4::splat(f64::NEG_INFINITY);
231 let mut min_val = f64x4::splat(f64::INFINITY);
232
233 for i in 0..chunks.saturating_sub(1) {
235 max_val = max_val.max(self.prices[i]);
236 min_val = min_val.min(self.prices[i]);
237 }
238
239 if chunks > 0 {
241 let last_chunk_idx = chunks - 1;
242 let last_chunk = self.prices[last_chunk_idx];
243 let remaining = self.count - last_chunk_idx * 4;
244
245 let mask = f64x4::from([
247 if remaining > 0 {
248 0.0
249 } else {
250 f64::NEG_INFINITY
251 },
252 if remaining > 1 {
253 0.0
254 } else {
255 f64::NEG_INFINITY
256 },
257 if remaining > 2 {
258 0.0
259 } else {
260 f64::NEG_INFINITY
261 },
262 if remaining > 3 {
263 0.0
264 } else {
265 f64::NEG_INFINITY
266 },
267 ]);
268
269 let masked_chunk = last_chunk + mask;
270 max_val = max_val.max(masked_chunk);
271
272 let mask_min = f64x4::from([
273 if remaining > 0 { 0.0 } else { f64::INFINITY },
274 if remaining > 1 { 0.0 } else { f64::INFINITY },
275 if remaining > 2 { 0.0 } else { f64::INFINITY },
276 if remaining > 3 { 0.0 } else { f64::INFINITY },
277 ]);
278
279 let masked_chunk_min = last_chunk + mask_min;
280 min_val = min_val.min(masked_chunk_min);
281 }
282
283 let max_arr = max_val.to_array();
285 let min_arr = min_val.to_array();
286
287 let max = max_arr[0].max(max_arr[1]).max(max_arr[2]).max(max_arr[3]);
288 let min = min_arr[0].min(min_arr[1]).min(min_arr[2]).min(min_arr[3]);
289
290 Some((min, max))
291 }
292
293 #[inline]
295 #[must_use]
296 pub fn buy_sell_ratio(&self) -> Option<f64> {
297 if self.count == 0 {
298 return None;
299 }
300
301 let quantities_flat = self.quantities.flat();
302 let mut buy_volume = 0.0;
303 let mut sell_volume = 0.0;
304
305 for (i, &quantity) in quantities_flat[..self.count].iter().enumerate() {
306 match self.sides[i] {
307 OrderSide::Buy => buy_volume += quantity,
308 OrderSide::Sell => sell_volume += quantity,
309 }
310 }
311
312 if sell_volume > 0.0 {
313 Some(buy_volume / sell_volume)
314 } else if buy_volume > 0.0 {
315 Some(f64::INFINITY)
316 } else {
317 None
318 }
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use crate::venues::Venue;
326 use quanta::Instant;
327 use rust_decimal::{Decimal, prelude::FromPrimitive};
328
329 #[test]
330 fn test_simd_trade_batch_vwap() {
331 let instrument_id = InstrumentId::new("TEST", Venue::Test);
332 let mut batch = SimdTradeBatch::with_capacity(10, instrument_id.clone());
333
334 let trades = vec![
336 (100.0, 10.0, OrderSide::Buy),
337 (100.5, 20.0, OrderSide::Sell),
338 (99.5, 15.0, OrderSide::Buy),
339 (100.2, 25.0, OrderSide::Sell),
340 ];
341
342 for (price, qty, side) in trades {
343 let trade = MarketTrade {
344 timestamp: Instant::now(),
345 exchange_time_ns: 0,
346 price: Decimal::from_f64(price).unwrap(),
347 quantity: Decimal::from_f64(qty).unwrap(),
348 direction: side,
349 instrument_id: instrument_id.clone(),
350 };
351 batch.add_trade(&trade);
352 }
353
354 let vwap = batch.vwap().unwrap();
356 let expected = 100.2f64.mul_add(
357 25.0,
358 99.5f64.mul_add(15.0, 100.0f64.mul_add(10.0, 100.5 * 20.0)),
359 ) / 70.0;
360 assert!((vwap - expected).abs() < 1e-10);
361
362 let volume = batch.total_volume();
364 assert!((volume - 70.0).abs() < 1e-10);
365
366 let dir_volume = batch.directional_volume();
368 assert!((dir_volume - (-20.0)).abs() < 1e-10); let (low, high) = batch.price_range().unwrap();
372 assert!((low - 99.5).abs() < 1e-10);
373 assert!((high - 100.5).abs() < 1e-10);
374 }
375
376 #[test]
377 fn test_simd_trade_batch_empty() {
378 let instrument_id = InstrumentId::new("TEST", Venue::Test);
379 let batch = SimdTradeBatch::with_capacity(10, instrument_id);
380
381 assert!(batch.total_volume().abs() < f64::EPSILON);
382 assert_eq!(batch.vwap(), None);
383 assert_eq!(batch.price_range(), None);
384 assert_eq!(batch.buy_sell_ratio(), None);
385 }
386}