Skip to main content

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}