1use crate::model::Bar;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum OrderSide {
14 Buy,
15 Sell,
16}
17
18impl OrderSide {
19 fn sign(self) -> f64 {
20 match self {
21 OrderSide::Buy => 1.0,
22 OrderSide::Sell => -1.0,
23 }
24 }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub enum OrderKind {
29 Market,
30 Limit {
31 price: f64,
32 },
33 Stop {
34 trigger: f64,
35 },
36 StopLimit {
37 trigger: f64,
38 limit: f64,
39 },
40 Trailing {
43 trail_amount: f64,
44 },
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum OrderStatus {
49 Pending,
50 PartiallyFilled,
51 Filled,
52 Cancelled,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq)]
56pub struct Order {
57 pub id: u64,
58 pub side: OrderSide,
59 pub kind: OrderKind,
60 pub quantity: f64,
61 pub filled_quantity: f64,
62 pub status: OrderStatus,
63 pub trailing_stop_price: Option<f64>,
65 pub stop_triggered: bool,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq)]
71pub struct Fill {
72 pub order_id: u64,
73 pub side: OrderSide,
74 pub price: f64,
75 pub quantity: f64,
76 pub fee: f64,
77 pub timestamp: i64,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Default)]
82pub struct ExecutionCosts {
83 pub fee_pct: f64,
85 pub spread: f64,
88 pub slippage_pct: f64,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Default)]
93pub struct Position {
94 pub quantity: f64,
96 pub avg_entry_price: f64,
97 pub realized_pnl: f64,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Default)]
101pub struct FillSimulatorConfig {
102 pub costs: ExecutionCosts,
103 pub max_fill_ratio_of_volume: Option<f64>,
107 pub max_pyramid_entries: Option<u32>,
110}
111
112pub struct FillSimulator {
113 config: FillSimulatorConfig,
114 orders: Vec<Order>,
115 next_order_id: u64,
116 position: Position,
117 pyramid_entries: u32,
118 fills: Vec<Fill>,
119}
120
121impl FillSimulator {
122 pub fn new(config: FillSimulatorConfig) -> Self {
123 Self {
124 config,
125 orders: Vec::new(),
126 next_order_id: 1,
127 position: Position::default(),
128 pyramid_entries: 0,
129 fills: Vec::new(),
130 }
131 }
132
133 pub fn position(&self) -> Position {
134 self.position
135 }
136
137 pub fn fills(&self) -> &[Fill] {
138 &self.fills
139 }
140
141 pub fn open_orders(&self) -> impl Iterator<Item = &Order> {
142 self.orders.iter().filter(|o| {
143 matches!(
144 o.status,
145 OrderStatus::Pending | OrderStatus::PartiallyFilled
146 )
147 })
148 }
149
150 pub fn submit(&mut self, side: OrderSide, kind: OrderKind, quantity: f64) -> Option<u64> {
153 if quantity <= 0.0 {
154 return None;
155 }
156 let would_pyramid = self.position.quantity != 0.0
157 && self.position.quantity.signum() == side.sign()
158 && self.pyramid_entries > 0;
159 if would_pyramid {
160 if let Some(max) = self.config.max_pyramid_entries {
161 if self.pyramid_entries >= max {
162 return None;
163 }
164 }
165 }
166
167 let id = self.next_order_id;
168 self.next_order_id += 1;
169 self.orders.push(Order {
170 id,
171 side,
172 kind,
173 quantity,
174 filled_quantity: 0.0,
175 status: OrderStatus::Pending,
176 trailing_stop_price: None,
177 stop_triggered: false,
178 });
179 Some(id)
180 }
181
182 pub fn cancel(&mut self, order_id: u64) -> bool {
183 if let Some(order) = self.orders.iter_mut().find(|o| o.id == order_id) {
184 if matches!(
185 order.status,
186 OrderStatus::Pending | OrderStatus::PartiallyFilled
187 ) {
188 order.status = OrderStatus::Cancelled;
189 return true;
190 }
191 }
192 false
193 }
194
195 pub fn on_bar(&mut self, bar: &Bar, timestamp: i64) -> Vec<Fill> {
199 let mut bar_fills = Vec::new();
200 let max_fill_qty = self
201 .config
202 .max_fill_ratio_of_volume
203 .map(|r| (r * bar.volume).max(0.0));
204
205 for order in &mut self.orders {
206 if !matches!(
207 order.status,
208 OrderStatus::Pending | OrderStatus::PartiallyFilled
209 ) {
210 continue;
211 }
212
213 if let OrderKind::Trailing { trail_amount } = order.kind {
214 let candidate = match order.side {
217 OrderSide::Sell => bar.high - trail_amount,
218 OrderSide::Buy => bar.low + trail_amount,
219 };
220 order.trailing_stop_price = Some(match (order.trailing_stop_price, order.side) {
221 (Some(prev), OrderSide::Sell) => prev.max(candidate),
222 (Some(prev), OrderSide::Buy) => prev.min(candidate),
223 (None, _) => candidate,
224 });
225 }
226
227 if let OrderKind::StopLimit { trigger, .. } = order.kind {
228 if !order.stop_triggered {
229 let crossed = match order.side {
230 OrderSide::Buy => bar.high >= trigger,
231 OrderSide::Sell => bar.low <= trigger,
232 };
233 if crossed {
234 order.stop_triggered = true;
235 }
236 }
237 }
238
239 let Some(fill_price) = fill_price_for(order, bar) else {
240 continue;
241 };
242
243 let remaining = order.quantity - order.filled_quantity;
244 let fill_qty = max_fill_qty
245 .map(|cap| remaining.min(cap))
246 .unwrap_or(remaining);
247 if fill_qty <= 0.0 {
248 continue;
249 }
250
251 let costs = &self.config.costs;
252 let side_sign = order.side.sign();
253 let executed_price = fill_price
254 * (1.0
255 + side_sign * (costs.spread / fill_price.max(1e-9) / 2.0 + costs.slippage_pct));
256 let fee = executed_price * fill_qty * costs.fee_pct;
257
258 order.filled_quantity += fill_qty;
259 order.status = if order.filled_quantity >= order.quantity - 1e-9 {
260 OrderStatus::Filled
261 } else {
262 OrderStatus::PartiallyFilled
263 };
264
265 bar_fills.push(Fill {
266 order_id: order.id,
267 side: order.side,
268 price: executed_price,
269 quantity: fill_qty,
270 fee,
271 timestamp,
272 });
273 }
274
275 for fill in &bar_fills {
276 self.apply_fill(fill);
277 }
278 self.fills.extend(bar_fills.iter().copied());
279
280 self.orders
283 .retain(|o| !matches!(o.status, OrderStatus::Filled | OrderStatus::Cancelled));
284
285 bar_fills
286 }
287
288 fn apply_fill(&mut self, fill: &Fill) {
289 let signed_qty = fill.quantity * fill.side.sign();
290 let prev_qty = self.position.quantity;
291 let new_qty = prev_qty + signed_qty;
292
293 if prev_qty == 0.0 || prev_qty.signum() == signed_qty.signum() {
294 let total_cost =
296 self.position.avg_entry_price * prev_qty.abs() + fill.price * fill.quantity;
297 self.position.avg_entry_price = if new_qty.abs() > 1e-12 {
298 total_cost / new_qty.abs()
299 } else {
300 0.0
301 };
302 if prev_qty == 0.0 {
303 self.pyramid_entries = 1;
304 } else {
305 self.pyramid_entries += 1;
306 }
307 } else {
308 let closing_qty = fill.quantity.min(prev_qty.abs());
310 let pnl_per_unit = (fill.price - self.position.avg_entry_price) * prev_qty.signum();
311 self.position.realized_pnl += pnl_per_unit * closing_qty;
312
313 if fill.quantity > prev_qty.abs() {
314 self.position.avg_entry_price = fill.price;
316 self.pyramid_entries = 1;
317 } else if new_qty.abs() < 1e-12 {
318 self.position.avg_entry_price = 0.0;
319 self.pyramid_entries = 0;
320 }
321 }
322
323 self.position.realized_pnl -= fill.fee;
324 self.position.quantity = new_qty;
325 }
326}
327
328fn fill_price_for(order: &Order, bar: &Bar) -> Option<f64> {
329 match order.kind {
330 OrderKind::Market => Some(bar.open),
331 OrderKind::Limit { price } => match order.side {
332 OrderSide::Buy if bar.low <= price => Some(price.min(bar.open)),
333 OrderSide::Sell if bar.high >= price => Some(price.max(bar.open)),
334 _ => None,
335 },
336 OrderKind::Stop { trigger } => match order.side {
337 OrderSide::Buy if bar.high >= trigger => Some(trigger.max(bar.open)),
338 OrderSide::Sell if bar.low <= trigger => Some(trigger.min(bar.open)),
339 _ => None,
340 },
341 OrderKind::StopLimit { limit, .. } => {
342 if !order.stop_triggered {
346 return None;
347 }
348 match order.side {
349 OrderSide::Buy if bar.low <= limit => Some(limit),
350 OrderSide::Sell if bar.high >= limit => Some(limit),
351 _ => None,
352 }
353 }
354 OrderKind::Trailing { .. } => {
355 let stop = order.trailing_stop_price?;
356 match order.side {
357 OrderSide::Buy if bar.high >= stop => Some(stop.max(bar.open)),
358 OrderSide::Sell if bar.low <= stop => Some(stop.min(bar.open)),
359 _ => None,
360 }
361 }
362 }
363}
364
365pub fn submit_bracket(
370 sim: &mut FillSimulator,
371 side: OrderSide,
372 quantity: f64,
373 entry: OrderKind,
374 stop_loss_trigger: f64,
375 take_profit_price: f64,
376) -> Option<(u64, u64, u64)> {
377 let exit_side = match side {
378 OrderSide::Buy => OrderSide::Sell,
379 OrderSide::Sell => OrderSide::Buy,
380 };
381 let entry_id = sim.submit(side, entry, quantity)?;
382 let stop_id = sim.submit(
383 exit_side,
384 OrderKind::Stop {
385 trigger: stop_loss_trigger,
386 },
387 quantity,
388 )?;
389 let target_id = sim.submit(
390 exit_side,
391 OrderKind::Limit {
392 price: take_profit_price,
393 },
394 quantity,
395 )?;
396 Some((entry_id, stop_id, target_id))
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 fn bar(o: f64, h: f64, l: f64, c: f64, v: f64) -> Bar {
404 Bar::new(0, o, h, l, c, v)
405 }
406
407 #[test]
408 fn test_market_order_fills_at_open() {
409 let mut sim = FillSimulator::new(FillSimulatorConfig::default());
410 sim.submit(OrderSide::Buy, OrderKind::Market, 10.0);
411 let fills = sim.on_bar(&bar(100.0, 101.0, 99.0, 100.5, 1000.0), 0);
412 assert_eq!(fills.len(), 1);
413 assert_eq!(fills[0].price, 100.0);
414 assert_eq!(sim.position().quantity, 10.0);
415 assert_eq!(sim.position().avg_entry_price, 100.0);
416 }
417
418 #[test]
419 fn test_limit_order_only_fills_when_touched() {
420 let mut sim = FillSimulator::new(FillSimulatorConfig::default());
421 sim.submit(OrderSide::Buy, OrderKind::Limit { price: 95.0 }, 5.0);
422
423 let no_touch = sim.on_bar(&bar(100.0, 101.0, 99.0, 100.5, 1000.0), 0);
424 assert!(no_touch.is_empty());
425
426 let touched = sim.on_bar(&bar(98.0, 99.0, 94.0, 96.0, 1000.0), 60);
427 assert_eq!(touched.len(), 1);
428 assert!(touched[0].price <= 95.0 + 1e-9);
429 }
430
431 #[test]
432 fn test_stop_order_fills_on_trigger() {
433 let mut sim = FillSimulator::new(FillSimulatorConfig::default());
434 sim.submit(OrderSide::Sell, OrderKind::Stop { trigger: 95.0 }, 5.0);
435 let fills = sim.on_bar(&bar(98.0, 99.0, 93.0, 94.0, 1000.0), 0);
436 assert_eq!(fills.len(), 1);
437 }
438
439 #[test]
440 fn test_stop_limit_trigger_persists_across_bars_after_price_retreats() {
441 let mut sim = FillSimulator::new(FillSimulatorConfig::default());
442 sim.submit(
443 OrderSide::Buy,
444 OrderKind::StopLimit {
445 trigger: 100.0,
446 limit: 99.0,
447 },
448 5.0,
449 );
450
451 let first = sim.on_bar(&bar(100.0, 101.0, 99.5, 100.5, 1000.0), 0);
454 assert!(first.is_empty());
455
456 let second = sim.on_bar(&bar(99.2, 99.4, 99.1, 99.3, 1000.0), 60);
461 assert!(second.is_empty());
462
463 let third = sim.on_bar(&bar(99.5, 100.0, 98.5, 99.0, 1000.0), 120);
465 assert_eq!(third.len(), 1);
466 assert_eq!(third[0].price, 99.0);
467 }
468
469 #[test]
470 fn test_partial_fill_capped_by_volume_ratio() {
471 let mut sim = FillSimulator::new(FillSimulatorConfig {
472 max_fill_ratio_of_volume: Some(0.1),
473 ..Default::default()
474 });
475 let id = sim
476 .submit(OrderSide::Buy, OrderKind::Market, 100.0)
477 .unwrap();
478
479 let first = sim.on_bar(&bar(100.0, 101.0, 99.0, 100.5, 500.0), 0);
480 assert_eq!(first[0].quantity, 50.0); assert_eq!(
482 sim.open_orders().find(|o| o.id == id).unwrap().status,
483 OrderStatus::PartiallyFilled
484 );
485
486 let second = sim.on_bar(&bar(100.0, 101.0, 99.0, 100.5, 500.0), 60);
487 assert_eq!(second[0].quantity, 50.0);
488 assert!(sim.open_orders().find(|o| o.id == id).is_none());
489 assert_eq!(sim.position().quantity, 100.0);
490 }
491
492 #[test]
493 fn test_pyramiding_accumulates_weighted_average_entry() {
494 let mut sim = FillSimulator::new(FillSimulatorConfig::default());
495 sim.submit(OrderSide::Buy, OrderKind::Market, 10.0);
496 sim.on_bar(&bar(100.0, 101.0, 99.0, 100.5, 1000.0), 0);
497 sim.submit(OrderSide::Buy, OrderKind::Market, 10.0);
498 sim.on_bar(&bar(110.0, 111.0, 109.0, 110.5, 1000.0), 60);
499
500 assert_eq!(sim.position().quantity, 20.0);
501 assert!((sim.position().avg_entry_price - 105.0).abs() < 1e-9);
502 }
503
504 #[test]
505 fn test_pyramid_cap_rejects_beyond_limit() {
506 let mut sim = FillSimulator::new(FillSimulatorConfig {
507 max_pyramid_entries: Some(1),
508 ..Default::default()
509 });
510 sim.submit(OrderSide::Buy, OrderKind::Market, 10.0);
511 sim.on_bar(&bar(100.0, 101.0, 99.0, 100.5, 1000.0), 0);
512
513 let rejected = sim.submit(OrderSide::Buy, OrderKind::Market, 10.0);
514 assert!(rejected.is_none());
515 }
516
517 #[test]
518 fn test_opposite_fill_realizes_pnl_and_reduces_position() {
519 let mut sim = FillSimulator::new(FillSimulatorConfig::default());
520 sim.submit(OrderSide::Buy, OrderKind::Market, 10.0);
521 sim.on_bar(&bar(100.0, 101.0, 99.0, 100.5, 1000.0), 0);
522
523 sim.submit(OrderSide::Sell, OrderKind::Market, 10.0);
524 sim.on_bar(&bar(110.0, 111.0, 109.0, 110.5, 1000.0), 60);
525
526 assert_eq!(sim.position().quantity, 0.0);
527 assert!((sim.position().realized_pnl - 100.0).abs() < 1e-9); }
529
530 #[test]
531 fn test_fees_and_spread_reduce_pnl() {
532 let mut sim = FillSimulator::new(FillSimulatorConfig {
533 costs: ExecutionCosts {
534 fee_pct: 0.01,
535 spread: 0.0,
536 slippage_pct: 0.0,
537 },
538 ..Default::default()
539 });
540 sim.submit(OrderSide::Buy, OrderKind::Market, 10.0);
541 let fills = sim.on_bar(&bar(100.0, 101.0, 99.0, 100.5, 1000.0), 0);
542 assert!(fills[0].fee > 0.0);
543 assert!(
544 sim.position().realized_pnl < 0.0,
545 "fees alone must show as negative realized PnL"
546 );
547 }
548
549 #[test]
550 fn test_trailing_stop_tightens_and_fills() {
551 let mut sim = FillSimulator::new(FillSimulatorConfig::default());
552 sim.submit(
554 OrderSide::Sell,
555 OrderKind::Trailing { trail_amount: 2.0 },
556 10.0,
557 );
558
559 let first = sim.on_bar(&bar(100.0, 105.0, 104.0, 104.5, 1000.0), 0); assert!(first.is_empty());
564 let second = sim.on_bar(&bar(104.0, 108.0, 107.0, 107.5, 1000.0), 60); assert!(second.is_empty());
566
567 let third = sim.on_bar(&bar(107.0, 107.5, 104.0, 105.0, 1000.0), 120);
570 assert_eq!(third.len(), 1);
571 assert!((third[0].price - 106.0).abs() < 1e-9);
572 }
573
574 #[test]
575 fn test_cancel_prevents_future_fills() {
576 let mut sim = FillSimulator::new(FillSimulatorConfig::default());
577 let id = sim
578 .submit(OrderSide::Buy, OrderKind::Limit { price: 50.0 }, 5.0)
579 .unwrap();
580 assert!(sim.cancel(id));
581 let fills = sim.on_bar(&bar(48.0, 49.0, 45.0, 46.0, 1000.0), 0);
582 assert!(fills.is_empty());
583 }
584
585 #[test]
586 fn test_submit_bracket_creates_entry_and_two_exits() {
587 let mut sim = FillSimulator::new(FillSimulatorConfig::default());
588 let (entry, stop, target) = submit_bracket(
589 &mut sim,
590 OrderSide::Buy,
591 10.0,
592 OrderKind::Market,
593 95.0,
594 110.0,
595 )
596 .unwrap();
597 assert_ne!(entry, stop);
598 assert_ne!(stop, target);
599 }
600}