nautilus_execution/
protection.rs1use nautilus_model::{
18 enums::{OrderSideSpecified, OrderType},
19 orders::{Order, OrderAny},
20 types::Price,
21};
22
23pub fn protection_price_calculate(
37 price_increment: Price,
38 order: &OrderAny,
39 protection_points: Option<u32>,
40 bid: Option<Price>,
41 ask: Option<Price>,
42) -> anyhow::Result<Price> {
43 let order_type = order.order_type();
44 if !matches!(order_type, OrderType::Market | OrderType::StopMarket) {
45 anyhow::bail!("Invalid `OrderType` {order_type} for protection price calculation");
46 }
47
48 let protection_points =
49 protection_points.ok_or_else(|| anyhow::anyhow!("Protection points required"))?;
50 let offset = f64::from(protection_points) * price_increment.as_f64();
51
52 let order_side = order.order_side_specified();
53 let protection_price = match order_side {
54 OrderSideSpecified::Buy => {
55 let opposite = ask.ok_or_else(|| anyhow::anyhow!("Ask required"))?;
56 let opposite_f64 = opposite.as_f64();
57 opposite_f64 + offset
58 }
59 OrderSideSpecified::Sell => {
60 let opposite = bid.ok_or_else(|| anyhow::anyhow!("Bid required"))?;
61 let opposite_f64 = opposite.as_f64();
62 opposite_f64 - offset
63 }
64 };
65
66 Ok(Price::new(protection_price, price_increment.precision))
67}
68
69#[cfg(test)]
70mod tests {
71 use nautilus_model::{
72 enums::{OrderSide, OrderType, TriggerType},
73 orders::builder::OrderTestBuilder,
74 types::Quantity,
75 };
76 use rstest::rstest;
77
78 use super::*;
79
80 fn build_stop_order(order_type: OrderType, side: OrderSide) -> OrderAny {
81 let mut builder = OrderTestBuilder::new(order_type);
82 builder
83 .instrument_id("BTCUSDT-PERP.BINANCE".into())
84 .side(side)
85 .quantity(Quantity::from(1))
86 .trigger_price(Price::new(100.0, 2))
87 .trigger_type(TriggerType::LastPrice);
88
89 if order_type == OrderType::StopLimit {
90 builder.price(Price::new(99.5, 2));
91 }
92
93 builder.build()
94 }
95
96 #[rstest]
97 fn test_calculate_with_invalid_order_type() {
98 let order = OrderTestBuilder::new(OrderType::Limit)
99 .instrument_id("BTCUSDT-PERP.BINANCE".into())
100 .side(OrderSide::Buy)
101 .price(Price::new(100.0, 2))
102 .quantity(Quantity::from(1))
103 .build();
104
105 let result = protection_price_calculate(Price::new(0.01, 2), &order, Some(600), None, None);
106
107 assert!(result.is_err());
108 }
109
110 #[rstest]
111 fn test_calculate_requires_protection_points() {
112 let order = build_stop_order(OrderType::StopMarket, OrderSide::Buy);
113
114 let result = protection_price_calculate(
115 Price::new(0.01, 2),
116 &order,
117 None,
118 Some(Price::new(99.0, 2)),
119 Some(Price::new(101.0, 2)),
120 );
121
122 assert!(result.is_err());
123 }
124
125 #[rstest]
126 #[case(OrderSide::Buy)]
127 #[case(OrderSide::Sell)]
128 fn test_calculate_requires_opposite_quote(#[case] side: OrderSide) {
129 let order = build_stop_order(OrderType::StopMarket, side);
130 let price_increment = Price::new(0.01, 2);
131
132 let (bid, ask) = match side {
133 OrderSide::Buy => (Some(Price::new(99.5, 2)), None),
134 OrderSide::Sell => (None, Some(Price::new(100.5, 2))),
135 OrderSide::NoOrderSide => panic!("Side is required"),
136 };
137
138 let result = protection_price_calculate(price_increment, &order, Some(25), bid, ask);
139
140 assert!(result.is_err());
141 }
142
143 #[rstest]
144 #[case(OrderType::StopMarket)]
145 #[case(OrderType::Market)]
146 fn test_protection_price_buy(#[case] order_type: OrderType) {
147 let order = build_stop_order(order_type, OrderSide::Buy);
148
149 let protection_price = protection_price_calculate(
150 Price::new(0.01, 2),
151 &order,
152 Some(50),
153 Some(Price::new(99.0, 2)),
154 Some(Price::new(101.0, 2)),
155 )
156 .unwrap();
157
158 assert_eq!(protection_price.as_f64(), 101.5);
159 }
160
161 #[rstest]
162 #[case(OrderType::StopMarket)]
163 #[case(OrderType::Market)]
164 fn test_protection_price_sell(#[case] order_type: OrderType) {
165 let order = build_stop_order(order_type, OrderSide::Sell);
166
167 let protection_price = protection_price_calculate(
168 Price::new(0.01, 2),
169 &order,
170 Some(50),
171 Some(Price::new(99.0, 2)),
172 Some(Price::new(101.0, 2)),
173 )
174 .unwrap();
175
176 assert_eq!(protection_price.as_f64(), 98.5);
177 }
178}