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 self.bets.clear();
363 }
364}
365
366impl Display for BetPosition {
367 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368 write!(
369 f,
370 "BetPosition(price: {:.2}, exposure: {:.2}, realized_pnl: {:.2})",
371 self.price, self.exposure, self.realized_pnl
372 )
373 }
374}
375
376#[must_use]
378pub fn calc_bets_pnl(bets: &[Bet]) -> Decimal {
379 bets.iter()
380 .fold(Decimal::ZERO, |acc, bet| acc + bet.outcome_win_payoff())
381}
382
383pub fn check_probability_non_zero(probability: Decimal) -> anyhow::Result<()> {
389 if probability.is_zero() {
390 anyhow::bail!("invalid probability: must be non-zero")
391 }
392 Ok(())
393}
394
395pub fn check_probability_invertible(probability: Decimal) -> anyhow::Result<()> {
401 if probability == Decimal::ONE {
402 anyhow::bail!("invalid probability: must not be 1.0 (inverse would be zero)")
403 }
404 Ok(())
405}
406
407pub fn probability_to_bet(
415 probability: Decimal,
416 volume: Decimal,
417 side: OrderSideSpecified,
418) -> anyhow::Result<Bet> {
419 check_probability_non_zero(probability)?;
420 let price = Decimal::ONE / probability;
421 let bet = match side {
422 OrderSideSpecified::Buy => Bet::new(price, volume / price, BetSide::Back),
423 OrderSideSpecified::Sell => Bet::new(price, volume / price, BetSide::Lay),
424 };
425 Ok(bet)
426}
427
428pub fn inverse_probability_to_bet(
436 probability: Decimal,
437 volume: Decimal,
438 side: OrderSideSpecified,
439) -> anyhow::Result<Bet> {
440 check_probability_invertible(probability)?;
441 let inverse_probability = Decimal::ONE - probability;
442 let inverse_side = match side {
443 OrderSideSpecified::Buy => OrderSideSpecified::Sell,
444 OrderSideSpecified::Sell => OrderSideSpecified::Buy,
445 };
446 probability_to_bet(inverse_probability, volume, inverse_side)
447}
448
449#[cfg(test)]
450mod tests {
451 use rstest::rstest;
452 use rust_decimal::Decimal;
453 use rust_decimal_macros::dec;
454
455 use super::*;
456
457 fn dec_str(s: &str) -> Decimal {
458 s.parse::<Decimal>().expect("Failed to parse Decimal")
459 }
460
461 #[rstest]
462 #[should_panic(expected = "Liability-based betting is only applicable for Lay side.")]
463 fn test_from_liability_panics_on_back_side() {
464 let _ = Bet::from_liability(dec!(2.0), dec!(100.0), BetSide::Back);
465 }
466
467 #[rstest]
468 fn test_bet_creation() {
469 let price = dec!(2.0);
470 let stake = dec!(100.0);
471 let side = BetSide::Back;
472 let bet = Bet::new(price, stake, side);
473 assert_eq!(bet.price, price);
474 assert_eq!(bet.stake, stake);
475 assert_eq!(bet.side, side);
476 }
477
478 #[rstest]
479 fn test_display_bet() {
480 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
481 let formatted = format!("{bet}");
482 assert!(formatted.contains("Back"));
483 assert!(formatted.contains("2.00"));
484 assert!(formatted.contains("100.00"));
485 }
486
487 #[rstest]
488 fn test_bet_exposure_back() {
489 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
490 let exposure = bet.exposure();
491 assert_eq!(exposure, dec!(200.0));
492 }
493
494 #[rstest]
495 fn test_bet_exposure_lay() {
496 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
497 let exposure = bet.exposure();
498 assert_eq!(exposure, dec!(-200.0));
499 }
500
501 #[rstest]
502 fn test_bet_liability_back() {
503 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
504 let liability = bet.liability();
505 assert_eq!(liability, dec!(100.0));
506 }
507
508 #[rstest]
509 fn test_bet_liability_lay() {
510 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
511 let liability = bet.liability();
512 assert_eq!(liability, dec!(100.0));
513 }
514
515 #[rstest]
516 fn test_bet_profit_back() {
517 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
518 let profit = bet.profit();
519 assert_eq!(profit, dec!(100.0));
520 }
521
522 #[rstest]
523 fn test_bet_profit_lay() {
524 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
525 let profit = bet.profit();
526 assert_eq!(profit, dec!(100.0));
527 }
528
529 #[rstest]
530 fn test_outcome_win_payoff_back() {
531 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
532 let win_payoff = bet.outcome_win_payoff();
533 assert_eq!(win_payoff, dec!(100.0));
534 }
535
536 #[rstest]
537 fn test_outcome_win_payoff_lay() {
538 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
539 let win_payoff = bet.outcome_win_payoff();
540 assert_eq!(win_payoff, dec!(-100.0));
541 }
542
543 #[rstest]
544 fn test_outcome_lose_payoff_back() {
545 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
546 let lose_payoff = bet.outcome_lose_payoff();
547 assert_eq!(lose_payoff, dec!(-100.0));
548 }
549
550 #[rstest]
551 fn test_outcome_lose_payoff_lay() {
552 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
553 let lose_payoff = bet.outcome_lose_payoff();
554 assert_eq!(lose_payoff, dec!(100.0));
555 }
556
557 #[rstest]
558 fn test_hedging_stake_back() {
559 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
560 let hedging_stake = bet.hedging_stake(dec!(1.5));
561 assert_eq!(hedging_stake.round_dp(8), dec_str("133.33333333"));
563 }
564
565 #[rstest]
566 fn test_hedging_bet_lay() {
567 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
568 let hedge_bet = bet.hedging_bet(dec!(1.5));
569 assert_eq!(hedge_bet.side, BetSide::Back);
570 assert_eq!(hedge_bet.price, dec!(1.5));
571 assert_eq!(hedge_bet.stake.round_dp(8), dec_str("133.33333333"));
572 }
573
574 #[rstest]
575 fn test_bet_position_initialization() {
576 let position = BetPosition::default();
577 assert_eq!(position.price, dec!(0.0));
578 assert_eq!(position.exposure, dec!(0.0));
579 assert_eq!(position.realized_pnl, dec!(0.0));
580 }
581
582 #[rstest]
583 fn test_display_bet_position() {
584 let mut position = BetPosition::default();
585 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
586 position.add_bet(bet);
587 let formatted = format!("{position}");
588
589 assert!(formatted.contains("price"));
590 assert!(formatted.contains("exposure"));
591 assert!(formatted.contains("realized_pnl"));
592 }
593
594 #[rstest]
595 fn test_as_bet() {
596 let mut position = BetPosition::default();
597 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
599 position.add_bet(bet);
600 let as_bet = position.as_bet().expect("Expected a bet representation");
601
602 assert_eq!(as_bet.price, position.price);
603 assert_eq!(as_bet.stake, position.exposure / position.price);
604 assert_eq!(as_bet.side, BetSide::Back);
605 }
606
607 #[rstest]
608 fn test_reset_position() {
609 let mut position = BetPosition::default();
610 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
611 position.add_bet(bet);
612 assert_ne!(position.exposure, dec!(0.0));
613 assert!(!position.bets().is_empty());
614 position.reset();
615
616 assert_eq!(position.price, dec!(0.0));
618 assert_eq!(position.exposure, dec!(0.0));
619 assert_eq!(position.realized_pnl, dec!(0.0));
620 assert!(position.bets().is_empty());
621 }
622
623 #[rstest]
624 fn test_bet_position_side_none() {
625 let position = BetPosition::default();
626 assert!(position.side().is_none());
627 }
628
629 #[rstest]
630 fn test_bet_position_side_back() {
631 let mut position = BetPosition::default();
632 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
633 position.add_bet(bet);
634 assert_eq!(position.side(), Some(BetSide::Back));
635 }
636
637 #[rstest]
638 fn test_bet_position_side_lay() {
639 let mut position = BetPosition::default();
640 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
641 position.add_bet(bet);
642 assert_eq!(position.side(), Some(BetSide::Lay));
643 }
644
645 #[rstest]
646 fn test_position_increase_back() {
647 let mut position = BetPosition::default();
648 let bet1 = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
649 let bet2 = Bet::new(dec!(2.0), dec!(50.0), BetSide::Back);
650 position.add_bet(bet1);
651 position.add_bet(bet2);
652 assert_eq!(position.exposure, dec!(300.0));
654 }
655
656 #[rstest]
657 fn test_position_increase_lay() {
658 let mut position = BetPosition::default();
659 let bet1 = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
660 let bet2 = Bet::new(dec!(2.0), dec!(50.0), BetSide::Lay);
661 position.add_bet(bet1);
662 position.add_bet(bet2);
663 assert_eq!(position.exposure, dec!(-300.0));
665 }
666
667 #[rstest]
668 fn test_position_back_then_lay() {
669 let mut position = BetPosition::default();
670 let bet1 = Bet::new(dec!(3.0), dec!(100_000), BetSide::Back);
671 let bet2 = Bet::new(dec!(2.0), dec!(10_000), BetSide::Lay);
672 position.add_bet(bet1);
673 position.add_bet(bet2);
674
675 assert_eq!(position.exposure, dec!(280_000.0));
676 assert_eq!(position.realized_pnl(), dec!(3333.333333333333333333333333));
677 assert_eq!(
678 position.unrealized_pnl(dec!(4.0)),
679 dec!(-23333.33333333333333333333334)
680 );
681 }
682
683 #[rstest]
684 fn test_position_lay_then_back() {
685 let mut position = BetPosition::default();
686 let bet1 = Bet::new(dec!(2.0), dec!(10_000), BetSide::Lay);
687 let bet2 = Bet::new(dec!(3.0), dec!(100_000), BetSide::Back);
688 position.add_bet(bet1);
689 position.add_bet(bet2);
690
691 assert_eq!(position.exposure, dec!(280_000.0));
692 assert_eq!(position.realized_pnl(), dec!(190_000));
693 assert_eq!(
694 position.unrealized_pnl(dec!(4.0)),
695 dec!(-23333.33333333333333333333334)
696 );
697 }
698
699 #[rstest]
700 fn test_position_flip() {
701 let mut position = BetPosition::default();
702 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);
705 position.add_bet(lay_bet);
706 assert_eq!(position.side(), Some(BetSide::Lay));
708 assert_eq!(position.exposure, dec!(-100.0));
709 }
710
711 #[rstest]
712 fn test_position_flat() {
713 let mut position = BetPosition::default();
714 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);
717 position.add_bet(lay_bet);
718 assert!(position.side().is_none());
719 assert_eq!(position.exposure, dec!(0.0));
720 }
721
722 #[rstest]
723 fn test_unrealized_pnl_negative() {
724 let mut position = BetPosition::default();
725 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back); position.add_bet(bet);
727 let unrealized_pnl = position.unrealized_pnl(dec!(2.5));
729 assert_eq!(unrealized_pnl, dec!(-20.0));
730 }
731
732 #[rstest]
733 fn test_total_pnl() {
734 let mut position = BetPosition::default();
735 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
736 position.add_bet(bet);
737 position.realized_pnl = dec!(10.0);
738 let total_pnl = position.total_pnl(dec!(2.5));
739 assert_eq!(total_pnl, dec!(-10.0));
741 }
742
743 #[rstest]
744 fn test_flattening_bet_back_profit() {
745 let mut position = BetPosition::default();
746 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
747 position.add_bet(bet);
748 let flattening_bet = position
749 .flattening_bet(dec!(1.6))
750 .expect("expected a flattening bet");
751 assert_eq!(flattening_bet.side, BetSide::Lay);
752 assert_eq!(flattening_bet.stake, dec_str("125"));
753 }
754
755 #[rstest]
756 fn test_flattening_bet_back_hack() {
757 let mut position = BetPosition::default();
758 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
759 position.add_bet(bet);
760 let flattening_bet = position
761 .flattening_bet(dec!(2.5))
762 .expect("expected a flattening bet");
763 assert_eq!(flattening_bet.side, BetSide::Lay);
764 assert_eq!(flattening_bet.stake, dec!(80.0));
766 }
767
768 #[rstest]
769 fn test_flattening_bet_lay() {
770 let mut position = BetPosition::default();
771 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
772 position.add_bet(bet);
773 let flattening_bet = position
774 .flattening_bet(dec!(1.5))
775 .expect("expected a flattening bet");
776 assert_eq!(flattening_bet.side, BetSide::Back);
777 assert_eq!(flattening_bet.stake.round_dp(8), dec_str("133.33333333"));
778 }
779
780 #[rstest]
781 fn test_realized_pnl_flattening() {
782 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();
785 position.add_bet(back);
786 position.add_bet(lay);
787 assert_eq!(position.realized_pnl, dec!(25.0));
789 }
790
791 #[rstest]
792 fn test_realized_pnl_single_side() {
793 let back = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back);
794 let mut position = BetPosition::default();
795 position.add_bet(back);
796 assert_eq!(position.realized_pnl, dec!(0.0));
798 }
799
800 #[rstest]
801 fn test_realized_pnl_open_position() {
802 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();
805 position.add_bet(back);
806 position.add_bet(lay);
807 assert_eq!(position.realized_pnl, dec!(20.0));
809 }
810
811 #[rstest]
812 fn test_realized_pnl_partial_close() {
813 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();
816 position.add_bet(back);
817 position.add_bet(lay);
818 assert_eq!(position.realized_pnl, dec!(22.0));
820 }
821
822 #[rstest]
823 fn test_realized_pnl_flipping() {
824 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();
827 position.add_bet(back);
828 position.add_bet(lay);
829 assert_eq!(position.realized_pnl, dec!(10.0));
831 }
832
833 #[rstest]
834 fn test_unrealized_pnl_positive() {
835 let back = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back); let mut position = BetPosition::default();
837 position.add_bet(back);
838 let unrealized_pnl = position.unrealized_pnl(dec!(4.0));
839 assert_eq!(unrealized_pnl, dec!(25.0));
841 }
842
843 #[rstest]
844 fn test_total_pnl_with_pnl() {
845 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();
848 position.add_bet(back);
849 position.add_bet(lay);
850 let realized_pnl = position.realized_pnl;
852 let unrealized_pnl = position.unrealized_pnl(dec!(4.0));
853 let total_pnl = position.total_pnl(dec!(4.0));
854 assert_eq!(realized_pnl, dec!(24.0));
855 assert_eq!(unrealized_pnl, dec!(1.0));
856 assert_eq!(total_pnl, dec!(25.0));
857 }
858
859 #[rstest]
860 fn test_open_position_realized_unrealized() {
861 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();
864 position.add_bet(back);
865 position.add_bet(lay);
866 let unrealized_pnl = position.unrealized_pnl(dec!(4.0));
867 assert_eq!(unrealized_pnl, dec!(5.0));
869 }
870
871 #[rstest]
872 fn test_unrealized_no_position() {
873 let back = Bet::new(dec!(5.0), dec!(100.0), BetSide::Lay);
874 let mut position = BetPosition::default();
875 position.add_bet(back);
876 let unrealized_pnl = position.unrealized_pnl(dec!(5.0));
877 assert_eq!(unrealized_pnl, dec!(0.0));
878 }
879
880 #[rstest]
881 fn test_calc_bets_pnl_single_back_bet() {
882 let bet = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back);
883 let pnl = calc_bets_pnl(&[bet]);
884 assert_eq!(pnl, dec!(400.0));
885 }
886
887 #[rstest]
888 fn test_calc_bets_pnl_single_lay_bet() {
889 let bet = Bet::new(dec!(4.0), dec!(100.0), BetSide::Lay);
890 let pnl = calc_bets_pnl(&[bet]);
891 assert_eq!(pnl, dec!(-300.0));
892 }
893
894 #[rstest]
895 fn test_calc_bets_pnl_multiple_bets() {
896 let back_bet = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back);
897 let lay_bet = Bet::new(dec!(4.0), dec!(100.0), BetSide::Lay);
898 let pnl = calc_bets_pnl(&[back_bet, lay_bet]);
899 let expected = dec!(400.0) + dec!(-300.0);
900 assert_eq!(pnl, expected);
901 }
902
903 #[rstest]
904 fn test_calc_bets_pnl_mixed_bets() {
905 let back_bet1 = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back);
906 let back_bet2 = Bet::new(dec!(2.0), dec!(50.0), BetSide::Back);
907 let lay_bet1 = Bet::new(dec!(3.0), dec!(75.0), BetSide::Lay);
908 let pnl = calc_bets_pnl(&[back_bet1, back_bet2, lay_bet1]);
909 let expected = dec!(400.0) + dec!(50.0) + dec!(-150.0);
910 assert_eq!(pnl, expected);
911 }
912
913 #[rstest]
914 fn test_calc_bets_pnl_no_bets() {
915 let bets: Vec<Bet> = vec![];
916 let pnl = calc_bets_pnl(&bets);
917 assert_eq!(pnl, dec!(0.0));
918 }
919
920 #[rstest]
921 fn test_calc_bets_pnl_zero_outcome() {
922 let back_bet = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back);
923 let lay_bet = Bet::new(dec!(5.0), dec!(100.0), BetSide::Lay);
924 let pnl = calc_bets_pnl(&[back_bet, lay_bet]);
925 assert_eq!(pnl, dec!(0.0));
926 }
927
928 #[rstest]
929 fn test_probability_to_bet_back_simple() {
930 let bet = probability_to_bet(dec!(0.50), dec!(50.0), OrderSideSpecified::Buy).unwrap();
932 let expected = Bet::new(dec!(2.0), dec!(25.0), BetSide::Back);
933 assert_eq!(bet, expected);
934 assert_eq!(bet.outcome_win_payoff(), dec!(25.0));
935 assert_eq!(bet.outcome_lose_payoff(), dec!(-25.0));
936 }
937
938 #[rstest]
939 fn test_probability_to_bet_back_high_prob() {
940 let bet = probability_to_bet(dec!(0.64), dec!(50.0), OrderSideSpecified::Buy).unwrap();
941 let expected = Bet::new(dec!(1.5625), dec!(32.0), BetSide::Back);
942 assert_eq!(bet, expected);
943 assert_eq!(bet.outcome_win_payoff(), dec!(18.0));
944 assert_eq!(bet.outcome_lose_payoff(), dec!(-32.0));
945 }
946
947 #[rstest]
948 fn test_probability_to_bet_back_low_prob() {
949 let bet = probability_to_bet(dec!(0.40), dec!(50.0), OrderSideSpecified::Buy).unwrap();
950 let expected = Bet::new(dec!(2.5), dec!(20.0), BetSide::Back);
951 assert_eq!(bet, expected);
952 assert_eq!(bet.outcome_win_payoff(), dec!(30.0));
953 assert_eq!(bet.outcome_lose_payoff(), dec!(-20.0));
954 }
955
956 #[rstest]
957 fn test_probability_to_bet_sell() {
958 let bet = probability_to_bet(dec!(0.80), dec!(50.0), OrderSideSpecified::Sell).unwrap();
959 let expected = Bet::new(dec_str("1.25"), dec_str("40"), BetSide::Lay);
960 assert_eq!(bet, expected);
961 assert_eq!(bet.outcome_win_payoff(), dec_str("-10"));
962 assert_eq!(bet.outcome_lose_payoff(), dec_str("40"));
963 }
964
965 #[rstest]
966 fn test_inverse_probability_to_bet() {
967 let original_bet =
969 probability_to_bet(dec!(0.80), dec!(100.0), OrderSideSpecified::Sell).unwrap();
970 let reverse_bet =
972 probability_to_bet(dec!(0.20), dec!(100.0), OrderSideSpecified::Buy).unwrap();
973 let inverse_bet =
974 inverse_probability_to_bet(dec!(0.80), dec!(100.0), OrderSideSpecified::Sell).unwrap();
975
976 assert_eq!(
977 original_bet.outcome_win_payoff(),
978 reverse_bet.outcome_lose_payoff(),
979 );
980 assert_eq!(
981 original_bet.outcome_win_payoff(),
982 inverse_bet.outcome_lose_payoff(),
983 );
984 assert_eq!(
985 original_bet.outcome_lose_payoff(),
986 reverse_bet.outcome_win_payoff(),
987 );
988 assert_eq!(
989 original_bet.outcome_lose_payoff(),
990 inverse_bet.outcome_win_payoff(),
991 );
992 }
993
994 #[rstest]
995 fn test_inverse_probability_to_bet_example2() {
996 let original_bet =
997 probability_to_bet(dec!(0.64), dec!(50.0), OrderSideSpecified::Sell).unwrap();
998 let inverse_bet =
999 inverse_probability_to_bet(dec!(0.64), dec!(50.0), OrderSideSpecified::Sell).unwrap();
1000
1001 assert_eq!(original_bet.stake, dec!(32.0));
1002 assert_eq!(original_bet.outcome_win_payoff(), dec!(-18.0));
1003 assert_eq!(original_bet.outcome_lose_payoff(), dec!(32.0));
1004
1005 assert_eq!(inverse_bet.stake, dec!(18.0));
1006 assert_eq!(inverse_bet.outcome_win_payoff(), dec!(32.0));
1007 assert_eq!(inverse_bet.outcome_lose_payoff(), dec!(-18.0));
1008 }
1009}