pine_broker/fill.rs
1//! When a pending order fills against a bar, and at what price.
2//!
3//! This is the one modelling choice a backtest makes, so it is behind a trait:
4//! a more pessimistic model (a limit fills only if price traded *through* it,
5//! not merely touched) is a swap here, leaving the accounting untouched.
6
7use crate::{Direction, Order, OrderKind};
8use pine_core::Bar;
9
10/// Decides whether `order` fills against `bar`, returning the fill price.
11pub trait FillModel {
12 fn fill(&self, order: &Order, bar: &Bar) -> Option<f64>;
13}
14
15/// TradingView's default assumptions:
16///
17/// - a market order fills at the bar's open;
18/// - a limit fills if the bar's range reaches its price, at the better of the
19/// limit and the open (a gap through the limit fills at the open);
20/// - a stop fills if the bar's range reaches its price, at the worse of the stop
21/// and the open (a gap through the stop fills at the open);
22/// - slippage moves the fill `slippage` ticks against the order.
23#[derive(Debug, Clone, Copy, Default)]
24pub struct PineFills {
25 /// Ticks of slippage applied against the order's direction.
26 pub slippage: f64,
27 /// Tick size, so slippage is a price. Zero disables slippage.
28 pub mintick: f64,
29}
30
31impl PineFills {
32 fn slip(&self, direction: Direction, price: f64) -> f64 {
33 price + direction.sign() * self.slippage * self.mintick
34 }
35}
36
37impl FillModel for PineFills {
38 fn fill(&self, order: &Order, bar: &Bar) -> Option<f64> {
39 let raw = match order.kind {
40 OrderKind::Market => bar.open,
41
42 // A buy limit sits at or below price and fills on a dip to it; a
43 // sell limit sits above and fills on a rise. A gap past it fills at
44 // the open, which is the better price.
45 OrderKind::Limit(price) => {
46 let reached = match order.direction {
47 Direction::Long => bar.low <= price,
48 Direction::Short => bar.high >= price,
49 };
50 if !reached {
51 return None;
52 }
53 match order.direction {
54 Direction::Long => bar.open.min(price),
55 Direction::Short => bar.open.max(price),
56 }
57 }
58
59 // A buy stop sits above price and triggers on a rise; a sell stop
60 // sits below. A gap past it fills at the open, the worse price.
61 OrderKind::Stop(price) => {
62 let reached = match order.direction {
63 Direction::Long => bar.high >= price,
64 Direction::Short => bar.low <= price,
65 };
66 if !reached {
67 return None;
68 }
69 match order.direction {
70 Direction::Long => bar.open.max(price),
71 Direction::Short => bar.open.min(price),
72 }
73 }
74
75 // Once the stop is reached this bar, treat the armed limit as a
76 // limit order for the rest of the same bar.
77 OrderKind::StopLimit { stop, limit } => {
78 let armed = match order.direction {
79 Direction::Long => bar.high >= stop,
80 Direction::Short => bar.low <= stop,
81 };
82 if !armed {
83 return None;
84 }
85 let reached = match order.direction {
86 Direction::Long => bar.low <= limit,
87 Direction::Short => bar.high >= limit,
88 };
89 if !reached {
90 return None;
91 }
92 limit
93 }
94 };
95
96 Some(self.slip(order.direction, raw))
97 }
98}