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