1use crate::{
8 Broker, Commission, Direction, Exit, FillModel, OcaType, Order, OrderKind, Position, Sizing,
9 Trade,
10};
11use pine_core::Bar;
12use std::collections::HashMap;
13
14pub struct BarBroker<F: FillModel> {
15 fills: F,
16 commission: Option<Commission>,
17 sizing: Sizing,
19 max_entries: usize,
21 mintick: f64,
23 initial: f64,
26 cash: f64,
28 realized: f64,
29
30 pending: HashMap<String, Order>,
33 order: Vec<String>,
35 exits: Vec<Exit>,
37
38 open: Vec<Trade>,
39 closed: Vec<Trade>,
40
41 bar_index: u64,
42}
43
44impl<F: FillModel> BarBroker<F> {
45 pub fn new(fills: F, initial_capital: f64) -> Self {
46 Self {
47 fills,
48 commission: None,
49 sizing: Sizing::Contracts(1.0),
50 max_entries: 1,
51 mintick: 0.0,
52 initial: initial_capital,
53 cash: initial_capital,
54 realized: 0.0,
55 pending: HashMap::new(),
56 order: Vec::new(),
57 exits: Vec::new(),
58 open: Vec::new(),
59 closed: Vec::new(),
60 bar_index: 0,
61 }
62 }
63
64 pub fn with_commission(mut self, commission: Commission) -> Self {
65 self.commission = Some(commission);
66 self
67 }
68
69 pub fn with_sizing(mut self, sizing: Sizing) -> Self {
70 self.sizing = sizing;
71 self
72 }
73
74 pub fn with_mintick(mut self, mintick: f64) -> Self {
75 self.mintick = mintick;
76 self
77 }
78
79 pub fn with_pyramiding(mut self, pyramiding: usize) -> Self {
80 self.max_entries = pyramiding.max(1);
81 self
82 }
83
84 fn open_lots_toward(&self, direction: Direction) -> usize {
85 self.open
86 .iter()
87 .filter(|t| t.size.signum() == direction.sign())
88 .count()
89 }
90
91 fn net_size(&self) -> f64 {
92 self.open.iter().map(|t| t.size).sum()
93 }
94
95 fn matched_size(&self, target: Option<&str>) -> f64 {
97 self.open
98 .iter()
99 .filter(|t| target.is_none_or(|id| t.entry_id == id))
100 .map(|t| t.size)
101 .sum()
102 }
103
104 fn matched_avg(&self, target: Option<&str>) -> f64 {
106 let (value, qty): (f64, f64) = self
107 .open
108 .iter()
109 .filter(|t| target.is_none_or(|id| t.entry_id == id))
110 .fold((0.0, 0.0), |(v, q), t| {
111 (v + t.entry_price * t.size, q + t.size)
112 });
113 if qty == 0.0 {
114 0.0
115 } else {
116 value / qty
117 }
118 }
119
120 fn commission_on(&self, qty: f64, price: f64) -> f64 {
121 self.commission.map_or(0.0, |c| c.charge(qty, price))
122 }
123
124 fn apply_fill(&mut self, mut signed_qty: f64, price: f64, id: &str, target: Option<&str>) {
129 let order_qty_abs = signed_qty.abs();
133 let order_commission = self.commission_on(signed_qty, price);
134 self.cash -= order_commission;
135
136 while signed_qty != 0.0 {
140 let Some(index) = self.open.iter().position(|t| {
141 t.size.signum() != signed_qty.signum()
142 && target.is_none_or(|want| t.entry_id == want)
143 }) else {
144 break;
145 };
146
147 let lot = &self.open[index];
148 let closed = signed_qty.abs().min(lot.size.abs());
149 let closed_signed = closed * lot.size.signum();
150 let entry_share = lot.commission * closed / lot.size.abs();
151 let exit_share = order_commission * closed / order_qty_abs;
152
153 self.realized += (price - lot.entry_price) * closed_signed;
154 signed_qty += closed_signed; self.closed.push(Trade {
157 entry_id: lot.entry_id.clone(),
158 size: closed_signed,
159 entry_price: lot.entry_price,
160 entry_bar: lot.entry_bar,
161 exit_price: Some(price),
162 exit_bar: Some(self.bar_index),
163 commission: entry_share + exit_share,
164 });
165
166 let lot = &mut self.open[index];
167 lot.size -= closed_signed;
168 lot.commission -= entry_share;
169 if lot.size == 0.0 {
170 self.open.remove(index);
171 }
172 }
173
174 if signed_qty != 0.0 && target.is_none() {
177 self.open.push(Trade {
178 entry_id: id.to_string(),
179 size: signed_qty,
180 entry_price: price,
181 entry_bar: self.bar_index,
182 exit_price: None,
183 exit_bar: None,
184 commission: order_commission * signed_qty.abs() / order_qty_abs,
185 });
186 }
187 }
188
189 fn resolve_qty(&self, order: &Order, price: f64) -> f64 {
192 if order.reduce_only {
193 let pool = self.matched_size(order.close_target.as_deref());
197 let closable = match (order.qty, order.qty_percent) {
198 (Some(q), _) => pool.abs().min(q.abs()),
199 (None, Some(pct)) => pool.abs() * (pct / 100.0),
200 (None, None) => pool.abs(),
201 };
202 return -pool.signum() * closable;
203 }
204
205 let requested = match order.qty {
206 Some(q) => q.abs(),
207 None => {
208 let sizing_price = order.sizing_price.unwrap_or(price);
211 self.sizing
212 .contracts(sizing_price, self.equity(sizing_price))
213 }
214 };
215 let net = self.net_size();
216 let want = order.direction.sign() * requested;
217 if order.reverses && net != 0.0 && net.signum() != order.direction.sign() {
218 want - net
220 } else {
221 want
222 }
223 }
224
225 fn evaluate_exits(&mut self, bar: &Bar) {
229 let ids: Vec<String> = self.exits.iter().map(|e| e.id.clone()).collect();
230 for id in ids {
231 let Some(exit) = self.exits.iter().find(|e| e.id == id).cloned() else {
232 continue;
233 };
234 let target = exit.from_entry.as_deref();
235 let pos = self.matched_size(target);
236 if pos == 0.0 {
237 continue; }
239 let dir = pos.signum();
240 let entry_avg = self.matched_avg(target);
241 let mintick = self.mintick;
242 let exit_dir = if dir > 0.0 {
243 Direction::Short
244 } else {
245 Direction::Long
246 };
247
248 let tp = exit
251 .limit
252 .or_else(|| exit.profit_ticks.map(|t| entry_avg + dir * t * mintick));
253 let sl = exit
254 .stop
255 .or_else(|| exit.loss_ticks.map(|t| entry_avg - dir * t * mintick));
256
257 let trail_stop = self.advance_trail(&id, dir, entry_avg, bar);
261
262 let hit = sl
266 .and_then(|p| self.leg_fill(OrderKind::Stop(p), exit_dir, bar))
267 .or_else(|| {
268 trail_stop.filter(|&ts| {
269 if dir > 0.0 {
270 bar.low <= ts
271 } else {
272 bar.high >= ts
273 }
274 })
275 })
276 .or_else(|| tp.and_then(|p| self.leg_fill(OrderKind::Limit(p), exit_dir, bar)));
277
278 if let Some(price) = hit {
279 let requested = match (exit.qty, exit.qty_percent) {
280 (Some(q), _) => pos.abs().min(q.abs()),
281 (None, Some(pct)) => pos.abs() * (pct / 100.0),
282 (None, None) => pos.abs(),
283 };
284 self.apply_fill(-dir * requested, price, &exit.id, target);
285 self.exits.retain(|e| e.id != id);
286 }
287 }
288 }
289
290 fn advance_trail(&mut self, id: &str, dir: f64, entry_avg: f64, bar: &Bar) -> Option<f64> {
293 let mintick = self.mintick;
294 let exit = self.exits.iter_mut().find(|e| e.id == id)?;
295 let offset = exit.trail_offset?;
296 let bar_best = if dir > 0.0 { bar.high } else { bar.low };
297
298 if !exit.activated {
299 let level = exit
300 .trail_price
301 .or_else(|| exit.trail_points.map(|pts| entry_avg + dir * pts * mintick));
302 if let Some(level) = level {
303 exit.activated = if dir > 0.0 {
304 bar.high >= level
305 } else {
306 bar.low <= level
307 };
308 }
309 }
310 if !exit.activated {
311 return None;
312 }
313
314 exit.peak = Some(match exit.peak {
315 Some(pk) if dir > 0.0 => pk.max(bar_best),
316 Some(pk) => pk.min(bar_best),
317 None => bar_best,
318 });
319 exit.peak.map(|pk| pk - dir * offset * mintick)
320 }
321
322 fn leg_fill(&self, kind: OrderKind, direction: Direction, bar: &Bar) -> Option<f64> {
324 let leg = Order {
325 kind,
326 ..Order::market("", direction, None)
327 };
328 self.fills.fill(&leg, bar)
329 }
330
331 fn pyramiding_blocks(&self, order: &Order) -> bool {
334 if order.reduce_only || !order.reverses {
335 return false; }
337 let net = self.net_size();
338 let same_side = net != 0.0 && net.signum() == order.direction.sign();
339 same_side && self.open_lots_toward(order.direction) >= self.max_entries
340 }
341
342 fn apply_oca(&mut self, filled: &Order, filled_qty: f64) {
345 let Some(group) = filled.oca_name.clone() else {
346 return;
347 };
348 if filled.oca_type == OcaType::None {
349 return;
350 }
351 let siblings: Vec<String> = self
352 .pending
353 .values()
354 .filter(|o| o.id != filled.id && o.oca_name.as_deref() == Some(group.as_str()))
355 .map(|o| o.id.clone())
356 .collect();
357 for id in siblings {
358 match filled.oca_type {
359 OcaType::Cancel => {
360 self.pending.remove(&id);
361 self.order.retain(|o| o != &id);
362 }
363 OcaType::Reduce => {
364 if let Some(o) = self.pending.get_mut(&id) {
365 let base = o.qty.unwrap_or(filled_qty.abs());
368 let left = base - filled_qty.abs();
369 if left > 0.0 {
370 o.qty = Some(left);
371 } else {
372 self.pending.remove(&id);
373 self.order.retain(|o| o != &id);
374 }
375 }
376 }
377 OcaType::None => {}
378 }
379 }
380 }
381}
382
383impl<F: FillModel> Broker for BarBroker<F> {
384 fn submit(&mut self, order: Order) {
385 if !self.pending.contains_key(&order.id) {
386 self.order.push(order.id.clone());
387 }
388 self.pending.insert(order.id.clone(), order);
389 }
390
391 fn submit_exit(&mut self, mut exit: Exit) {
392 if let Some(slot) = self.exits.iter_mut().find(|e| e.id == exit.id) {
393 exit.activated = slot.activated;
396 exit.peak = slot.peak;
397 *slot = exit;
398 } else {
399 self.exits.push(exit);
400 }
401 }
402
403 fn cancel(&mut self, id: &str) {
404 if self.pending.remove(id).is_some() {
405 self.order.retain(|o| o != id);
406 }
407 self.exits.retain(|e| e.id != id);
408 }
409
410 fn cancel_all(&mut self) {
411 self.pending.clear();
412 self.order.clear();
413 self.exits.clear();
414 }
415
416 fn advance(&mut self, bar: &Bar) {
417 self.bar_index = bar.index;
418
419 let ids: Vec<String> = self.order.clone();
421 for id in ids {
422 let Some(order) = self.pending.get(&id).cloned() else {
423 continue;
424 };
425 if self.pyramiding_blocks(&order) {
426 self.pending.remove(&id);
428 self.order.retain(|o| o != &id);
429 continue;
430 }
431 if let Some(price) = self.fills.fill(&order, bar) {
432 let qty = self.resolve_qty(&order, price);
433 if qty != 0.0 {
434 self.apply_fill(qty, price, &order.id, order.close_target.as_deref());
435 self.apply_oca(&order, qty);
436 }
437 self.pending.remove(&id);
438 self.order.retain(|o| o != &id);
439 }
440 }
441
442 self.evaluate_exits(bar);
444 }
445
446 fn position(&self) -> Position {
447 let size = self.net_size();
448 if size == 0.0 {
449 return Position::default();
450 }
451 let (value, qty): (f64, f64) = self
453 .open
454 .iter()
455 .filter(|t| t.size.signum() == size.signum())
456 .fold((0.0, 0.0), |(v, q), t| {
457 (v + t.entry_price * t.size, q + t.size)
458 });
459 Position {
460 size,
461 avg_price: if qty == 0.0 { 0.0 } else { value / qty },
462 }
463 }
464
465 fn initial_capital(&self) -> f64 {
466 self.initial
467 }
468
469 fn equity(&self, price: f64) -> f64 {
470 let unrealized: f64 = self
471 .open
472 .iter()
473 .map(|t| (price - t.entry_price) * t.size)
474 .sum();
475 self.cash + self.realized + unrealized
476 }
477
478 fn open_trades(&self) -> Vec<&Trade> {
479 self.open.iter().collect()
480 }
481
482 fn closed_trades(&self) -> &[Trade] {
483 &self.closed
484 }
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490 use crate::{Commission, Direction, OrderKind, PineFills};
491
492 fn bar(index: u64, open: f64, high: f64, low: f64, close: f64) -> Bar {
493 Bar {
494 open,
495 high,
496 low,
497 close,
498 volume: 0.0,
499 index,
500 ..Bar::default()
501 }
502 }
503
504 fn broker() -> BarBroker<PineFills> {
505 BarBroker::new(PineFills::default(), 10_000.0)
506 }
507
508 #[test]
509 fn a_market_entry_fills_at_the_open() {
510 let mut b = broker();
511 b.submit(Order::market("long", Direction::Long, Some(2.0)));
512 b.advance(&bar(0, 100.0, 105.0, 99.0, 104.0));
513
514 let pos = b.position();
515 assert_eq!(pos.size, 2.0);
516 assert_eq!(pos.avg_price, 100.0);
517 assert_eq!(b.equity(104.0), 10_008.0);
519 }
520
521 #[test]
522 fn a_closed_trade_keeps_its_size_and_profit() {
523 let mut b = broker();
524 b.submit(Order::market("L", Direction::Long, Some(2.0)));
525 b.advance(&bar(0, 100.0, 100.0, 100.0, 100.0));
526 b.submit(Order {
527 reduce_only: true,
528 ..Order::market("L", Direction::Short, None)
529 });
530 b.advance(&bar(1, 110.0, 110.0, 110.0, 110.0));
531
532 let trade = &b.closed_trades()[0];
533 assert_eq!(trade.size, 2.0);
534 assert_eq!(trade.entry_price, 100.0);
535 assert_eq!(trade.exit_price, Some(110.0));
536 assert_eq!(trade.profit(0.0), 20.0); }
538
539 #[test]
540 fn closing_realises_profit_and_flattens() {
541 let mut b = broker();
542 b.submit(Order::market("long", Direction::Long, Some(1.0)));
543 b.advance(&bar(0, 100.0, 100.0, 100.0, 100.0));
544
545 b.submit(Order {
546 reduce_only: true,
547 ..Order::market("exit", Direction::Short, Some(1.0))
548 });
549 b.advance(&bar(1, 110.0, 110.0, 110.0, 110.0));
550
551 assert!(b.position().is_flat());
552 assert_eq!(b.closed_trades().len(), 1);
553 assert_eq!(b.equity(110.0), 10_010.0);
554 }
555
556 #[test]
557 fn an_opposite_entry_reverses_the_position() {
558 let mut b = broker();
559 b.submit(Order::market("a", Direction::Long, Some(5.0)));
560 b.advance(&bar(0, 100.0, 100.0, 100.0, 100.0));
561
562 b.submit(Order::market("b", Direction::Short, Some(5.0)));
564 b.advance(&bar(1, 100.0, 100.0, 100.0, 100.0));
565
566 assert_eq!(b.position().size, -5.0);
567 assert_eq!(b.closed_trades().len(), 1);
568 }
569
570 #[test]
571 fn a_buy_limit_waits_for_the_price() {
572 let mut b = broker();
573 b.submit(Order {
574 kind: OrderKind::Limit(95.0),
575 reverses: false,
576 ..Order::market("buy", Direction::Long, Some(1.0))
577 });
578
579 b.advance(&bar(0, 100.0, 101.0, 96.0, 99.0));
581 assert!(b.position().is_flat());
582
583 b.advance(&bar(1, 97.0, 98.0, 94.0, 96.0));
585 assert_eq!(b.position().size, 1.0);
586 assert_eq!(b.position().avg_price, 95.0);
587 }
588
589 #[test]
590 fn commission_reduces_equity() {
591 let mut b = broker().with_commission(Commission::Percent(1.0));
592 b.submit(Order::market("long", Direction::Long, Some(1.0)));
593 b.advance(&bar(0, 100.0, 100.0, 100.0, 100.0));
594
595 assert_eq!(b.equity(100.0), 9_999.0);
597 }
598
599 #[test]
600 fn a_take_profit_exit_closes_when_price_reaches_it() {
601 let mut b = broker();
602 b.submit(Order::market("L", Direction::Long, Some(1.0)));
603 b.submit_exit(Exit {
604 limit: Some(110.0),
605 ..Exit::resting("X", Some("L".into()), None, None)
606 });
607
608 b.advance(&bar(0, 100.0, 105.0, 99.0, 104.0));
610 assert_eq!(b.position().size, 1.0);
611
612 b.advance(&bar(1, 106.0, 112.0, 105.0, 108.0));
614 assert!(b.position().is_flat());
615 assert_eq!(b.closed_trades().len(), 1);
616 assert_eq!(b.equity(108.0), 10_010.0); }
618
619 #[test]
620 fn a_stop_loss_in_ticks_sits_a_distance_from_the_entry() {
621 let fills = PineFills {
623 slippage: 0.0,
624 mintick: 0.5,
625 };
626 let mut b = BarBroker::new(fills, 10_000.0).with_mintick(0.5);
627 b.submit(Order::market("L", Direction::Long, Some(1.0)));
628 b.submit_exit(Exit {
629 loss_ticks: Some(4.0),
630 ..Exit::resting("X", Some("L".into()), None, None)
631 });
632
633 b.advance(&bar(0, 100.0, 105.0, 99.0, 104.0));
635 assert_eq!(b.position().size, 1.0);
636
637 b.advance(&bar(1, 100.0, 101.0, 97.0, 99.0));
639 assert!(b.position().is_flat());
640 assert_eq!(b.equity(99.0), 9_998.0); }
642
643 #[test]
644 fn close_targets_only_the_named_entry() {
645 let mut b = broker();
646 b.submit(Order {
647 reverses: false,
648 ..Order::market("A", Direction::Long, Some(1.0))
649 });
650 b.advance(&bar(0, 100.0, 100.0, 100.0, 100.0));
651 b.submit(Order {
652 reverses: false,
653 ..Order::market("B", Direction::Long, Some(1.0))
654 });
655 b.advance(&bar(1, 101.0, 101.0, 101.0, 101.0));
656 assert_eq!(b.position().size, 2.0);
657
658 b.submit(Order {
660 reduce_only: true,
661 close_target: Some("A".into()),
662 qty: None,
663 ..Order::market("A", Direction::Long, None)
664 });
665 b.advance(&bar(2, 102.0, 102.0, 102.0, 102.0));
666 assert_eq!(b.position().size, 1.0);
667 assert_eq!(b.closed_trades().len(), 1);
668 assert_eq!(b.position().avg_price, 101.0); }
670
671 #[test]
672 fn cash_sizing_buys_contracts_worth_the_cash() {
673 let mut b = broker().with_sizing(Sizing::Cash(1_000.0));
674 b.submit(Order::market("L", Direction::Long, None));
676 b.advance(&bar(0, 100.0, 100.0, 100.0, 100.0));
677 assert_eq!(b.position().size, 10.0);
678 }
679
680 #[test]
681 fn percent_of_equity_sizing_scales_with_the_account() {
682 let mut b = broker().with_sizing(Sizing::PercentOfEquity(50.0));
683 b.submit(Order::market("L", Direction::Long, None));
685 b.advance(&bar(0, 100.0, 100.0, 100.0, 100.0));
686 assert_eq!(b.position().size, 50.0);
687 }
688
689 #[test]
690 fn pyramiding_caps_entries_in_one_direction() {
691 let mut b = broker().with_pyramiding(2);
692 for (i, id) in ["A", "B", "C"].iter().enumerate() {
693 b.submit(Order {
694 reverses: true,
695 ..Order::market(*id, Direction::Long, Some(1.0))
696 });
697 b.advance(&bar(i as u64, 100.0, 100.0, 100.0, 100.0));
698 }
699 assert_eq!(b.position().size, 2.0);
701 }
702
703 #[test]
704 fn oca_cancel_removes_the_sibling_when_one_fills() {
705 let mut b = broker();
706 b.submit(Order {
708 kind: OrderKind::Stop(105.0),
709 oca_name: Some("G".into()),
710 oca_type: OcaType::Cancel,
711 ..Order::market("up", Direction::Long, Some(1.0))
712 });
713 b.submit(Order {
714 kind: OrderKind::Limit(95.0),
715 oca_name: Some("G".into()),
716 oca_type: OcaType::Cancel,
717 ..Order::market("down", Direction::Long, Some(1.0))
718 });
719
720 b.advance(&bar(0, 100.0, 106.0, 94.0, 100.0));
722 assert_eq!(b.position().size, 1.0);
723 }
724
725 #[test]
726 fn close_qty_percent_reduces_the_position() {
727 let mut b = broker();
728 b.submit(Order::market("L", Direction::Long, Some(4.0)));
729 b.advance(&bar(0, 100.0, 100.0, 100.0, 100.0));
730
731 b.submit(Order {
733 reduce_only: true,
734 close_target: Some("L".into()),
735 qty_percent: Some(50.0),
736 qty: None,
737 ..Order::market("L", Direction::Long, None)
738 });
739 b.advance(&bar(1, 110.0, 110.0, 110.0, 110.0));
740 assert_eq!(b.position().size, 2.0);
741
742 assert_eq!(b.closed_trades().len(), 1);
744 assert_eq!(b.closed_trades()[0].size, 2.0);
745 assert_eq!(b.closed_trades()[0].profit(0.0), 20.0); assert_eq!(b.open_trades().len(), 1);
747 assert_eq!(b.open_trades()[0].size, 2.0);
748 }
749
750 #[test]
751 fn a_trailing_stop_follows_the_peak_and_fills_at_its_level() {
752 let fills = PineFills {
755 slippage: 0.0,
756 mintick: 0.5,
757 };
758 let mut b = BarBroker::new(fills, 10_000.0).with_mintick(0.5);
759 b.submit(Order::market("L", Direction::Long, Some(1.0)));
760 b.submit_exit(Exit {
761 trail_points: Some(4.0),
762 trail_offset: Some(2.0),
763 ..Exit::resting("X", Some("L".into()), None, None)
764 });
765
766 b.advance(&bar(0, 100.0, 101.0, 99.0, 100.0));
768 assert_eq!(b.position().size, 1.0);
769
770 b.advance(&bar(1, 102.0, 105.0, 101.0, 104.0));
774 assert!(b.position().is_flat());
775 assert_eq!(b.closed_trades().len(), 1);
776 assert_eq!(b.equity(104.0), 10_004.0);
777 }
778}