1use crate::model::Bar;
11use std::collections::BTreeMap;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum OrderSide {
15 Buy,
16 Sell,
17}
18
19impl OrderSide {
20 fn sign(self) -> f64 {
21 match self {
22 OrderSide::Buy => 1.0,
23 OrderSide::Sell => -1.0,
24 }
25 }
26}
27
28#[derive(Debug, Clone, Copy, PartialEq)]
29pub enum OrderKind {
30 Market,
31 Limit {
32 price: f64,
33 },
34 Stop {
35 trigger: f64,
36 },
37 StopLimit {
38 trigger: f64,
39 limit: f64,
40 },
41 Trailing {
44 trail_amount: f64,
45 },
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum OrderStatus {
50 Pending,
51 PartiallyFilled,
52 Filled,
53 Cancelled,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq)]
57pub struct Order {
58 pub id: u64,
59 pub side: OrderSide,
60 pub kind: OrderKind,
61 pub quantity: f64,
62 pub filled_quantity: f64,
63 pub status: OrderStatus,
64 pub trailing_stop_price: Option<f64>,
66 pub stop_triggered: bool,
69 pub bracket: Option<BracketLink>,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct BracketLink {
79 pub entry_id: u64,
82 pub role: BracketRole,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum BracketRole {
89 Entry,
91 StopLoss,
93 TakeProfit,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
101pub enum IntrabarFillPolicy {
102 #[default]
105 StopFirst,
106 TargetFirst,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Default)]
114struct BracketState {
115 entry_filled: f64,
116 exit_closed: f64,
117 entry_done: bool,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq)]
124pub struct Fill {
125 pub order_id: u64,
126 pub side: OrderSide,
127 pub price: f64,
128 pub quantity: f64,
129 pub fee: f64,
130 pub timestamp: i64,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Default)]
135pub struct ExecutionCosts {
136 pub fee_pct: f64,
138 pub spread: f64,
141 pub slippage_pct: f64,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Default)]
146pub struct Position {
147 pub quantity: f64,
149 pub avg_entry_price: f64,
150 pub realized_pnl: f64,
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Default)]
154pub struct FillSimulatorConfig {
155 pub costs: ExecutionCosts,
156 pub max_fill_ratio_of_volume: Option<f64>,
160 pub max_pyramid_entries: Option<u32>,
163 pub bracket_intrabar_policy: IntrabarFillPolicy,
166}
167
168pub struct FillSimulator {
169 config: FillSimulatorConfig,
170 orders: Vec<Order>,
171 next_order_id: u64,
172 position: Position,
173 pyramid_entries: u32,
174 fills: Vec<Fill>,
175 bracket_state: BTreeMap<u64, BracketState>,
179}
180
181impl FillSimulator {
182 pub fn new(config: FillSimulatorConfig) -> Self {
183 Self {
184 config,
185 orders: Vec::new(),
186 next_order_id: 1,
187 position: Position::default(),
188 pyramid_entries: 0,
189 fills: Vec::new(),
190 bracket_state: BTreeMap::new(),
191 }
192 }
193
194 pub fn position(&self) -> Position {
195 self.position
196 }
197
198 pub fn fills(&self) -> &[Fill] {
199 &self.fills
200 }
201
202 pub fn open_orders(&self) -> impl Iterator<Item = &Order> {
203 self.orders.iter().filter(|o| {
204 matches!(
205 o.status,
206 OrderStatus::Pending | OrderStatus::PartiallyFilled
207 )
208 })
209 }
210
211 pub fn submit(&mut self, side: OrderSide, kind: OrderKind, quantity: f64) -> Option<u64> {
214 self.submit_internal(side, kind, quantity, None)
215 }
216
217 fn submit_internal(
218 &mut self,
219 side: OrderSide,
220 kind: OrderKind,
221 quantity: f64,
222 bracket: Option<BracketLink>,
223 ) -> Option<u64> {
224 if quantity <= 0.0 {
225 return None;
226 }
227 let would_pyramid = self.position.quantity != 0.0
228 && self.position.quantity.signum() == side.sign()
229 && self.pyramid_entries > 0;
230 if would_pyramid {
231 if let Some(max) = self.config.max_pyramid_entries {
232 if self.pyramid_entries >= max {
233 return None;
234 }
235 }
236 }
237
238 let id = self.next_order_id;
239 self.next_order_id += 1;
240 self.orders.push(Order {
241 id,
242 side,
243 kind,
244 quantity,
245 filled_quantity: 0.0,
246 status: OrderStatus::Pending,
247 trailing_stop_price: None,
248 stop_triggered: false,
249 bracket,
250 });
251 Some(id)
252 }
253
254 pub fn cancel(&mut self, order_id: u64) -> bool {
255 if let Some(order) = self.orders.iter_mut().find(|o| o.id == order_id) {
256 if matches!(
257 order.status,
258 OrderStatus::Pending | OrderStatus::PartiallyFilled
259 ) {
260 order.status = OrderStatus::Cancelled;
261 if let Some(BracketLink {
262 entry_id,
263 role: BracketRole::Entry,
264 }) = order.bracket
265 {
266 self.bracket_state.entry(entry_id).or_default().entry_done = true;
269 }
270 return true;
271 }
272 }
273 false
274 }
275
276 pub fn on_bar(&mut self, bar: &Bar, timestamp: i64) -> Vec<Fill> {
288 let mut bar_fills = Vec::new();
289 let max_fill_qty = self
290 .config
291 .max_fill_ratio_of_volume
292 .map(|r| (r * bar.volume).max(0.0));
293
294 let primary_ids: Vec<u64> = self
295 .orders
296 .iter()
297 .filter(|o| {
298 matches!(
299 o.status,
300 OrderStatus::Pending | OrderStatus::PartiallyFilled
301 )
302 })
303 .filter(|o| {
304 !matches!(
305 o.bracket,
306 Some(BracketLink {
307 role: BracketRole::StopLoss | BracketRole::TakeProfit,
308 ..
309 })
310 )
311 })
312 .map(|o| o.id)
313 .collect();
314 for id in primary_ids {
315 self.attempt_fill(id, bar, timestamp, max_fill_qty, None, &mut bar_fills);
316 }
317
318 let mut brackets: BTreeMap<u64, (Option<u64>, Option<u64>)> = BTreeMap::new();
319 for order in self.orders.iter().filter(|o| {
320 matches!(
321 o.status,
322 OrderStatus::Pending | OrderStatus::PartiallyFilled
323 )
324 }) {
325 if let Some(BracketLink { entry_id, role }) = order.bracket {
326 let slot = brackets.entry(entry_id).or_default();
327 match role {
328 BracketRole::StopLoss => slot.0 = Some(order.id),
329 BracketRole::TakeProfit => slot.1 = Some(order.id),
330 BracketRole::Entry => {}
331 }
332 }
333 }
334 for (entry_id, (stop_id, target_id)) in brackets {
335 let ordered = match self.config.bracket_intrabar_policy {
336 IntrabarFillPolicy::StopFirst => [stop_id, target_id],
337 IntrabarFillPolicy::TargetFirst => [target_id, stop_id],
338 };
339 for id in ordered.into_iter().flatten() {
340 let capacity = self
341 .bracket_state
342 .get(&entry_id)
343 .map(|s| s.entry_filled - s.exit_closed)
344 .unwrap_or(0.0);
345 if capacity <= 0.0 {
346 continue;
347 }
348 self.attempt_fill(
349 id,
350 bar,
351 timestamp,
352 max_fill_qty,
353 Some(capacity),
354 &mut bar_fills,
355 );
356 }
357 }
358
359 for fill in &bar_fills {
360 self.apply_fill(fill);
361 }
362 self.fills.extend(bar_fills.iter().copied());
363
364 self.cancel_exhausted_bracket_exits();
368
369 self.orders
372 .retain(|o| !matches!(o.status, OrderStatus::Filled | OrderStatus::Cancelled));
373
374 bar_fills
375 }
376
377 fn attempt_fill(
382 &mut self,
383 order_id: u64,
384 bar: &Bar,
385 timestamp: i64,
386 max_fill_qty: Option<f64>,
387 capacity_cap: Option<f64>,
388 bar_fills: &mut Vec<Fill>,
389 ) {
390 let Some(idx) = self.orders.iter().position(|o| o.id == order_id) else {
391 return;
392 };
393
394 {
395 let order = &mut self.orders[idx];
396 if !matches!(
397 order.status,
398 OrderStatus::Pending | OrderStatus::PartiallyFilled
399 ) {
400 return;
401 }
402
403 if let OrderKind::Trailing { trail_amount } = order.kind {
404 let candidate = match order.side {
407 OrderSide::Sell => bar.high - trail_amount,
408 OrderSide::Buy => bar.low + trail_amount,
409 };
410 order.trailing_stop_price = Some(match (order.trailing_stop_price, order.side) {
411 (Some(prev), OrderSide::Sell) => prev.max(candidate),
412 (Some(prev), OrderSide::Buy) => prev.min(candidate),
413 (None, _) => candidate,
414 });
415 }
416
417 if let OrderKind::StopLimit { trigger, .. } = order.kind {
418 if !order.stop_triggered {
419 let crossed = match order.side {
420 OrderSide::Buy => bar.high >= trigger,
421 OrderSide::Sell => bar.low <= trigger,
422 };
423 if crossed {
424 order.stop_triggered = true;
425 }
426 }
427 }
428 }
429
430 let (oid, side, bracket, executed_price, fill_qty, fee) = {
431 let order = &self.orders[idx];
432 let Some(fill_price) = fill_price_for(order, bar) else {
433 return;
434 };
435
436 let remaining = order.quantity - order.filled_quantity;
437 let mut fill_qty = max_fill_qty
438 .map(|cap| remaining.min(cap))
439 .unwrap_or(remaining);
440 if let Some(cap) = capacity_cap {
441 fill_qty = fill_qty.min(cap.max(0.0));
442 }
443 if fill_qty <= 0.0 {
444 return;
445 }
446
447 let costs = self.config.costs;
448 let side_sign = order.side.sign();
449 let executed_price = fill_price
450 * (1.0
451 + side_sign * (costs.spread / fill_price.max(1e-9) / 2.0 + costs.slippage_pct));
452 let fee = executed_price * fill_qty * costs.fee_pct;
453 (
454 order.id,
455 order.side,
456 order.bracket,
457 executed_price,
458 fill_qty,
459 fee,
460 )
461 };
462
463 let new_status = {
464 let order = &mut self.orders[idx];
465 order.filled_quantity += fill_qty;
466 order.status = if order.filled_quantity >= order.quantity - 1e-9 {
467 OrderStatus::Filled
468 } else {
469 OrderStatus::PartiallyFilled
470 };
471 order.status
472 };
473
474 bar_fills.push(Fill {
475 order_id: oid,
476 side,
477 price: executed_price,
478 quantity: fill_qty,
479 fee,
480 timestamp,
481 });
482
483 if let Some(link) = bracket {
484 let state = self.bracket_state.entry(link.entry_id).or_default();
485 match link.role {
486 BracketRole::Entry => {
487 state.entry_filled += fill_qty;
488 if new_status == OrderStatus::Filled {
489 state.entry_done = true;
492 }
493 }
494 BracketRole::StopLoss | BracketRole::TakeProfit => state.exit_closed += fill_qty,
495 }
496 }
497 }
498
499 fn cancel_exhausted_bracket_exits(&mut self) {
505 let exhausted: Vec<u64> = self
506 .bracket_state
507 .iter()
508 .filter(|(_, s)| s.entry_done && s.entry_filled - s.exit_closed <= 1e-9)
509 .map(|(entry_id, _)| *entry_id)
510 .collect();
511 if exhausted.is_empty() {
512 return;
513 }
514 for order in &mut self.orders {
515 if let Some(BracketLink { entry_id, role }) = order.bracket {
516 if exhausted.contains(&entry_id)
517 && matches!(role, BracketRole::StopLoss | BracketRole::TakeProfit)
518 && matches!(
519 order.status,
520 OrderStatus::Pending | OrderStatus::PartiallyFilled
521 )
522 {
523 order.status = OrderStatus::Cancelled;
524 }
525 }
526 }
527 }
528
529 fn apply_fill(&mut self, fill: &Fill) {
530 let signed_qty = fill.quantity * fill.side.sign();
531 let prev_qty = self.position.quantity;
532 let new_qty = prev_qty + signed_qty;
533
534 if prev_qty == 0.0 || prev_qty.signum() == signed_qty.signum() {
535 let total_cost =
537 self.position.avg_entry_price * prev_qty.abs() + fill.price * fill.quantity;
538 self.position.avg_entry_price = if new_qty.abs() > 1e-12 {
539 total_cost / new_qty.abs()
540 } else {
541 0.0
542 };
543 if prev_qty == 0.0 {
544 self.pyramid_entries = 1;
545 } else {
546 self.pyramid_entries += 1;
547 }
548 } else {
549 let closing_qty = fill.quantity.min(prev_qty.abs());
551 let pnl_per_unit = (fill.price - self.position.avg_entry_price) * prev_qty.signum();
552 self.position.realized_pnl += pnl_per_unit * closing_qty;
553
554 if fill.quantity > prev_qty.abs() {
555 self.position.avg_entry_price = fill.price;
557 self.pyramid_entries = 1;
558 } else if new_qty.abs() < 1e-12 {
559 self.position.avg_entry_price = 0.0;
560 self.pyramid_entries = 0;
561 }
562 }
563
564 self.position.realized_pnl -= fill.fee;
565 self.position.quantity = new_qty;
566 }
567}
568
569fn fill_price_for(order: &Order, bar: &Bar) -> Option<f64> {
570 match order.kind {
571 OrderKind::Market => Some(bar.open),
572 OrderKind::Limit { price } => match order.side {
573 OrderSide::Buy if bar.low <= price => Some(price.min(bar.open)),
574 OrderSide::Sell if bar.high >= price => Some(price.max(bar.open)),
575 _ => None,
576 },
577 OrderKind::Stop { trigger } => match order.side {
578 OrderSide::Buy if bar.high >= trigger => Some(trigger.max(bar.open)),
579 OrderSide::Sell if bar.low <= trigger => Some(trigger.min(bar.open)),
580 _ => None,
581 },
582 OrderKind::StopLimit { limit, .. } => {
583 if !order.stop_triggered {
587 return None;
588 }
589 match order.side {
590 OrderSide::Buy if bar.low <= limit => Some(limit),
591 OrderSide::Sell if bar.high >= limit => Some(limit),
592 _ => None,
593 }
594 }
595 OrderKind::Trailing { .. } => {
596 let stop = order.trailing_stop_price?;
597 match order.side {
598 OrderSide::Buy if bar.high >= stop => Some(stop.max(bar.open)),
599 OrderSide::Sell if bar.low <= stop => Some(stop.min(bar.open)),
600 _ => None,
601 }
602 }
603 }
604}
605
606pub fn submit_bracket(
614 sim: &mut FillSimulator,
615 side: OrderSide,
616 quantity: f64,
617 entry: OrderKind,
618 stop_loss_trigger: f64,
619 take_profit_price: f64,
620) -> Option<(u64, u64, u64)> {
621 let exit_side = match side {
622 OrderSide::Buy => OrderSide::Sell,
623 OrderSide::Sell => OrderSide::Buy,
624 };
625 let entry_id = sim.next_order_id;
628 let confirmed_entry_id = sim.submit_internal(
629 side,
630 entry,
631 quantity,
632 Some(BracketLink {
633 entry_id,
634 role: BracketRole::Entry,
635 }),
636 )?;
637 debug_assert_eq!(entry_id, confirmed_entry_id);
638
639 let stop_id = sim.submit_internal(
640 exit_side,
641 OrderKind::Stop {
642 trigger: stop_loss_trigger,
643 },
644 quantity,
645 Some(BracketLink {
646 entry_id,
647 role: BracketRole::StopLoss,
648 }),
649 )?;
650 let target_id = sim.submit_internal(
651 exit_side,
652 OrderKind::Limit {
653 price: take_profit_price,
654 },
655 quantity,
656 Some(BracketLink {
657 entry_id,
658 role: BracketRole::TakeProfit,
659 }),
660 )?;
661 Some((entry_id, stop_id, target_id))
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667
668 fn bar(o: f64, h: f64, l: f64, c: f64, v: f64) -> Bar {
669 Bar::new(0, o, h, l, c, v)
670 }
671
672 #[test]
673 fn test_market_order_fills_at_open() {
674 let mut sim = FillSimulator::new(FillSimulatorConfig::default());
675 sim.submit(OrderSide::Buy, OrderKind::Market, 10.0);
676 let fills = sim.on_bar(&bar(100.0, 101.0, 99.0, 100.5, 1000.0), 0);
677 assert_eq!(fills.len(), 1);
678 assert_eq!(fills[0].price, 100.0);
679 assert_eq!(sim.position().quantity, 10.0);
680 assert_eq!(sim.position().avg_entry_price, 100.0);
681 }
682
683 #[test]
684 fn test_limit_order_only_fills_when_touched() {
685 let mut sim = FillSimulator::new(FillSimulatorConfig::default());
686 sim.submit(OrderSide::Buy, OrderKind::Limit { price: 95.0 }, 5.0);
687
688 let no_touch = sim.on_bar(&bar(100.0, 101.0, 99.0, 100.5, 1000.0), 0);
689 assert!(no_touch.is_empty());
690
691 let touched = sim.on_bar(&bar(98.0, 99.0, 94.0, 96.0, 1000.0), 60);
692 assert_eq!(touched.len(), 1);
693 assert!(touched[0].price <= 95.0 + 1e-9);
694 }
695
696 #[test]
697 fn test_stop_order_fills_on_trigger() {
698 let mut sim = FillSimulator::new(FillSimulatorConfig::default());
699 sim.submit(OrderSide::Sell, OrderKind::Stop { trigger: 95.0 }, 5.0);
700 let fills = sim.on_bar(&bar(98.0, 99.0, 93.0, 94.0, 1000.0), 0);
701 assert_eq!(fills.len(), 1);
702 }
703
704 #[test]
705 fn test_stop_limit_trigger_persists_across_bars_after_price_retreats() {
706 let mut sim = FillSimulator::new(FillSimulatorConfig::default());
707 sim.submit(
708 OrderSide::Buy,
709 OrderKind::StopLimit {
710 trigger: 100.0,
711 limit: 99.0,
712 },
713 5.0,
714 );
715
716 let first = sim.on_bar(&bar(100.0, 101.0, 99.5, 100.5, 1000.0), 0);
719 assert!(first.is_empty());
720
721 let second = sim.on_bar(&bar(99.2, 99.4, 99.1, 99.3, 1000.0), 60);
726 assert!(second.is_empty());
727
728 let third = sim.on_bar(&bar(99.5, 100.0, 98.5, 99.0, 1000.0), 120);
730 assert_eq!(third.len(), 1);
731 assert_eq!(third[0].price, 99.0);
732 }
733
734 #[test]
735 fn test_partial_fill_capped_by_volume_ratio() {
736 let mut sim = FillSimulator::new(FillSimulatorConfig {
737 max_fill_ratio_of_volume: Some(0.1),
738 ..Default::default()
739 });
740 let id = sim
741 .submit(OrderSide::Buy, OrderKind::Market, 100.0)
742 .unwrap();
743
744 let first = sim.on_bar(&bar(100.0, 101.0, 99.0, 100.5, 500.0), 0);
745 assert_eq!(first[0].quantity, 50.0); assert_eq!(
747 sim.open_orders().find(|o| o.id == id).unwrap().status,
748 OrderStatus::PartiallyFilled
749 );
750
751 let second = sim.on_bar(&bar(100.0, 101.0, 99.0, 100.5, 500.0), 60);
752 assert_eq!(second[0].quantity, 50.0);
753 assert!(sim.open_orders().find(|o| o.id == id).is_none());
754 assert_eq!(sim.position().quantity, 100.0);
755 }
756
757 #[test]
758 fn test_pyramiding_accumulates_weighted_average_entry() {
759 let mut sim = FillSimulator::new(FillSimulatorConfig::default());
760 sim.submit(OrderSide::Buy, OrderKind::Market, 10.0);
761 sim.on_bar(&bar(100.0, 101.0, 99.0, 100.5, 1000.0), 0);
762 sim.submit(OrderSide::Buy, OrderKind::Market, 10.0);
763 sim.on_bar(&bar(110.0, 111.0, 109.0, 110.5, 1000.0), 60);
764
765 assert_eq!(sim.position().quantity, 20.0);
766 assert!((sim.position().avg_entry_price - 105.0).abs() < 1e-9);
767 }
768
769 #[test]
770 fn test_pyramid_cap_rejects_beyond_limit() {
771 let mut sim = FillSimulator::new(FillSimulatorConfig {
772 max_pyramid_entries: Some(1),
773 ..Default::default()
774 });
775 sim.submit(OrderSide::Buy, OrderKind::Market, 10.0);
776 sim.on_bar(&bar(100.0, 101.0, 99.0, 100.5, 1000.0), 0);
777
778 let rejected = sim.submit(OrderSide::Buy, OrderKind::Market, 10.0);
779 assert!(rejected.is_none());
780 }
781
782 #[test]
783 fn test_opposite_fill_realizes_pnl_and_reduces_position() {
784 let mut sim = FillSimulator::new(FillSimulatorConfig::default());
785 sim.submit(OrderSide::Buy, OrderKind::Market, 10.0);
786 sim.on_bar(&bar(100.0, 101.0, 99.0, 100.5, 1000.0), 0);
787
788 sim.submit(OrderSide::Sell, OrderKind::Market, 10.0);
789 sim.on_bar(&bar(110.0, 111.0, 109.0, 110.5, 1000.0), 60);
790
791 assert_eq!(sim.position().quantity, 0.0);
792 assert!((sim.position().realized_pnl - 100.0).abs() < 1e-9); }
794
795 #[test]
796 fn test_fees_and_spread_reduce_pnl() {
797 let mut sim = FillSimulator::new(FillSimulatorConfig {
798 costs: ExecutionCosts {
799 fee_pct: 0.01,
800 spread: 0.0,
801 slippage_pct: 0.0,
802 },
803 ..Default::default()
804 });
805 sim.submit(OrderSide::Buy, OrderKind::Market, 10.0);
806 let fills = sim.on_bar(&bar(100.0, 101.0, 99.0, 100.5, 1000.0), 0);
807 assert!(fills[0].fee > 0.0);
808 assert!(
809 sim.position().realized_pnl < 0.0,
810 "fees alone must show as negative realized PnL"
811 );
812 }
813
814 #[test]
815 fn test_trailing_stop_tightens_and_fills() {
816 let mut sim = FillSimulator::new(FillSimulatorConfig::default());
817 sim.submit(
819 OrderSide::Sell,
820 OrderKind::Trailing { trail_amount: 2.0 },
821 10.0,
822 );
823
824 let first = sim.on_bar(&bar(100.0, 105.0, 104.0, 104.5, 1000.0), 0); assert!(first.is_empty());
829 let second = sim.on_bar(&bar(104.0, 108.0, 107.0, 107.5, 1000.0), 60); assert!(second.is_empty());
831
832 let third = sim.on_bar(&bar(107.0, 107.5, 104.0, 105.0, 1000.0), 120);
835 assert_eq!(third.len(), 1);
836 assert!((third[0].price - 106.0).abs() < 1e-9);
837 }
838
839 #[test]
840 fn test_cancel_prevents_future_fills() {
841 let mut sim = FillSimulator::new(FillSimulatorConfig::default());
842 let id = sim
843 .submit(OrderSide::Buy, OrderKind::Limit { price: 50.0 }, 5.0)
844 .unwrap();
845 assert!(sim.cancel(id));
846 let fills = sim.on_bar(&bar(48.0, 49.0, 45.0, 46.0, 1000.0), 0);
847 assert!(fills.is_empty());
848 }
849
850 #[test]
851 fn test_submit_bracket_creates_entry_and_two_exits() {
852 let mut sim = FillSimulator::new(FillSimulatorConfig::default());
853 let (entry, stop, target) = submit_bracket(
854 &mut sim,
855 OrderSide::Buy,
856 10.0,
857 OrderKind::Market,
858 95.0,
859 110.0,
860 )
861 .unwrap();
862 assert_ne!(entry, stop);
863 assert_ne!(stop, target);
864 }
865
866 #[test]
869 fn test_bracket_exit_cannot_fill_before_entry() {
870 let mut sim = FillSimulator::new(FillSimulatorConfig::default());
871 let (entry_id, stop_id, target_id) = submit_bracket(
872 &mut sim,
873 OrderSide::Buy,
874 10.0,
875 OrderKind::Limit { price: 90.0 },
876 80.0,
877 110.0,
878 )
879 .unwrap();
880
881 let fills = sim.on_bar(&bar(120.0, 121.0, 119.0, 120.0, 1000.0), 0);
884 assert!(
885 fills.is_empty(),
886 "target must not fill before its entry: {fills:?}"
887 );
888 assert_eq!(sim.position().quantity, 0.0);
889 assert_eq!(
890 sim.open_orders().find(|o| o.id == entry_id).unwrap().status,
891 OrderStatus::Pending
892 );
893 assert_eq!(
894 sim.open_orders().find(|o| o.id == stop_id).unwrap().status,
895 OrderStatus::Pending
896 );
897 assert_eq!(
898 sim.open_orders()
899 .find(|o| o.id == target_id)
900 .unwrap()
901 .status,
902 OrderStatus::Pending
903 );
904 }
905
906 #[test]
910 fn test_bracket_same_bar_stop_and_target_only_stop_fires_under_default_policy() {
911 let mut sim = FillSimulator::new(FillSimulatorConfig::default());
912 let (_entry_id, stop_id, target_id) = submit_bracket(
913 &mut sim,
914 OrderSide::Buy,
915 10.0,
916 OrderKind::Market,
917 95.0,
918 110.0,
919 )
920 .unwrap();
921
922 let fills = sim.on_bar(&bar(100.0, 112.0, 94.0, 105.0, 1000.0), 0);
923 assert_eq!(
924 fills.len(),
925 2,
926 "expected entry fill + exactly one exit fill: {fills:?}"
927 );
928 assert_eq!(fills[1].order_id, stop_id, "default policy is stop-first");
929 assert_eq!(sim.position().quantity, 0.0);
930 assert!(
931 sim.open_orders().find(|o| o.id == target_id).is_none(),
932 "untouched sibling must be cancelled, not left resting"
933 );
934 }
935
936 #[test]
939 fn test_bracket_same_bar_stop_and_target_target_first_policy() {
940 let mut sim = FillSimulator::new(FillSimulatorConfig {
941 bracket_intrabar_policy: IntrabarFillPolicy::TargetFirst,
942 ..Default::default()
943 });
944 let (_entry_id, stop_id, target_id) = submit_bracket(
945 &mut sim,
946 OrderSide::Buy,
947 10.0,
948 OrderKind::Market,
949 95.0,
950 110.0,
951 )
952 .unwrap();
953
954 let fills = sim.on_bar(&bar(100.0, 112.0, 94.0, 105.0, 1000.0), 0);
955 assert_eq!(fills.len(), 2);
956 assert_eq!(fills[1].order_id, target_id);
957 assert_eq!(sim.position().quantity, 0.0);
958 assert!(sim.open_orders().find(|o| o.id == stop_id).is_none());
959 }
960
961 #[test]
964 fn test_bracket_exit_never_exceeds_partially_filled_entry_quantity() {
965 let mut sim = FillSimulator::new(FillSimulatorConfig {
966 max_fill_ratio_of_volume: Some(0.1),
967 ..Default::default()
968 });
969 let (entry_id, stop_id, _target_id) = submit_bracket(
970 &mut sim,
971 OrderSide::Buy,
972 100.0,
973 OrderKind::Market,
974 95.0,
975 110.0,
976 )
977 .unwrap();
978
979 let fills = sim.on_bar(&bar(100.0, 100.0, 90.0, 95.0, 500.0), 0);
983 let entry_filled: f64 = fills
984 .iter()
985 .filter(|f| f.order_id == entry_id)
986 .map(|f| f.quantity)
987 .sum();
988 let stop_filled: f64 = fills
989 .iter()
990 .filter(|f| f.order_id == stop_id)
991 .map(|f| f.quantity)
992 .sum();
993 assert_eq!(entry_filled, 50.0);
994 assert!(
995 stop_filled <= entry_filled + 1e-9,
996 "exit filled {stop_filled} against only {entry_filled} of entry"
997 );
998 }
999
1000 #[test]
1003 fn test_short_bracket_exit_cannot_fill_before_entry() {
1004 let mut sim = FillSimulator::new(FillSimulatorConfig::default());
1005 let (_entry_id, _stop_id, target_id) = submit_bracket(
1006 &mut sim,
1007 OrderSide::Sell,
1008 10.0,
1009 OrderKind::Limit { price: 110.0 },
1010 120.0,
1011 90.0,
1012 )
1013 .unwrap();
1014
1015 let fills = sim.on_bar(&bar(100.0, 108.0, 85.0, 95.0, 1000.0), 0);
1018 assert!(
1019 fills.is_empty(),
1020 "target must not fill before its entry: {fills:?}"
1021 );
1022 assert_eq!(sim.position().quantity, 0.0);
1023 assert_eq!(
1024 sim.open_orders()
1025 .find(|o| o.id == target_id)
1026 .unwrap()
1027 .status,
1028 OrderStatus::Pending
1029 );
1030 }
1031
1032 #[test]
1037 fn test_pending_same_direction_entries_not_capped_by_pyramid_limit_while_flat() {
1038 let mut sim = FillSimulator::new(FillSimulatorConfig {
1039 max_pyramid_entries: Some(1),
1040 ..Default::default()
1041 });
1042 let first = sim.submit(OrderSide::Buy, OrderKind::Limit { price: 100.0 }, 5.0);
1043 let second = sim.submit(OrderSide::Buy, OrderKind::Limit { price: 100.0 }, 5.0);
1044 assert!(first.is_some());
1045 assert!(
1046 second.is_some(),
1047 "pending pyramid cap is not yet enforced pre-fill"
1048 );
1049
1050 let fills = sim.on_bar(&bar(100.0, 100.0, 99.0, 100.0, 1000.0), 0);
1051 assert_eq!(fills.len(), 2);
1052 assert_eq!(sim.position().quantity, 10.0);
1053 }
1054
1055 #[test]
1059 fn test_bracket_linkage_unaffected_by_costs() {
1060 let mut sim = FillSimulator::new(FillSimulatorConfig {
1061 costs: ExecutionCosts {
1062 fee_pct: 0.001,
1063 spread: 0.5,
1064 slippage_pct: 0.0005,
1065 },
1066 ..Default::default()
1067 });
1068 let (_entry_id, stop_id, target_id) = submit_bracket(
1069 &mut sim,
1070 OrderSide::Buy,
1071 10.0,
1072 OrderKind::Market,
1073 95.0,
1074 110.0,
1075 )
1076 .unwrap();
1077
1078 let fills = sim.on_bar(&bar(100.0, 112.0, 94.0, 105.0, 1000.0), 0);
1079 assert_eq!(fills.len(), 2);
1080 assert_eq!(fills[1].order_id, stop_id);
1081 assert_eq!(sim.position().quantity, 0.0);
1082 assert!(sim.open_orders().find(|o| o.id == target_id).is_none());
1083 }
1084}