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