1use nautilus_model::{
18 enums::{OrderSideSpecified, 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_specified();
49 let protection_raw = match order_side {
50 OrderSideSpecified::Buy => {
51 let opposite = ask.ok_or_else(|| anyhow::anyhow!("Ask required"))?;
52 opposite.raw + offset_raw
53 }
54 OrderSideSpecified::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 OrderSide::NoOrderSide => panic!("Side is required"),
115 };
116
117 let result = protection_price_calculate(price_increment, &order, 25, bid, ask);
118
119 assert!(result.is_err());
120 }
121
122 #[rstest]
123 #[case(OrderType::StopMarket)]
124 #[case(OrderType::Market)]
125 fn test_protection_price_buy(#[case] order_type: OrderType) {
126 let order = build_stop_order(order_type, OrderSide::Buy);
127
128 let protection_price = protection_price_calculate(
129 Price::new(0.01, 2),
130 &order,
131 50,
132 Some(Price::new(99.0, 2)),
133 Some(Price::new(101.0, 2)),
134 )
135 .unwrap();
136
137 assert_eq!(protection_price.as_f64(), 101.5);
138 }
139
140 #[rstest]
141 #[case(OrderType::StopMarket)]
142 #[case(OrderType::Market)]
143 fn test_protection_price_sell(#[case] order_type: OrderType) {
144 let order = build_stop_order(order_type, OrderSide::Sell);
145
146 let protection_price = protection_price_calculate(
147 Price::new(0.01, 2),
148 &order,
149 50,
150 Some(Price::new(99.0, 2)),
151 Some(Price::new(101.0, 2)),
152 )
153 .unwrap();
154
155 assert_eq!(protection_price.as_f64(), 98.5);
156 }
157
158 #[rstest]
159 fn test_protection_price_zero_points() {
160 let order = build_stop_order(OrderType::Market, OrderSide::Buy);
161
162 let protection_price = protection_price_calculate(
163 Price::new(0.01, 2),
164 &order,
165 0,
166 Some(Price::new(99.0, 2)),
167 Some(Price::new(101.0, 2)),
168 )
169 .unwrap();
170
171 assert_eq!(protection_price.as_f64(), 101.0);
173 }
174
175 #[rstest]
176 fn test_protection_price_sell_negative_result() {
177 let order = build_stop_order(OrderType::Market, OrderSide::Sell);
178
179 let protection_price = protection_price_calculate(
180 Price::new(0.01, 2),
181 &order,
182 1000,
183 Some(Price::new(5.0, 2)),
184 Some(Price::new(6.0, 2)),
185 )
186 .unwrap();
187
188 assert_eq!(protection_price.as_f64(), -5.0);
190 }
191
192 #[rstest]
193 fn test_protection_price_large_points() {
194 let order = build_stop_order(OrderType::Market, OrderSide::Buy);
195
196 let protection_price = protection_price_calculate(
197 Price::new(0.01, 2),
198 &order,
199 100_000,
200 Some(Price::new(50_000.0, 2)),
201 Some(Price::new(50_001.0, 2)),
202 )
203 .unwrap();
204
205 assert_eq!(protection_price.as_f64(), 51001.0);
207 }
208}