1use nautilus_model::{
18 enums::{OrderSideSpecified, OrderType, TrailingOffsetType, TriggerType},
19 orders::{Order, OrderAny, OrderError},
20 types::Price,
21};
22use rust_decimal::{Decimal, prelude::*};
23
24pub fn trailing_stop_calculate(
45 price_increment: Price,
46 trigger_px: Option<Price>,
47 activation_px: Option<Price>,
48 order: &OrderAny,
49 bid: Option<Price>,
50 ask: Option<Price>,
51 last: Option<Price>,
52) -> anyhow::Result<(Option<Price>, Option<Price>)> {
53 let order_side = order.order_side_specified();
54 let order_type = order.order_type();
55
56 if !matches!(
57 order_type,
58 OrderType::TrailingStopMarket | OrderType::TrailingStopLimit
59 ) {
60 anyhow::bail!("Invalid `OrderType` {order_type} for trailing stop calculation");
61 }
62
63 let mut trigger_price = trigger_px
64 .or(order.trigger_price())
65 .or(activation_px)
66 .or(order.activation_price());
67
68 let mut limit_price = if order_type == OrderType::TrailingStopLimit {
69 order.price()
70 } else {
71 None
72 };
73
74 let trigger_type = order.trigger_type().unwrap();
75 let trailing_offset = order.trailing_offset().unwrap();
76 let trailing_offset_type = order.trailing_offset_type().unwrap();
77 anyhow::ensure!(
78 trigger_type != TriggerType::NoTrigger,
79 "Invalid `TriggerType::NoTrigger` for trailing stop calculation"
80 );
81 anyhow::ensure!(
82 trailing_offset_type != TrailingOffsetType::NoTrailingOffset,
83 "Invalid `TrailingOffsetType::NoTrailingOffset` for trailing stop calculation"
84 );
85
86 let mut new_trigger_price: Option<Price>;
87 let mut new_limit_price: Option<Price> = None;
88
89 let maybe_move = |current: &mut Option<Price>,
90 candidate: Price,
91 better: fn(Price, Price) -> bool|
92 -> Option<Price> {
93 match current {
94 Some(p) if better(candidate, *p) => {
95 *current = Some(candidate);
96 Some(candidate)
97 }
98 None => {
99 *current = Some(candidate);
100 Some(candidate)
101 }
102 _ => None,
103 }
104 };
105
106 let better_trigger: fn(Price, Price) -> bool = match order_side {
107 OrderSideSpecified::Buy => |c, p| c < p,
108 OrderSideSpecified::Sell => |c, p| c > p,
109 };
110 let better_limit = better_trigger;
111
112 let compute = |off: Decimal, basis: f64| -> Price {
113 Price::new(
114 match trailing_offset_type {
115 TrailingOffsetType::Price => off.to_f64().unwrap().mul_add(
116 match order_side {
117 OrderSideSpecified::Buy => 1.0,
118 OrderSideSpecified::Sell => -1.0,
119 },
120 basis,
121 ),
122 TrailingOffsetType::BasisPoints => {
123 let delta = basis * (off.to_f64().unwrap() / 10_000.0);
124 delta.mul_add(
125 match order_side {
126 OrderSideSpecified::Buy => 1.0,
127 OrderSideSpecified::Sell => -1.0,
128 },
129 basis,
130 )
131 }
132 TrailingOffsetType::Ticks => {
133 let delta = off.to_f64().unwrap() * price_increment.as_f64();
134 delta.mul_add(
135 match order_side {
136 OrderSideSpecified::Buy => 1.0,
137 OrderSideSpecified::Sell => -1.0,
138 },
139 basis,
140 )
141 }
142 _ => unreachable!("checked above"),
143 },
144 price_increment.precision,
145 )
146 };
147
148 match trigger_type {
149 TriggerType::Default | TriggerType::LastPrice | TriggerType::MarkPrice => {
150 let last = last.ok_or(OrderError::InvalidStateTransition)?;
151 let cand_trigger = compute(trailing_offset, last.as_f64());
152 new_trigger_price = maybe_move(&mut trigger_price, cand_trigger, better_trigger);
153
154 if order_type == OrderType::TrailingStopLimit {
155 let cand_limit = compute(order.limit_offset().unwrap(), last.as_f64());
156 new_limit_price = maybe_move(&mut limit_price, cand_limit, better_limit);
157 }
158 }
159 TriggerType::BidAsk | TriggerType::LastOrBidAsk => {
160 let (bid, ask) = (
161 bid.ok_or_else(|| anyhow::anyhow!("Bid required"))?,
162 ask.ok_or_else(|| anyhow::anyhow!("Ask required"))?,
163 );
164 let basis = match order_side {
165 OrderSideSpecified::Buy => ask.as_f64(),
166 OrderSideSpecified::Sell => bid.as_f64(),
167 };
168 let cand_trigger = compute(trailing_offset, basis);
169 new_trigger_price = maybe_move(&mut trigger_price, cand_trigger, better_trigger);
170
171 if order_type == OrderType::TrailingStopLimit {
172 let cand_limit = compute(order.limit_offset().unwrap(), basis);
173 new_limit_price = maybe_move(&mut limit_price, cand_limit, better_limit);
174 }
175
176 if trigger_type == TriggerType::LastOrBidAsk {
177 let last = last.ok_or_else(|| anyhow::anyhow!("Last required"))?;
178 let cand_trigger = compute(trailing_offset, last.as_f64());
179 let updated = maybe_move(&mut trigger_price, cand_trigger, better_trigger);
180 if updated.is_some() {
181 new_trigger_price = updated;
182 }
183
184 if order_type == OrderType::TrailingStopLimit {
185 let cand_limit = compute(order.limit_offset().unwrap(), last.as_f64());
186 let updated = maybe_move(&mut limit_price, cand_limit, better_limit);
187 if updated.is_some() {
188 new_limit_price = updated;
189 }
190 }
191 }
192 }
193 _ => anyhow::bail!("`TriggerType` {trigger_type} not currently supported"),
194 }
195
196 Ok((new_trigger_price, new_limit_price))
197}
198
199pub fn trailing_stop_calculate_with_last(
209 price_increment: Price,
210 trailing_offset_type: TrailingOffsetType,
211 side: OrderSideSpecified,
212 offset: Decimal,
213 last: Price,
214) -> anyhow::Result<Price> {
215 let mut offset_value = offset.to_f64().expect("Invalid `offset` value");
216 let last_f64 = last.as_f64();
217
218 match trailing_offset_type {
219 TrailingOffsetType::Price => {} TrailingOffsetType::BasisPoints => {
221 offset_value = last_f64 * (offset_value / 100.0) / 100.0;
222 }
223 TrailingOffsetType::Ticks => {
224 offset_value *= price_increment.as_f64();
225 }
226 _ => anyhow::bail!("`TrailingOffsetType` {trailing_offset_type} not currently supported"),
227 }
228
229 let price_value = match side {
230 OrderSideSpecified::Buy => last_f64 + offset_value,
231 OrderSideSpecified::Sell => last_f64 - offset_value,
232 };
233
234 Ok(Price::new(price_value, price_increment.precision))
235}
236
237pub fn trailing_stop_calculate_with_bid_ask(
247 price_increment: Price,
248 trailing_offset_type: TrailingOffsetType,
249 side: OrderSideSpecified,
250 offset: Decimal,
251 bid: Price,
252 ask: Price,
253) -> anyhow::Result<Price> {
254 let mut offset_value = offset.to_f64().expect("Invalid `offset` value");
255 let bid_f64 = bid.as_f64();
256 let ask_f64 = ask.as_f64();
257
258 match trailing_offset_type {
259 TrailingOffsetType::Price => {} TrailingOffsetType::BasisPoints => match side {
261 OrderSideSpecified::Buy => offset_value = ask_f64 * (offset_value / 100.0) / 100.0,
262 OrderSideSpecified::Sell => offset_value = bid_f64 * (offset_value / 100.0) / 100.0,
263 },
264 TrailingOffsetType::Ticks => {
265 offset_value *= price_increment.as_f64();
266 }
267 _ => anyhow::bail!("`TrailingOffsetType` {trailing_offset_type} not currently supported"),
268 }
269
270 let price_value = match side {
271 OrderSideSpecified::Buy => ask_f64 + offset_value,
272 OrderSideSpecified::Sell => bid_f64 - offset_value,
273 };
274
275 Ok(Price::new(price_value, price_increment.precision))
276}
277
278#[cfg(test)]
279mod tests {
280 use nautilus_model::{
281 enums::{OrderSide, OrderType, TrailingOffsetType, TriggerType},
282 orders::builder::OrderTestBuilder,
283 types::Quantity,
284 };
285 use rstest::rstest;
286 use rust_decimal_macros::dec;
287
288 use super::*;
289
290 #[rstest]
291 fn test_calculate_with_invalid_order_type() {
292 let order = OrderTestBuilder::new(OrderType::Market)
293 .instrument_id("BTCUSDT-PERP.BINANCE".into())
294 .side(OrderSide::Buy)
295 .quantity(Quantity::from(1))
296 .build();
297
298 let result =
299 trailing_stop_calculate(Price::new(0.01, 2), None, None, &order, None, None, None);
300
301 assert!(result.is_err());
303 }
304
305 #[rstest]
306 #[case(OrderSide::Buy)]
307 #[case(OrderSide::Sell)]
308 fn test_calculate_with_last_price_no_last(#[case] side: OrderSide) {
309 let order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
310 .instrument_id("BTCUSDT-PERP.BINANCE".into())
311 .side(side)
312 .trigger_price(Price::new(100.0, 2))
313 .trailing_offset_type(TrailingOffsetType::Price)
314 .trailing_offset(dec!(1.0))
315 .trigger_type(TriggerType::LastPrice)
316 .quantity(Quantity::from(1))
317 .build();
318
319 let result =
320 trailing_stop_calculate(Price::new(0.01, 2), None, None, &order, None, None, None);
321
322 assert!(result.is_err());
324 }
325
326 #[rstest]
327 #[case(OrderSide::Buy)]
328 #[case(OrderSide::Sell)]
329 fn test_calculate_with_bid_ask_no_bid_ask(#[case] side: OrderSide) {
330 let order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
331 .instrument_id("BTCUSDT-PERP.BINANCE".into())
332 .side(side)
333 .trigger_price(Price::new(100.0, 2))
334 .trailing_offset_type(TrailingOffsetType::Price)
335 .trailing_offset(dec!(1.0))
336 .trigger_type(TriggerType::BidAsk)
337 .quantity(Quantity::from(1))
338 .build();
339
340 let result =
341 trailing_stop_calculate(Price::new(0.01, 2), None, None, &order, None, None, None);
342
343 assert!(result.is_err());
345 }
346
347 #[rstest]
348 fn test_calculate_with_unsupported_trigger_type() {
349 let order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
350 .instrument_id("BTCUSDT-PERP.BINANCE".into())
351 .side(OrderSide::Buy)
352 .trigger_price(Price::new(100.0, 2))
353 .trailing_offset_type(TrailingOffsetType::Price)
354 .trailing_offset(dec!(1.0))
355 .trigger_type(TriggerType::IndexPrice) .quantity(Quantity::from(1))
357 .build();
358
359 let result =
360 trailing_stop_calculate(Price::new(0.01, 2), None, None, &order, None, None, None);
361
362 assert!(result.is_err());
364 }
365
366 #[rstest]
367 #[case(OrderSide::Buy, 100.0, 1.0, 99.0, None)] #[case(OrderSide::Buy, 100.0, 1.0, 98.0, Some(99.0))] #[case(OrderSide::Sell, 100.0, 1.0, 101.0, None)] #[case(OrderSide::Sell, 100.0, 1.0, 102.0, Some(101.0))] fn test_trailing_stop_market_last_price(
372 #[case] side: OrderSide,
373 #[case] initial_trigger: f64,
374 #[case] offset: f64,
375 #[case] last_price: f64,
376 #[case] expected_trigger: Option<f64>,
377 ) {
378 let order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
379 .instrument_id("BTCUSDT-PERP.BINANCE".into())
380 .side(side)
381 .trigger_price(Price::new(initial_trigger, 2))
382 .trailing_offset_type(TrailingOffsetType::Price)
383 .trailing_offset(Decimal::from_f64(offset).unwrap())
384 .trigger_type(TriggerType::LastPrice)
385 .quantity(Quantity::from(1))
386 .build();
387
388 let result = trailing_stop_calculate(
389 Price::new(0.01, 2),
390 None,
391 None,
392 &order,
393 None,
394 None,
395 Some(Price::new(last_price, 2)),
396 );
397
398 let actual_trigger = result.unwrap().0;
399 match (actual_trigger, expected_trigger) {
400 (Some(actual), Some(expected)) => assert_eq!(actual.as_f64(), expected),
401 (None, None) => (),
402 _ => panic!("Expected trigger {expected_trigger:?} but got {actual_trigger:?}"),
403 }
404 }
405
406 #[rstest]
407 #[case(OrderSide::Buy, 100.0, 50.0, 98.0, Some(98.49))] #[case(OrderSide::Buy, 100.0, 100.0, 97.0, Some(97.97))] #[case(OrderSide::Sell, 100.0, 50.0, 102.0, Some(101.49))] #[case(OrderSide::Sell, 100.0, 100.0, 103.0, Some(101.97))] fn test_trailing_stop_market_basis_points(
412 #[case] side: OrderSide,
413 #[case] initial_trigger: f64,
414 #[case] basis_points: f64,
415 #[case] last_price: f64,
416 #[case] expected_trigger: Option<f64>,
417 ) {
418 let order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
419 .instrument_id("BTCUSDT-PERP.BINANCE".into())
420 .side(side)
421 .trigger_price(Price::new(initial_trigger, 2))
422 .trailing_offset_type(TrailingOffsetType::BasisPoints)
423 .trailing_offset(Decimal::from_f64(basis_points).unwrap())
424 .trigger_type(TriggerType::LastPrice)
425 .quantity(Quantity::from(1))
426 .build();
427
428 let result = trailing_stop_calculate(
429 Price::new(0.01, 2),
430 None,
431 None,
432 &order,
433 None,
434 None,
435 Some(Price::new(last_price, 2)),
436 );
437
438 let actual_trigger = result.unwrap().0;
439 match (actual_trigger, expected_trigger) {
440 (Some(actual), Some(expected)) => assert_eq!(actual.as_f64(), expected),
441 (None, None) => (),
442 _ => panic!("Expected trigger {expected_trigger:?} but got {actual_trigger:?}"),
443 }
444 }
445
446 #[rstest]
447 #[case(OrderSide::Buy, 100.0, 1.0, 98.0, 99.0, None)] #[case(OrderSide::Buy, 100.0, 1.0, 97.0, 98.0, Some(99.0))] #[case(OrderSide::Sell, 100.0, 1.0, 101.0, 102.0, None)] #[case(OrderSide::Sell, 100.0, 1.0, 102.0, 103.0, Some(101.0))] fn test_trailing_stop_market_bid_ask(
452 #[case] side: OrderSide,
453 #[case] initial_trigger: f64,
454 #[case] offset: f64,
455 #[case] bid: f64,
456 #[case] ask: f64,
457 #[case] expected_trigger: Option<f64>,
458 ) {
459 let order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
460 .instrument_id("BTCUSDT-PERP.BINANCE".into())
461 .side(side)
462 .trigger_price(Price::new(initial_trigger, 2))
463 .trailing_offset_type(TrailingOffsetType::Price)
464 .trailing_offset(Decimal::from_f64(offset).unwrap())
465 .trigger_type(TriggerType::BidAsk)
466 .quantity(Quantity::from(1))
467 .build();
468
469 let result = trailing_stop_calculate(
470 Price::new(0.01, 2),
471 None,
472 None,
473 &order,
474 Some(Price::new(bid, 2)),
475 Some(Price::new(ask, 2)),
476 None, );
478
479 let actual_trigger = result.unwrap().0;
480 match (actual_trigger, expected_trigger) {
481 (Some(actual), Some(expected)) => assert_eq!(actual.as_f64(), expected),
482 (None, None) => (),
483 _ => panic!("Expected trigger {expected_trigger:?} but got {actual_trigger:?}"),
484 }
485 }
486
487 #[rstest]
488 #[case(OrderSide::Buy, 100.0, 5, 98.0, Some(98.05))] #[case(OrderSide::Buy, 100.0, 10, 97.0, Some(97.10))] #[case(OrderSide::Sell, 100.0, 5, 102.0, Some(101.95))] #[case(OrderSide::Sell, 100.0, 10, 103.0, Some(102.90))] fn test_trailing_stop_market_ticks(
493 #[case] side: OrderSide,
494 #[case] initial_trigger: f64,
495 #[case] ticks: u32,
496 #[case] last_price: f64,
497 #[case] expected_trigger: Option<f64>,
498 ) {
499 let order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
500 .instrument_id("BTCUSDT-PERP.BINANCE".into())
501 .side(side)
502 .trigger_price(Price::new(initial_trigger, 2))
503 .trailing_offset_type(TrailingOffsetType::Ticks)
504 .trailing_offset(Decimal::from_u32(ticks).unwrap())
505 .trigger_type(TriggerType::LastPrice)
506 .quantity(Quantity::from(1))
507 .build();
508
509 let result = trailing_stop_calculate(
510 Price::new(0.01, 2),
511 None,
512 None,
513 &order,
514 None,
515 None,
516 Some(Price::new(last_price, 2)),
517 );
518
519 let actual_trigger = result.unwrap().0;
520 match (actual_trigger, expected_trigger) {
521 (Some(actual), Some(expected)) => assert_eq!(actual.as_f64(), expected),
522 (None, None) => (),
523 _ => panic!("Expected trigger {expected_trigger:?} but got {actual_trigger:?}"),
524 }
525 }
526
527 #[rstest]
528 #[case(OrderSide::Buy, 100.0, 1.0, 98.0, 97.0, 98.0, Some(99.0))] #[case(OrderSide::Buy, 100.0, 1.0, 97.0, 96.0, 99.0, Some(98.0))] #[case(OrderSide::Sell, 100.0, 1.0, 102.0, 102.0, 103.0, Some(101.0))] #[case(OrderSide::Sell, 100.0, 1.0, 103.0, 101.0, 102.0, Some(102.0))] fn test_trailing_stop_last_or_bid_ask(
533 #[case] side: OrderSide,
534 #[case] initial_trigger: f64,
535 #[case] offset: f64,
536 #[case] last_price: f64,
537 #[case] bid: f64,
538 #[case] ask: f64,
539 #[case] expected_trigger: Option<f64>,
540 ) {
541 let order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
542 .instrument_id("BTCUSDT-PERP.BINANCE".into())
543 .side(side)
544 .trigger_price(Price::new(initial_trigger, 2))
545 .trailing_offset_type(TrailingOffsetType::Price)
546 .trailing_offset(Decimal::from_f64(offset).unwrap())
547 .trigger_type(TriggerType::LastOrBidAsk)
548 .quantity(Quantity::from(1))
549 .build();
550
551 let result = trailing_stop_calculate(
552 Price::new(0.01, 2),
553 None,
554 None,
555 &order,
556 Some(Price::new(bid, 2)),
557 Some(Price::new(ask, 2)),
558 Some(Price::new(last_price, 2)),
559 );
560
561 let actual_trigger = result.unwrap().0;
562 match (actual_trigger, expected_trigger) {
563 (Some(actual), Some(expected)) => assert_eq!(actual.as_f64(), expected),
564 (None, None) => (),
565 _ => panic!("Expected trigger {expected_trigger:?} but got {actual_trigger:?}"),
566 }
567 }
568
569 #[rstest]
570 #[case(OrderSide::Buy, 100.0, 1.0, 98.0, Some(99.0))]
571 #[case(OrderSide::Sell, 100.0, 1.0, 102.0, Some(101.0))]
572 fn test_trailing_stop_market_last_price_move_in_favour(
573 #[case] side: OrderSide,
574 #[case] initial_trigger: f64,
575 #[case] offset: f64,
576 #[case] last_price: f64,
577 #[case] expected_trigger: Option<f64>,
578 ) {
579 let order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
580 .instrument_id("BTCUSDT-PERP.BINANCE".into())
581 .side(side)
582 .trigger_price(Price::new(initial_trigger, 2))
583 .trailing_offset_type(TrailingOffsetType::Price)
584 .trailing_offset(Decimal::from_f64(offset).unwrap())
585 .trigger_type(TriggerType::LastPrice)
586 .quantity(Quantity::from(1))
587 .build();
588
589 let (maybe_trigger, _) = trailing_stop_calculate(
590 Price::new(0.01, 2),
591 None,
592 None,
593 &order,
594 None,
595 None,
596 Some(Price::new(last_price, 2)),
597 )
598 .unwrap();
599
600 match (maybe_trigger, expected_trigger) {
601 (Some(actual), Some(expected)) => assert_eq!(actual.as_f64(), expected),
602 (None, None) => (),
603 _ => panic!("expected {expected_trigger:?}, was {maybe_trigger:?}"),
604 }
605 }
606
607 #[rstest]
608 fn test_trailing_stop_limit_last_price_buy_improve_trigger_and_limit() {
609 let order = OrderTestBuilder::new(OrderType::TrailingStopLimit)
610 .instrument_id("BTCUSDT-PERP.BINANCE".into())
611 .side(OrderSide::Buy)
612 .trigger_price(Price::new(105.0, 2))
613 .price(Price::new(104.5, 2))
614 .trailing_offset_type(TrailingOffsetType::Price)
615 .trailing_offset(dec!(1.0))
616 .limit_offset(dec!(0.5))
617 .trigger_type(TriggerType::LastPrice)
618 .quantity(Quantity::from(1))
619 .build();
620
621 let (new_trigger, new_limit) = trailing_stop_calculate(
622 Price::new(0.01, 2),
623 None,
624 None,
625 &order,
626 None,
627 None,
628 Some(Price::new(100.0, 2)),
629 )
630 .unwrap();
631
632 assert_eq!(new_trigger.unwrap().as_f64(), 101.0);
633 assert_eq!(new_limit.unwrap().as_f64(), 100.5);
634 }
635
636 #[rstest]
637 fn test_trailing_stop_limit_last_price_sell_improve() {
638 let order = OrderTestBuilder::new(OrderType::TrailingStopLimit)
639 .instrument_id("BTCUSDT-PERP.BINANCE".into())
640 .side(OrderSide::Sell)
641 .trigger_price(Price::new(95.0, 2))
642 .price(Price::new(95.5, 2))
643 .trailing_offset_type(TrailingOffsetType::Price)
644 .trailing_offset(dec!(1.0))
645 .limit_offset(dec!(0.5))
646 .trigger_type(TriggerType::LastPrice)
647 .quantity(Quantity::from(1))
648 .build();
649
650 let (new_trigger, new_limit) = trailing_stop_calculate(
651 Price::new(0.01, 2),
652 None,
653 None,
654 &order,
655 None,
656 None,
657 Some(Price::new(100.0, 2)),
658 )
659 .unwrap();
660
661 assert_eq!(new_trigger.unwrap().as_f64(), 99.0);
662 assert_eq!(new_limit.unwrap().as_f64(), 99.5);
663 }
664
665 #[rstest]
666 #[case(OrderSide::Buy, 100.0, 1.0, 99.0)]
667 #[case(OrderSide::Sell, 100.0, 1.0, 101.0)]
668 fn test_no_update_when_candidate_worse(
669 #[case] side: OrderSide,
670 #[case] initial_trigger: f64,
671 #[case] offset: f64,
672 #[case] basis: f64,
673 ) {
674 let order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
675 .instrument_id("BTCUSDT-PERP.BINANCE".into())
676 .side(side)
677 .trigger_price(Price::new(initial_trigger, 2))
678 .trailing_offset_type(TrailingOffsetType::Price)
679 .trailing_offset(Decimal::from_f64(offset).unwrap())
680 .trigger_type(TriggerType::LastPrice)
681 .quantity(Quantity::from(1))
682 .build();
683
684 let (maybe_trigger, _) = trailing_stop_calculate(
685 Price::new(0.01, 2),
686 None,
687 None,
688 &order,
689 None,
690 None,
691 Some(Price::new(basis, 2)),
692 )
693 .unwrap();
694
695 assert!(maybe_trigger.is_none());
696 }
697
698 #[rstest]
699 fn test_trailing_stop_limit_basis_points_buy_improve() {
700 let order = OrderTestBuilder::new(OrderType::TrailingStopLimit)
701 .instrument_id("BTCUSDT-PERP.BINANCE".into())
702 .side(OrderSide::Buy)
703 .trigger_price(Price::new(110.0, 2))
704 .price(Price::new(109.5, 2))
705 .trailing_offset_type(TrailingOffsetType::BasisPoints)
706 .trailing_offset(dec!(50))
707 .limit_offset(dec!(25))
708 .trigger_type(TriggerType::LastPrice)
709 .quantity(Quantity::from(1))
710 .build();
711
712 let (new_trigger, new_limit) = trailing_stop_calculate(
713 Price::new(0.01, 2),
714 None,
715 None,
716 &order,
717 None,
718 None,
719 Some(Price::new(98.0, 2)),
720 )
721 .unwrap();
722
723 assert_eq!(new_trigger.unwrap().as_f64(), 98.49);
724 assert_eq!(new_limit.unwrap().as_f64(), 98.25);
725 }
726}