1use std::fmt::Display;
19
20use rust_decimal::Decimal;
21
22use crate::enums::{BetSide, OrderSideSpecified};
23
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26#[cfg_attr(
27 feature = "python",
28 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
29)]
30#[cfg_attr(
31 feature = "python",
32 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
33)]
34pub struct Bet {
35 price: Decimal,
36 stake: Decimal,
37 side: BetSide,
38}
39
40impl Bet {
41 #[must_use]
43 pub fn new(price: Decimal, stake: Decimal, side: BetSide) -> Self {
44 Self { price, stake, side }
45 }
46
47 #[must_use]
49 pub fn price(&self) -> Decimal {
50 self.price
51 }
52
53 #[must_use]
55 pub fn stake(&self) -> Decimal {
56 self.stake
57 }
58
59 #[must_use]
61 pub fn side(&self) -> BetSide {
62 self.side
63 }
64
65 #[must_use]
70 pub fn from_stake_or_liability(price: Decimal, volume: Decimal, side: BetSide) -> Self {
71 match side {
72 BetSide::Back => Self::from_stake(price, volume, side),
73 BetSide::Lay => Self::from_liability(price, volume, side),
74 }
75 }
76
77 #[must_use]
79 pub fn from_stake(price: Decimal, stake: Decimal, side: BetSide) -> Self {
80 Self::new(price, stake, side)
81 }
82
83 #[must_use]
89 pub fn from_liability(price: Decimal, liability: Decimal, side: BetSide) -> Self {
90 assert!(
91 side == BetSide::Lay,
92 "Liability-based betting is only applicable for Lay side."
93 );
94 assert!(
95 price > Decimal::ONE,
96 "Price must be greater than 1.0 for lay liability calculation, was {price}"
97 );
98 let adjusted_volume = liability / (price - Decimal::ONE);
99 Self::new(price, adjusted_volume, side)
100 }
101
102 #[must_use]
106 pub fn exposure(&self) -> Decimal {
107 match self.side {
108 BetSide::Back => self.price * self.stake,
109 BetSide::Lay => -self.price * self.stake,
110 }
111 }
112
113 #[must_use]
118 pub fn liability(&self) -> Decimal {
119 match self.side {
120 BetSide::Back => self.stake,
121 BetSide::Lay => self.stake * (self.price - Decimal::ONE),
122 }
123 }
124
125 #[must_use]
129 pub fn profit(&self) -> Decimal {
130 match self.side {
131 BetSide::Back => self.stake * (self.price - Decimal::ONE),
132 BetSide::Lay => self.stake,
133 }
134 }
135
136 #[must_use]
140 pub fn outcome_win_payoff(&self) -> Decimal {
141 match self.side {
142 BetSide::Back => self.profit(),
143 BetSide::Lay => -self.liability(),
144 }
145 }
146
147 #[must_use]
151 pub fn outcome_lose_payoff(&self) -> Decimal {
152 match self.side {
153 BetSide::Back => -self.liability(),
154 BetSide::Lay => self.profit(),
155 }
156 }
157
158 #[must_use]
160 pub fn hedging_stake(&self, price: Decimal) -> Decimal {
161 match self.side {
162 BetSide::Back => (self.price / price) * self.stake,
163 BetSide::Lay => self.stake / (price / self.price),
164 }
165 }
166
167 #[must_use]
169 pub fn hedging_bet(&self, price: Decimal) -> Self {
170 Self::new(price, self.hedging_stake(price), self.side.opposite())
171 }
172}
173
174impl Display for Bet {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 write!(
178 f,
179 "Bet({:?} @ {:.2} x{:.2})",
180 self.side, self.price, self.stake
181 )
182 }
183}
184
185#[derive(Debug, Clone)]
187#[cfg_attr(
188 feature = "python",
189 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
190)]
191#[cfg_attr(
192 feature = "python",
193 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
194)]
195pub struct BetPosition {
196 price: Decimal,
197 exposure: Decimal,
198 realized_pnl: Decimal,
199 bets: Vec<Bet>,
200}
201
202impl Default for BetPosition {
203 fn default() -> Self {
204 Self {
205 price: Decimal::ZERO,
206 exposure: Decimal::ZERO,
207 realized_pnl: Decimal::ZERO,
208 bets: vec![],
209 }
210 }
211}
212
213impl BetPosition {
214 #[must_use]
216 pub fn price(&self) -> Decimal {
217 self.price
218 }
219
220 #[must_use]
222 pub fn exposure(&self) -> Decimal {
223 self.exposure
224 }
225
226 #[must_use]
228 pub fn realized_pnl(&self) -> Decimal {
229 self.realized_pnl
230 }
231
232 #[must_use]
234 pub fn bets(&self) -> &[Bet] {
235 &self.bets
236 }
237
238 #[must_use]
242 pub fn side(&self) -> Option<BetSide> {
243 match self.exposure.cmp(&Decimal::ZERO) {
244 std::cmp::Ordering::Less => Some(BetSide::Lay),
245 std::cmp::Ordering::Greater => Some(BetSide::Back),
246 std::cmp::Ordering::Equal => None,
247 }
248 }
249
250 #[must_use]
252 pub fn as_bet(&self) -> Option<Bet> {
253 self.side().map(|side| {
254 let stake = match side {
255 BetSide::Back => self.exposure / self.price,
256 BetSide::Lay => -self.exposure / self.price,
257 };
258 Bet::new(self.price, stake, side)
259 })
260 }
261
262 pub fn add_bet(&mut self, bet: Bet) {
264 match self.side() {
265 None => self.position_increase(&bet),
266 Some(current_side) => {
267 if current_side == bet.side {
268 self.position_increase(&bet);
269 } else {
270 self.position_decrease(&bet);
271 }
272 }
273 }
274 self.bets.push(bet);
275 }
276
277 pub fn position_increase(&mut self, bet: &Bet) {
279 if self.side().is_none() {
280 self.price = bet.price;
281 }
282 self.exposure += bet.exposure();
283 }
284
285 pub fn position_decrease(&mut self, bet: &Bet) {
291 let abs_bet_exposure = bet.exposure().abs();
292 let abs_self_exposure = self.exposure.abs();
293
294 match abs_bet_exposure.cmp(&abs_self_exposure) {
295 std::cmp::Ordering::Less => {
296 let decreasing_volume = abs_bet_exposure / self.price;
297 let current_side = self.side().unwrap();
298 let decreasing_bet = Bet::new(self.price, decreasing_volume, current_side);
299 let pnl = calc_bets_pnl(&[bet.clone(), decreasing_bet]);
300 self.realized_pnl += pnl;
301 self.exposure += bet.exposure();
302 }
303 std::cmp::Ordering::Greater => {
304 if let Some(self_bet) = self.as_bet() {
305 let pnl = calc_bets_pnl(&[bet.clone(), self_bet]);
306 self.realized_pnl += pnl;
307 }
308 self.price = bet.price;
309 self.exposure += bet.exposure();
310 }
311 std::cmp::Ordering::Equal => {
312 if let Some(self_bet) = self.as_bet() {
313 let pnl = calc_bets_pnl(&[bet.clone(), self_bet]);
314 self.realized_pnl += pnl;
315 }
316 self.price = Decimal::ZERO;
317 self.exposure = Decimal::ZERO;
318 }
319 }
320 }
321
322 #[must_use]
324 pub fn unrealized_pnl(&self, price: Decimal) -> Decimal {
325 if self.side().is_none() {
326 Decimal::ZERO
327 } else if let Some(flattening_bet) = self.flattening_bet(price) {
328 if let Some(self_bet) = self.as_bet() {
329 calc_bets_pnl(&[flattening_bet, self_bet])
330 } else {
331 Decimal::ZERO
332 }
333 } else {
334 Decimal::ZERO
335 }
336 }
337
338 #[must_use]
340 pub fn total_pnl(&self, price: Decimal) -> Decimal {
341 self.realized_pnl + self.unrealized_pnl(price)
342 }
343
344 #[must_use]
346 pub fn flattening_bet(&self, price: Decimal) -> Option<Bet> {
347 self.side().map(|side| {
348 let stake = match side {
349 BetSide::Back => self.exposure / price,
350 BetSide::Lay => -self.exposure / price,
351 };
352 Bet::new(price, stake, side.opposite())
354 })
355 }
356
357 pub fn reset(&mut self) {
359 self.price = Decimal::ZERO;
360 self.exposure = Decimal::ZERO;
361 self.realized_pnl = Decimal::ZERO;
362 }
363}
364
365impl Display for BetPosition {
366 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
367 write!(
368 f,
369 "BetPosition(price: {:.2}, exposure: {:.2}, realized_pnl: {:.2})",
370 self.price, self.exposure, self.realized_pnl
371 )
372 }
373}
374
375#[must_use]
377pub fn calc_bets_pnl(bets: &[Bet]) -> Decimal {
378 bets.iter()
379 .fold(Decimal::ZERO, |acc, bet| acc + bet.outcome_win_payoff())
380}
381
382pub fn check_probability_non_zero(probability: Decimal) -> anyhow::Result<()> {
388 if probability.is_zero() {
389 anyhow::bail!("invalid probability: must be non-zero")
390 }
391 Ok(())
392}
393
394pub fn check_probability_invertible(probability: Decimal) -> anyhow::Result<()> {
400 if probability == Decimal::ONE {
401 anyhow::bail!("invalid probability: must not be 1.0 (inverse would be zero)")
402 }
403 Ok(())
404}
405
406pub fn probability_to_bet(
414 probability: Decimal,
415 volume: Decimal,
416 side: OrderSideSpecified,
417) -> anyhow::Result<Bet> {
418 check_probability_non_zero(probability)?;
419 let price = Decimal::ONE / probability;
420 let bet = match side {
421 OrderSideSpecified::Buy => Bet::new(price, volume / price, BetSide::Back),
422 OrderSideSpecified::Sell => Bet::new(price, volume / price, BetSide::Lay),
423 };
424 Ok(bet)
425}
426
427pub fn inverse_probability_to_bet(
435 probability: Decimal,
436 volume: Decimal,
437 side: OrderSideSpecified,
438) -> anyhow::Result<Bet> {
439 check_probability_invertible(probability)?;
440 let inverse_probability = Decimal::ONE - probability;
441 let inverse_side = match side {
442 OrderSideSpecified::Buy => OrderSideSpecified::Sell,
443 OrderSideSpecified::Sell => OrderSideSpecified::Buy,
444 };
445 probability_to_bet(inverse_probability, volume, inverse_side)
446}
447
448#[cfg(test)]
449mod tests {
450 use rstest::rstest;
451 use rust_decimal::Decimal;
452 use rust_decimal_macros::dec;
453
454 use super::*;
455
456 fn dec_str(s: &str) -> Decimal {
457 s.parse::<Decimal>().expect("Failed to parse Decimal")
458 }
459
460 #[rstest]
461 #[should_panic(expected = "Liability-based betting is only applicable for Lay side.")]
462 fn test_from_liability_panics_on_back_side() {
463 let _ = Bet::from_liability(dec!(2.0), dec!(100.0), BetSide::Back);
464 }
465
466 #[rstest]
467 fn test_bet_creation() {
468 let price = dec!(2.0);
469 let stake = dec!(100.0);
470 let side = BetSide::Back;
471 let bet = Bet::new(price, stake, side);
472 assert_eq!(bet.price, price);
473 assert_eq!(bet.stake, stake);
474 assert_eq!(bet.side, side);
475 }
476
477 #[rstest]
478 fn test_display_bet() {
479 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
480 let formatted = format!("{bet}");
481 assert!(formatted.contains("Back"));
482 assert!(formatted.contains("2.00"));
483 assert!(formatted.contains("100.00"));
484 }
485
486 #[rstest]
487 fn test_bet_exposure_back() {
488 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
489 let exposure = bet.exposure();
490 assert_eq!(exposure, dec!(200.0));
491 }
492
493 #[rstest]
494 fn test_bet_exposure_lay() {
495 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
496 let exposure = bet.exposure();
497 assert_eq!(exposure, dec!(-200.0));
498 }
499
500 #[rstest]
501 fn test_bet_liability_back() {
502 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
503 let liability = bet.liability();
504 assert_eq!(liability, dec!(100.0));
505 }
506
507 #[rstest]
508 fn test_bet_liability_lay() {
509 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
510 let liability = bet.liability();
511 assert_eq!(liability, dec!(100.0));
512 }
513
514 #[rstest]
515 fn test_bet_profit_back() {
516 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
517 let profit = bet.profit();
518 assert_eq!(profit, dec!(100.0));
519 }
520
521 #[rstest]
522 fn test_bet_profit_lay() {
523 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
524 let profit = bet.profit();
525 assert_eq!(profit, dec!(100.0));
526 }
527
528 #[rstest]
529 fn test_outcome_win_payoff_back() {
530 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
531 let win_payoff = bet.outcome_win_payoff();
532 assert_eq!(win_payoff, dec!(100.0));
533 }
534
535 #[rstest]
536 fn test_outcome_win_payoff_lay() {
537 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
538 let win_payoff = bet.outcome_win_payoff();
539 assert_eq!(win_payoff, dec!(-100.0));
540 }
541
542 #[rstest]
543 fn test_outcome_lose_payoff_back() {
544 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
545 let lose_payoff = bet.outcome_lose_payoff();
546 assert_eq!(lose_payoff, dec!(-100.0));
547 }
548
549 #[rstest]
550 fn test_outcome_lose_payoff_lay() {
551 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
552 let lose_payoff = bet.outcome_lose_payoff();
553 assert_eq!(lose_payoff, dec!(100.0));
554 }
555
556 #[rstest]
557 fn test_hedging_stake_back() {
558 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
559 let hedging_stake = bet.hedging_stake(dec!(1.5));
560 assert_eq!(hedging_stake.round_dp(8), dec_str("133.33333333"));
562 }
563
564 #[rstest]
565 fn test_hedging_bet_lay() {
566 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
567 let hedge_bet = bet.hedging_bet(dec!(1.5));
568 assert_eq!(hedge_bet.side, BetSide::Back);
569 assert_eq!(hedge_bet.price, dec!(1.5));
570 assert_eq!(hedge_bet.stake.round_dp(8), dec_str("133.33333333"));
571 }
572
573 #[rstest]
574 fn test_bet_position_initialization() {
575 let position = BetPosition::default();
576 assert_eq!(position.price, dec!(0.0));
577 assert_eq!(position.exposure, dec!(0.0));
578 assert_eq!(position.realized_pnl, dec!(0.0));
579 }
580
581 #[rstest]
582 fn test_display_bet_position() {
583 let mut position = BetPosition::default();
584 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
585 position.add_bet(bet);
586 let formatted = format!("{position}");
587
588 assert!(formatted.contains("price"));
589 assert!(formatted.contains("exposure"));
590 assert!(formatted.contains("realized_pnl"));
591 }
592
593 #[rstest]
594 fn test_as_bet() {
595 let mut position = BetPosition::default();
596 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
598 position.add_bet(bet);
599 let as_bet = position.as_bet().expect("Expected a bet representation");
600
601 assert_eq!(as_bet.price, position.price);
602 assert_eq!(as_bet.stake, position.exposure / position.price);
603 assert_eq!(as_bet.side, BetSide::Back);
604 }
605
606 #[rstest]
607 fn test_reset_position() {
608 let mut position = BetPosition::default();
609 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
610 position.add_bet(bet);
611 assert!(position.exposure != dec!(0.0));
612 position.reset();
613
614 assert_eq!(position.price, dec!(0.0));
616 assert_eq!(position.exposure, dec!(0.0));
617 assert_eq!(position.realized_pnl, dec!(0.0));
618 }
619
620 #[rstest]
621 fn test_bet_position_side_none() {
622 let position = BetPosition::default();
623 assert!(position.side().is_none());
624 }
625
626 #[rstest]
627 fn test_bet_position_side_back() {
628 let mut position = BetPosition::default();
629 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
630 position.add_bet(bet);
631 assert_eq!(position.side(), Some(BetSide::Back));
632 }
633
634 #[rstest]
635 fn test_bet_position_side_lay() {
636 let mut position = BetPosition::default();
637 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
638 position.add_bet(bet);
639 assert_eq!(position.side(), Some(BetSide::Lay));
640 }
641
642 #[rstest]
643 fn test_position_increase_back() {
644 let mut position = BetPosition::default();
645 let bet1 = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
646 let bet2 = Bet::new(dec!(2.0), dec!(50.0), BetSide::Back);
647 position.add_bet(bet1);
648 position.add_bet(bet2);
649 assert_eq!(position.exposure, dec!(300.0));
651 }
652
653 #[rstest]
654 fn test_position_increase_lay() {
655 let mut position = BetPosition::default();
656 let bet1 = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
657 let bet2 = Bet::new(dec!(2.0), dec!(50.0), BetSide::Lay);
658 position.add_bet(bet1);
659 position.add_bet(bet2);
660 assert_eq!(position.exposure, dec!(-300.0));
662 }
663
664 #[rstest]
665 fn test_position_back_then_lay() {
666 let mut position = BetPosition::default();
667 let bet1 = Bet::new(dec!(3.0), dec!(100_000), BetSide::Back);
668 let bet2 = Bet::new(dec!(2.0), dec!(10_000), BetSide::Lay);
669 position.add_bet(bet1);
670 position.add_bet(bet2);
671
672 assert_eq!(position.exposure, dec!(280_000.0));
673 assert_eq!(position.realized_pnl(), dec!(3333.333333333333333333333333));
674 assert_eq!(
675 position.unrealized_pnl(dec!(4.0)),
676 dec!(-23333.33333333333333333333334)
677 );
678 }
679
680 #[rstest]
681 fn test_position_lay_then_back() {
682 let mut position = BetPosition::default();
683 let bet1 = Bet::new(dec!(2.0), dec!(10_000), BetSide::Lay);
684 let bet2 = Bet::new(dec!(3.0), dec!(100_000), BetSide::Back);
685 position.add_bet(bet1);
686 position.add_bet(bet2);
687
688 assert_eq!(position.exposure, dec!(280_000.0));
689 assert_eq!(position.realized_pnl(), dec!(190_000));
690 assert_eq!(
691 position.unrealized_pnl(dec!(4.0)),
692 dec!(-23333.33333333333333333333334)
693 );
694 }
695
696 #[rstest]
697 fn test_position_flip() {
698 let mut position = BetPosition::default();
699 let back_bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back); let lay_bet = Bet::new(dec!(2.0), dec!(150.0), BetSide::Lay); position.add_bet(back_bet);
702 position.add_bet(lay_bet);
703 assert_eq!(position.side(), Some(BetSide::Lay));
705 assert_eq!(position.exposure, dec!(-100.0));
706 }
707
708 #[rstest]
709 fn test_position_flat() {
710 let mut position = BetPosition::default();
711 let back_bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back); let lay_bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay); position.add_bet(back_bet);
714 position.add_bet(lay_bet);
715 assert!(position.side().is_none());
716 assert_eq!(position.exposure, dec!(0.0));
717 }
718
719 #[rstest]
720 fn test_unrealized_pnl_negative() {
721 let mut position = BetPosition::default();
722 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back); position.add_bet(bet);
724 let unrealized_pnl = position.unrealized_pnl(dec!(2.5));
726 assert_eq!(unrealized_pnl, dec!(-20.0));
727 }
728
729 #[rstest]
730 fn test_total_pnl() {
731 let mut position = BetPosition::default();
732 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
733 position.add_bet(bet);
734 position.realized_pnl = dec!(10.0);
735 let total_pnl = position.total_pnl(dec!(2.5));
736 assert_eq!(total_pnl, dec!(-10.0));
738 }
739
740 #[rstest]
741 fn test_flattening_bet_back_profit() {
742 let mut position = BetPosition::default();
743 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
744 position.add_bet(bet);
745 let flattening_bet = position
746 .flattening_bet(dec!(1.6))
747 .expect("expected a flattening bet");
748 assert_eq!(flattening_bet.side, BetSide::Lay);
749 assert_eq!(flattening_bet.stake, dec_str("125"));
750 }
751
752 #[rstest]
753 fn test_flattening_bet_back_hack() {
754 let mut position = BetPosition::default();
755 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
756 position.add_bet(bet);
757 let flattening_bet = position
758 .flattening_bet(dec!(2.5))
759 .expect("expected a flattening bet");
760 assert_eq!(flattening_bet.side, BetSide::Lay);
761 assert_eq!(flattening_bet.stake, dec!(80.0));
763 }
764
765 #[rstest]
766 fn test_flattening_bet_lay() {
767 let mut position = BetPosition::default();
768 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
769 position.add_bet(bet);
770 let flattening_bet = position
771 .flattening_bet(dec!(1.5))
772 .expect("expected a flattening bet");
773 assert_eq!(flattening_bet.side, BetSide::Back);
774 assert_eq!(flattening_bet.stake.round_dp(8), dec_str("133.33333333"));
775 }
776
777 #[rstest]
778 fn test_realized_pnl_flattening() {
779 let back = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back); let lay = Bet::new(dec!(4.0), dec!(125.0), BetSide::Lay); let mut position = BetPosition::default();
782 position.add_bet(back);
783 position.add_bet(lay);
784 assert_eq!(position.realized_pnl, dec!(25.0));
786 }
787
788 #[rstest]
789 fn test_realized_pnl_single_side() {
790 let back = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back);
791 let mut position = BetPosition::default();
792 position.add_bet(back);
793 assert_eq!(position.realized_pnl, dec!(0.0));
795 }
796
797 #[rstest]
798 fn test_realized_pnl_open_position() {
799 let back = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back); let lay = Bet::new(dec!(4.0), dec!(100.0), BetSide::Lay); let mut position = BetPosition::default();
802 position.add_bet(back);
803 position.add_bet(lay);
804 assert_eq!(position.realized_pnl, dec!(20.0));
806 }
807
808 #[rstest]
809 fn test_realized_pnl_partial_close() {
810 let back = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back); let lay = Bet::new(dec!(4.0), dec!(110.0), BetSide::Lay); let mut position = BetPosition::default();
813 position.add_bet(back);
814 position.add_bet(lay);
815 assert_eq!(position.realized_pnl, dec!(22.0));
817 }
818
819 #[rstest]
820 fn test_realized_pnl_flipping() {
821 let back = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back); let lay = Bet::new(dec!(4.0), dec!(130.0), BetSide::Lay); let mut position = BetPosition::default();
824 position.add_bet(back);
825 position.add_bet(lay);
826 assert_eq!(position.realized_pnl, dec!(10.0));
828 }
829
830 #[rstest]
831 fn test_unrealized_pnl_positive() {
832 let back = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back); let mut position = BetPosition::default();
834 position.add_bet(back);
835 let unrealized_pnl = position.unrealized_pnl(dec!(4.0));
836 assert_eq!(unrealized_pnl, dec!(25.0));
838 }
839
840 #[rstest]
841 fn test_total_pnl_with_pnl() {
842 let back = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back); let lay = Bet::new(dec!(4.0), dec!(120.0), BetSide::Lay); let mut position = BetPosition::default();
845 position.add_bet(back);
846 position.add_bet(lay);
847 let realized_pnl = position.realized_pnl;
849 let unrealized_pnl = position.unrealized_pnl(dec!(4.0));
850 let total_pnl = position.total_pnl(dec!(4.0));
851 assert_eq!(realized_pnl, dec!(24.0));
852 assert_eq!(unrealized_pnl, dec!(1.0));
853 assert_eq!(total_pnl, dec!(25.0));
854 }
855
856 #[rstest]
857 fn test_open_position_realized_unrealized() {
858 let back = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back); let lay = Bet::new(dec!(4.0), dec!(100.0), BetSide::Lay); let mut position = BetPosition::default();
861 position.add_bet(back);
862 position.add_bet(lay);
863 let unrealized_pnl = position.unrealized_pnl(dec!(4.0));
864 assert_eq!(unrealized_pnl, dec!(5.0));
866 }
867
868 #[rstest]
869 fn test_unrealized_no_position() {
870 let back = Bet::new(dec!(5.0), dec!(100.0), BetSide::Lay);
871 let mut position = BetPosition::default();
872 position.add_bet(back);
873 let unrealized_pnl = position.unrealized_pnl(dec!(5.0));
874 assert_eq!(unrealized_pnl, dec!(0.0));
875 }
876
877 #[rstest]
878 fn test_calc_bets_pnl_single_back_bet() {
879 let bet = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back);
880 let pnl = calc_bets_pnl(&[bet]);
881 assert_eq!(pnl, dec!(400.0));
882 }
883
884 #[rstest]
885 fn test_calc_bets_pnl_single_lay_bet() {
886 let bet = Bet::new(dec!(4.0), dec!(100.0), BetSide::Lay);
887 let pnl = calc_bets_pnl(&[bet]);
888 assert_eq!(pnl, dec!(-300.0));
889 }
890
891 #[rstest]
892 fn test_calc_bets_pnl_multiple_bets() {
893 let back_bet = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back);
894 let lay_bet = Bet::new(dec!(4.0), dec!(100.0), BetSide::Lay);
895 let pnl = calc_bets_pnl(&[back_bet, lay_bet]);
896 let expected = dec!(400.0) + dec!(-300.0);
897 assert_eq!(pnl, expected);
898 }
899
900 #[rstest]
901 fn test_calc_bets_pnl_mixed_bets() {
902 let back_bet1 = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back);
903 let back_bet2 = Bet::new(dec!(2.0), dec!(50.0), BetSide::Back);
904 let lay_bet1 = Bet::new(dec!(3.0), dec!(75.0), BetSide::Lay);
905 let pnl = calc_bets_pnl(&[back_bet1, back_bet2, lay_bet1]);
906 let expected = dec!(400.0) + dec!(50.0) + dec!(-150.0);
907 assert_eq!(pnl, expected);
908 }
909
910 #[rstest]
911 fn test_calc_bets_pnl_no_bets() {
912 let bets: Vec<Bet> = vec![];
913 let pnl = calc_bets_pnl(&bets);
914 assert_eq!(pnl, dec!(0.0));
915 }
916
917 #[rstest]
918 fn test_calc_bets_pnl_zero_outcome() {
919 let back_bet = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back);
920 let lay_bet = Bet::new(dec!(5.0), dec!(100.0), BetSide::Lay);
921 let pnl = calc_bets_pnl(&[back_bet, lay_bet]);
922 assert_eq!(pnl, dec!(0.0));
923 }
924
925 #[rstest]
926 fn test_probability_to_bet_back_simple() {
927 let bet = probability_to_bet(dec!(0.50), dec!(50.0), OrderSideSpecified::Buy).unwrap();
929 let expected = Bet::new(dec!(2.0), dec!(25.0), BetSide::Back);
930 assert_eq!(bet, expected);
931 assert_eq!(bet.outcome_win_payoff(), dec!(25.0));
932 assert_eq!(bet.outcome_lose_payoff(), dec!(-25.0));
933 }
934
935 #[rstest]
936 fn test_probability_to_bet_back_high_prob() {
937 let bet = probability_to_bet(dec!(0.64), dec!(50.0), OrderSideSpecified::Buy).unwrap();
938 let expected = Bet::new(dec!(1.5625), dec!(32.0), BetSide::Back);
939 assert_eq!(bet, expected);
940 assert_eq!(bet.outcome_win_payoff(), dec!(18.0));
941 assert_eq!(bet.outcome_lose_payoff(), dec!(-32.0));
942 }
943
944 #[rstest]
945 fn test_probability_to_bet_back_low_prob() {
946 let bet = probability_to_bet(dec!(0.40), dec!(50.0), OrderSideSpecified::Buy).unwrap();
947 let expected = Bet::new(dec!(2.5), dec!(20.0), BetSide::Back);
948 assert_eq!(bet, expected);
949 assert_eq!(bet.outcome_win_payoff(), dec!(30.0));
950 assert_eq!(bet.outcome_lose_payoff(), dec!(-20.0));
951 }
952
953 #[rstest]
954 fn test_probability_to_bet_sell() {
955 let bet = probability_to_bet(dec!(0.80), dec!(50.0), OrderSideSpecified::Sell).unwrap();
956 let expected = Bet::new(dec_str("1.25"), dec_str("40"), BetSide::Lay);
957 assert_eq!(bet, expected);
958 assert_eq!(bet.outcome_win_payoff(), dec_str("-10"));
959 assert_eq!(bet.outcome_lose_payoff(), dec_str("40"));
960 }
961
962 #[rstest]
963 fn test_inverse_probability_to_bet() {
964 let original_bet =
966 probability_to_bet(dec!(0.80), dec!(100.0), OrderSideSpecified::Sell).unwrap();
967 let reverse_bet =
969 probability_to_bet(dec!(0.20), dec!(100.0), OrderSideSpecified::Buy).unwrap();
970 let inverse_bet =
971 inverse_probability_to_bet(dec!(0.80), dec!(100.0), OrderSideSpecified::Sell).unwrap();
972
973 assert_eq!(
974 original_bet.outcome_win_payoff(),
975 reverse_bet.outcome_lose_payoff(),
976 );
977 assert_eq!(
978 original_bet.outcome_win_payoff(),
979 inverse_bet.outcome_lose_payoff(),
980 );
981 assert_eq!(
982 original_bet.outcome_lose_payoff(),
983 reverse_bet.outcome_win_payoff(),
984 );
985 assert_eq!(
986 original_bet.outcome_lose_payoff(),
987 inverse_bet.outcome_win_payoff(),
988 );
989 }
990
991 #[rstest]
992 fn test_inverse_probability_to_bet_example2() {
993 let original_bet =
994 probability_to_bet(dec!(0.64), dec!(50.0), OrderSideSpecified::Sell).unwrap();
995 let inverse_bet =
996 inverse_probability_to_bet(dec!(0.64), dec!(50.0), OrderSideSpecified::Sell).unwrap();
997
998 assert_eq!(original_bet.stake, dec!(32.0));
999 assert_eq!(original_bet.outcome_win_payoff(), dec!(-18.0));
1000 assert_eq!(original_bet.outcome_lose_payoff(), dec!(32.0));
1001
1002 assert_eq!(inverse_bet.stake, dec!(18.0));
1003 assert_eq!(inverse_bet.outcome_win_payoff(), dec!(32.0));
1004 assert_eq!(inverse_bet.outcome_lose_payoff(), dec!(-18.0));
1005 }
1006}