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