1use std::fmt::Display;
17
18use rust_decimal::Decimal;
19
20use crate::enums::{BetSide, OrderSideSpecified};
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
24#[cfg_attr(
25 feature = "python",
26 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
27)]
28pub struct Bet {
29 price: Decimal,
30 stake: Decimal,
31 side: BetSide,
32}
33
34impl Bet {
35 pub fn new(price: Decimal, stake: Decimal, side: BetSide) -> Self {
37 Self { price, stake, side }
38 }
39
40 #[must_use]
42 pub fn price(&self) -> Decimal {
43 self.price
44 }
45
46 #[must_use]
48 pub fn stake(&self) -> Decimal {
49 self.stake
50 }
51
52 #[must_use]
54 pub fn side(&self) -> BetSide {
55 self.side
56 }
57
58 pub fn from_stake_or_liability(price: Decimal, volume: Decimal, side: BetSide) -> Self {
63 match side {
64 BetSide::Back => Self::from_stake(price, volume, side),
65 BetSide::Lay => Self::from_liability(price, volume, side),
66 }
67 }
68
69 pub fn from_stake(price: Decimal, stake: Decimal, side: BetSide) -> Self {
71 Self::new(price, stake, side)
72 }
73
74 pub fn from_liability(price: Decimal, liability: Decimal, side: BetSide) -> Self {
80 if side != BetSide::Lay {
81 panic!("Liability-based betting is only applicable for Lay side.");
82 }
83 let adjusted_volume = liability / (price - Decimal::ONE);
84 Self::new(price, adjusted_volume, side)
85 }
86
87 pub fn exposure(&self) -> Decimal {
91 match self.side {
92 BetSide::Back => self.price * self.stake,
93 BetSide::Lay => -self.price * self.stake,
94 }
95 }
96
97 pub fn liability(&self) -> Decimal {
102 match self.side {
103 BetSide::Back => self.stake,
104 BetSide::Lay => self.stake * (self.price - Decimal::ONE),
105 }
106 }
107
108 pub fn profit(&self) -> Decimal {
112 match self.side {
113 BetSide::Back => self.stake * (self.price - Decimal::ONE),
114 BetSide::Lay => self.stake,
115 }
116 }
117
118 pub fn outcome_win_payoff(&self) -> Decimal {
122 match self.side {
123 BetSide::Back => self.profit(),
124 BetSide::Lay => -self.liability(),
125 }
126 }
127
128 pub fn outcome_lose_payoff(&self) -> Decimal {
132 match self.side {
133 BetSide::Back => -self.liability(),
134 BetSide::Lay => self.profit(),
135 }
136 }
137
138 pub fn hedging_stake(&self, price: Decimal) -> Decimal {
140 match self.side {
141 BetSide::Back => (self.price / price) * self.stake,
142 BetSide::Lay => self.stake / (price / self.price),
143 }
144 }
145
146 pub fn hedging_bet(&self, price: Decimal) -> Self {
148 Self::new(price, self.hedging_stake(price), self.side.opposite())
149 }
150}
151
152impl Display for Bet {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 write!(
156 f,
157 "Bet({:?} @ {:.2} x{:.2})",
158 self.side, self.price, self.stake
159 )
160 }
161}
162
163#[derive(Debug, Clone)]
165#[cfg_attr(
166 feature = "python",
167 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
168)]
169pub struct BetPosition {
170 price: Decimal,
171 exposure: Decimal,
172 realized_pnl: Decimal,
173 bets: Vec<Bet>,
174}
175
176impl Default for BetPosition {
177 fn default() -> Self {
178 Self {
179 price: Decimal::ZERO,
180 exposure: Decimal::ZERO,
181 realized_pnl: Decimal::ZERO,
182 bets: vec![],
183 }
184 }
185}
186
187impl BetPosition {
188 #[must_use]
190 pub fn price(&self) -> Decimal {
191 self.price
192 }
193
194 #[must_use]
196 pub fn exposure(&self) -> Decimal {
197 self.exposure
198 }
199
200 #[must_use]
202 pub fn realized_pnl(&self) -> Decimal {
203 self.realized_pnl
204 }
205
206 #[must_use]
208 pub fn bets(&self) -> &[Bet] {
209 &self.bets
210 }
211
212 pub fn side(&self) -> Option<BetSide> {
216 match self.exposure.cmp(&Decimal::ZERO) {
217 std::cmp::Ordering::Less => Some(BetSide::Lay),
218 std::cmp::Ordering::Greater => Some(BetSide::Back),
219 std::cmp::Ordering::Equal => None,
220 }
221 }
222
223 pub fn as_bet(&self) -> Option<Bet> {
225 self.side().map(|side| {
226 let stake = match side {
227 BetSide::Back => self.exposure / self.price,
228 BetSide::Lay => -self.exposure / self.price,
229 };
230 Bet::new(self.price, stake, side)
231 })
232 }
233
234 pub fn add_bet(&mut self, bet: Bet) {
236 match self.side() {
237 None => self.position_increase(&bet),
238 Some(current_side) => {
239 if current_side == bet.side {
240 self.position_increase(&bet);
241 } else {
242 self.position_decrease(&bet);
243 }
244 }
245 }
246 self.bets.push(bet);
247 }
248
249 pub fn position_increase(&mut self, bet: &Bet) {
251 if self.side().is_none() {
252 self.price = bet.price;
253 self.exposure = bet.exposure();
254 } else {
255 self.exposure += bet.exposure();
256 }
257 }
258
259 pub fn position_decrease(&mut self, bet: &Bet) {
264 let abs_bet_exposure = bet.exposure().abs();
265 let abs_self_exposure = self.exposure.abs();
266
267 match abs_bet_exposure.cmp(&abs_self_exposure) {
268 std::cmp::Ordering::Less => {
269 let decreasing_volume = abs_bet_exposure / self.price;
270 let current_side = self.side().unwrap();
271 let decreasing_bet = Bet::new(self.price, decreasing_volume, current_side);
272 let pnl = calc_bets_pnl(&[bet.clone(), decreasing_bet]);
273 self.realized_pnl += pnl;
274 self.exposure += bet.exposure();
275 }
276 std::cmp::Ordering::Greater => {
277 if let Some(self_bet) = self.as_bet() {
278 let pnl = calc_bets_pnl(&[bet.clone(), self_bet]);
279 self.realized_pnl += pnl;
280 }
281 self.price = bet.price;
282 self.exposure += bet.exposure();
283 }
284 std::cmp::Ordering::Equal => {
285 if let Some(self_bet) = self.as_bet() {
286 let pnl = calc_bets_pnl(&[bet.clone(), self_bet]);
287 self.realized_pnl += pnl;
288 }
289 self.price = Decimal::ZERO;
290 self.exposure = Decimal::ZERO;
291 }
292 }
293 }
294
295 pub fn unrealized_pnl(&self, price: Decimal) -> Decimal {
297 if self.side().is_none() {
298 Decimal::ZERO
299 } else if let Some(flattening_bet) = self.flattening_bet(price) {
300 if let Some(self_bet) = self.as_bet() {
301 calc_bets_pnl(&[flattening_bet, self_bet])
302 } else {
303 Decimal::ZERO
304 }
305 } else {
306 Decimal::ZERO
307 }
308 }
309
310 pub fn total_pnl(&self, price: Decimal) -> Decimal {
312 self.realized_pnl + self.unrealized_pnl(price)
313 }
314
315 pub fn flattening_bet(&self, price: Decimal) -> Option<Bet> {
317 self.side().map(|side| {
318 let stake = match side {
319 BetSide::Back => self.exposure / price,
320 BetSide::Lay => -self.exposure / price,
321 };
322 Bet::new(price, stake, side.opposite())
324 })
325 }
326
327 pub fn reset(&mut self) {
329 self.price = Decimal::ZERO;
330 self.exposure = Decimal::ZERO;
331 self.realized_pnl = Decimal::ZERO;
332 }
333}
334
335impl Display for BetPosition {
336 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
337 write!(
338 f,
339 "BetPosition(price: {:.2}, exposure: {:.2}, realized_pnl: {:.2})",
340 self.price, self.exposure, self.realized_pnl
341 )
342 }
343}
344
345pub fn calc_bets_pnl(bets: &[Bet]) -> Decimal {
347 bets.iter()
348 .fold(Decimal::ZERO, |acc, bet| acc + bet.outcome_win_payoff())
349}
350
351pub fn probability_to_bet(probability: Decimal, volume: Decimal, side: OrderSideSpecified) -> Bet {
355 let price = Decimal::ONE / probability;
356 match side {
357 OrderSideSpecified::Buy => Bet::new(price, volume / price, BetSide::Back),
358 OrderSideSpecified::Sell => Bet::new(price, volume / price, BetSide::Lay),
359 }
360}
361
362pub fn inverse_probability_to_bet(
366 probability: Decimal,
367 volume: Decimal,
368 side: OrderSideSpecified,
369) -> Bet {
370 let inverse_probability = Decimal::ONE - probability;
371 let inverse_side = match side {
372 OrderSideSpecified::Buy => OrderSideSpecified::Sell,
373 OrderSideSpecified::Sell => OrderSideSpecified::Buy,
374 };
375 probability_to_bet(inverse_probability, volume, inverse_side)
376}
377
378#[cfg(test)]
382mod tests {
383 use rstest::rstest;
384 use rust_decimal::Decimal;
385 use rust_decimal_macros::dec;
386
387 use super::*;
388
389 fn dec_str(s: &str) -> Decimal {
390 s.parse::<Decimal>().expect("Failed to parse Decimal")
391 }
392
393 #[rstest]
394 #[should_panic(expected = "Liability-based betting is only applicable for Lay side.")]
395 fn test_from_liability_panics_on_back_side() {
396 let _ = Bet::from_liability(dec!(2.0), dec!(100.0), BetSide::Back);
397 }
398
399 #[rstest]
400 fn test_bet_creation() {
401 let price = dec!(2.0);
402 let stake = dec!(100.0);
403 let side = BetSide::Back;
404 let bet = Bet::new(price, stake, side);
405 assert_eq!(bet.price, price);
406 assert_eq!(bet.stake, stake);
407 assert_eq!(bet.side, side);
408 }
409
410 #[rstest]
411 fn test_display_bet() {
412 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
413 let formatted = format!("{}", bet);
414 assert!(formatted.contains("Back"));
415 assert!(formatted.contains("2.00"));
416 assert!(formatted.contains("100.00"));
417 }
418
419 #[rstest]
420 fn test_bet_exposure_back() {
421 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
422 let exposure = bet.exposure();
423 assert_eq!(exposure, dec!(200.0));
424 }
425
426 #[rstest]
427 fn test_bet_exposure_lay() {
428 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
429 let exposure = bet.exposure();
430 assert_eq!(exposure, dec!(-200.0));
431 }
432
433 #[rstest]
434 fn test_bet_liability_back() {
435 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
436 let liability = bet.liability();
437 assert_eq!(liability, dec!(100.0));
438 }
439
440 #[rstest]
441 fn test_bet_liability_lay() {
442 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
443 let liability = bet.liability();
444 assert_eq!(liability, dec!(100.0));
445 }
446
447 #[rstest]
448 fn test_bet_profit_back() {
449 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
450 let profit = bet.profit();
451 assert_eq!(profit, dec!(100.0));
452 }
453
454 #[rstest]
455 fn test_bet_profit_lay() {
456 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
457 let profit = bet.profit();
458 assert_eq!(profit, dec!(100.0));
459 }
460
461 #[rstest]
462 fn test_outcome_win_payoff_back() {
463 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
464 let win_payoff = bet.outcome_win_payoff();
465 assert_eq!(win_payoff, dec!(100.0));
466 }
467
468 #[rstest]
469 fn test_outcome_win_payoff_lay() {
470 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
471 let win_payoff = bet.outcome_win_payoff();
472 assert_eq!(win_payoff, dec!(-100.0));
473 }
474
475 #[rstest]
476 fn test_outcome_lose_payoff_back() {
477 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
478 let lose_payoff = bet.outcome_lose_payoff();
479 assert_eq!(lose_payoff, dec!(-100.0));
480 }
481
482 #[rstest]
483 fn test_outcome_lose_payoff_lay() {
484 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
485 let lose_payoff = bet.outcome_lose_payoff();
486 assert_eq!(lose_payoff, dec!(100.0));
487 }
488
489 #[rstest]
490 fn test_hedging_stake_back() {
491 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
492 let hedging_stake = bet.hedging_stake(dec!(1.5));
493 assert_eq!(hedging_stake.round_dp(8), dec_str("133.33333333"));
495 }
496
497 #[rstest]
498 fn test_hedging_bet_lay() {
499 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
500 let hedge_bet = bet.hedging_bet(dec!(1.5));
501 assert_eq!(hedge_bet.side, BetSide::Back);
502 assert_eq!(hedge_bet.price, dec!(1.5));
503 assert_eq!(hedge_bet.stake.round_dp(8), dec_str("133.33333333"));
504 }
505
506 #[rstest]
507 fn test_bet_position_initialization() {
508 let position = BetPosition::default();
509 assert_eq!(position.price, dec!(0.0));
510 assert_eq!(position.exposure, dec!(0.0));
511 assert_eq!(position.realized_pnl, dec!(0.0));
512 }
513
514 #[rstest]
515 fn test_display_bet_position() {
516 let mut position = BetPosition::default();
517 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
518 position.add_bet(bet);
519 let formatted = format!("{}", position);
520
521 assert!(formatted.contains("price"));
522 assert!(formatted.contains("exposure"));
523 assert!(formatted.contains("realized_pnl"));
524 }
525
526 #[rstest]
527 fn test_as_bet() {
528 let mut position = BetPosition::default();
529 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
531 position.add_bet(bet);
532 let as_bet = position.as_bet().expect("Expected a bet representation");
533
534 assert_eq!(as_bet.price, position.price);
535 assert_eq!(as_bet.stake, position.exposure / position.price);
536 assert_eq!(as_bet.side, BetSide::Back);
537 }
538
539 #[rstest]
540 fn test_reset_position() {
541 let mut position = BetPosition::default();
542 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
543 position.add_bet(bet);
544 assert!(position.exposure != dec!(0.0));
545 position.reset();
546
547 assert_eq!(position.price, dec!(0.0));
549 assert_eq!(position.exposure, dec!(0.0));
550 assert_eq!(position.realized_pnl, dec!(0.0));
551 }
552
553 #[rstest]
554 fn test_bet_position_side_none() {
555 let position = BetPosition::default();
556 assert!(position.side().is_none());
557 }
558
559 #[rstest]
560 fn test_bet_position_side_back() {
561 let mut position = BetPosition::default();
562 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
563 position.add_bet(bet);
564 assert_eq!(position.side(), Some(BetSide::Back));
565 }
566
567 #[rstest]
568 fn test_bet_position_side_lay() {
569 let mut position = BetPosition::default();
570 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
571 position.add_bet(bet);
572 assert_eq!(position.side(), Some(BetSide::Lay));
573 }
574
575 #[rstest]
576 fn test_position_increase_back() {
577 let mut position = BetPosition::default();
578 let bet1 = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
579 let bet2 = Bet::new(dec!(2.0), dec!(50.0), BetSide::Back);
580 position.add_bet(bet1);
581 position.add_bet(bet2);
582 assert_eq!(position.exposure, dec!(300.0));
584 }
585
586 #[rstest]
587 fn test_position_increase_lay() {
588 let mut position = BetPosition::default();
589 let bet1 = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
590 let bet2 = Bet::new(dec!(2.0), dec!(50.0), BetSide::Lay);
591 position.add_bet(bet1);
592 position.add_bet(bet2);
593 assert_eq!(position.exposure, dec!(-300.0));
595 }
596
597 #[rstest]
598 fn test_position_flip() {
599 let mut position = BetPosition::default();
600 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);
603 position.add_bet(lay_bet);
604 assert_eq!(position.side(), Some(BetSide::Lay));
606 assert_eq!(position.exposure, dec!(-100.0));
607 }
608
609 #[rstest]
610 fn test_position_flat() {
611 let mut position = BetPosition::default();
612 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);
615 position.add_bet(lay_bet);
616 assert!(position.side().is_none());
617 assert_eq!(position.exposure, dec!(0.0));
618 }
619
620 #[rstest]
621 fn test_unrealized_pnl_negative() {
622 let mut position = BetPosition::default();
623 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back); position.add_bet(bet);
625 let unrealized_pnl = position.unrealized_pnl(dec!(2.5));
627 assert_eq!(unrealized_pnl, dec!(-20.0));
628 }
629
630 #[rstest]
631 fn test_total_pnl() {
632 let mut position = BetPosition::default();
633 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
634 position.add_bet(bet);
635 position.realized_pnl = dec!(10.0);
636 let total_pnl = position.total_pnl(dec!(2.5));
637 assert_eq!(total_pnl, dec!(-10.0));
639 }
640
641 #[rstest]
642 fn test_flattening_bet_back_profit() {
643 let mut position = BetPosition::default();
644 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
645 position.add_bet(bet);
646 let flattening_bet = position
647 .flattening_bet(dec!(1.6))
648 .expect("expected a flattening bet");
649 assert_eq!(flattening_bet.side, BetSide::Lay);
650 assert_eq!(flattening_bet.stake, dec_str("125"));
651 }
652
653 #[rstest]
654 fn test_flattening_bet_back_hack() {
655 let mut position = BetPosition::default();
656 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
657 position.add_bet(bet);
658 let flattening_bet = position
659 .flattening_bet(dec!(2.5))
660 .expect("expected a flattening bet");
661 assert_eq!(flattening_bet.side, BetSide::Lay);
662 assert_eq!(flattening_bet.stake, dec!(80.0));
664 }
665
666 #[rstest]
667 fn test_flattening_bet_lay() {
668 let mut position = BetPosition::default();
669 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
670 position.add_bet(bet);
671 let flattening_bet = position
672 .flattening_bet(dec!(1.5))
673 .expect("expected a flattening bet");
674 assert_eq!(flattening_bet.side, BetSide::Back);
675 assert_eq!(flattening_bet.stake.round_dp(8), dec_str("133.33333333"));
676 }
677
678 #[rstest]
679 fn test_realized_pnl_flattening() {
680 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();
683 position.add_bet(back);
684 position.add_bet(lay);
685 assert_eq!(position.realized_pnl, dec!(25.0));
687 }
688
689 #[rstest]
690 fn test_realized_pnl_single_side() {
691 let back = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back);
692 let mut position = BetPosition::default();
693 position.add_bet(back);
694 assert_eq!(position.realized_pnl, dec!(0.0));
696 }
697
698 #[rstest]
699 fn test_realized_pnl_open_position() {
700 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();
703 position.add_bet(back);
704 position.add_bet(lay);
705 assert_eq!(position.realized_pnl, dec!(20.0));
707 }
708
709 #[rstest]
710 fn test_realized_pnl_partial_close() {
711 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();
714 position.add_bet(back);
715 position.add_bet(lay);
716 assert_eq!(position.realized_pnl, dec!(22.0));
718 }
719
720 #[rstest]
721 fn test_realized_pnl_flipping() {
722 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();
725 position.add_bet(back);
726 position.add_bet(lay);
727 assert_eq!(position.realized_pnl, dec!(10.0));
729 }
730
731 #[rstest]
732 fn test_unrealized_pnl_positive() {
733 let back = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back); let mut position = BetPosition::default();
735 position.add_bet(back);
736 let unrealized_pnl = position.unrealized_pnl(dec!(4.0));
737 assert_eq!(unrealized_pnl, dec!(25.0));
739 }
740
741 #[rstest]
742 fn test_total_pnl_with_pnl() {
743 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();
746 position.add_bet(back);
747 position.add_bet(lay);
748 let realized_pnl = position.realized_pnl;
750 let unrealized_pnl = position.unrealized_pnl(dec!(4.0));
751 let total_pnl = position.total_pnl(dec!(4.0));
752 assert_eq!(realized_pnl, dec!(24.0));
753 assert_eq!(unrealized_pnl, dec!(1.0));
754 assert_eq!(total_pnl, dec!(25.0));
755 }
756
757 #[rstest]
758 fn test_open_position_realized_unrealized() {
759 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();
762 position.add_bet(back);
763 position.add_bet(lay);
764 let unrealized_pnl = position.unrealized_pnl(dec!(4.0));
765 assert_eq!(unrealized_pnl, dec!(5.0));
767 }
768
769 #[rstest]
770 fn test_unrealized_no_position() {
771 let back = Bet::new(dec!(5.0), dec!(100.0), BetSide::Lay);
772 let mut position = BetPosition::default();
773 position.add_bet(back);
774 let unrealized_pnl = position.unrealized_pnl(dec!(5.0));
775 assert_eq!(unrealized_pnl, dec!(0.0));
776 }
777
778 #[rstest]
779 fn test_calc_bets_pnl_single_back_bet() {
780 let bet = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back);
781 let pnl = calc_bets_pnl(&[bet]);
782 assert_eq!(pnl, dec!(400.0));
783 }
784
785 #[rstest]
786 fn test_calc_bets_pnl_single_lay_bet() {
787 let bet = Bet::new(dec!(4.0), dec!(100.0), BetSide::Lay);
788 let pnl = calc_bets_pnl(&[bet]);
789 assert_eq!(pnl, dec!(-300.0));
790 }
791
792 #[rstest]
793 fn test_calc_bets_pnl_multiple_bets() {
794 let back_bet = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back);
795 let lay_bet = Bet::new(dec!(4.0), dec!(100.0), BetSide::Lay);
796 let pnl = calc_bets_pnl(&[back_bet, lay_bet]);
797 let expected = dec!(400.0) + dec!(-300.0);
798 assert_eq!(pnl, expected);
799 }
800
801 #[rstest]
802 fn test_calc_bets_pnl_mixed_bets() {
803 let back_bet1 = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back);
804 let back_bet2 = Bet::new(dec!(2.0), dec!(50.0), BetSide::Back);
805 let lay_bet1 = Bet::new(dec!(3.0), dec!(75.0), BetSide::Lay);
806 let pnl = calc_bets_pnl(&[back_bet1, back_bet2, lay_bet1]);
807 let expected = dec!(400.0) + dec!(50.0) + dec!(-150.0);
808 assert_eq!(pnl, expected);
809 }
810
811 #[rstest]
812 fn test_calc_bets_pnl_no_bets() {
813 let bets: Vec<Bet> = vec![];
814 let pnl = calc_bets_pnl(&bets);
815 assert_eq!(pnl, dec!(0.0));
816 }
817
818 #[rstest]
819 fn test_calc_bets_pnl_zero_outcome() {
820 let back_bet = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back);
821 let lay_bet = Bet::new(dec!(5.0), dec!(100.0), BetSide::Lay);
822 let pnl = calc_bets_pnl(&[back_bet, lay_bet]);
823 assert_eq!(pnl, dec!(0.0));
824 }
825
826 #[rstest]
827 fn test_probability_to_bet_back_simple() {
828 let bet = probability_to_bet(dec!(0.50), dec!(50.0), OrderSideSpecified::Buy);
830 let expected = Bet::new(dec!(2.0), dec!(25.0), BetSide::Back);
831 assert_eq!(bet, expected);
832 assert_eq!(bet.outcome_win_payoff(), dec!(25.0));
833 assert_eq!(bet.outcome_lose_payoff(), dec!(-25.0));
834 }
835
836 #[rstest]
837 fn test_probability_to_bet_back_high_prob() {
838 let bet = probability_to_bet(dec!(0.64), dec!(50.0), OrderSideSpecified::Buy);
839 let expected = Bet::new(dec!(1.5625), dec!(32.0), BetSide::Back);
840 assert_eq!(bet, expected);
841 assert_eq!(bet.outcome_win_payoff(), dec!(18.0));
842 assert_eq!(bet.outcome_lose_payoff(), dec!(-32.0));
843 }
844
845 #[rstest]
846 fn test_probability_to_bet_back_low_prob() {
847 let bet = probability_to_bet(dec!(0.40), dec!(50.0), OrderSideSpecified::Buy);
848 let expected = Bet::new(dec!(2.5), dec!(20.0), BetSide::Back);
849 assert_eq!(bet, expected);
850 assert_eq!(bet.outcome_win_payoff(), dec!(30.0));
851 assert_eq!(bet.outcome_lose_payoff(), dec!(-20.0));
852 }
853
854 #[rstest]
855 fn test_probability_to_bet_sell() {
856 let bet = probability_to_bet(dec!(0.80), dec!(50.0), OrderSideSpecified::Sell);
857 let expected = Bet::new(dec_str("1.25"), dec_str("40"), BetSide::Lay);
858 assert_eq!(bet, expected);
859 assert_eq!(bet.outcome_win_payoff(), dec_str("-10"));
860 assert_eq!(bet.outcome_lose_payoff(), dec_str("40"));
861 }
862
863 #[rstest]
864 fn test_inverse_probability_to_bet() {
865 let original_bet = probability_to_bet(dec!(0.80), dec!(100.0), OrderSideSpecified::Sell);
867 let reverse_bet = probability_to_bet(dec!(0.20), dec!(100.0), OrderSideSpecified::Buy);
869 let inverse_bet =
870 inverse_probability_to_bet(dec!(0.80), dec!(100.0), OrderSideSpecified::Sell);
871
872 assert_eq!(
873 original_bet.outcome_win_payoff(),
874 reverse_bet.outcome_lose_payoff(),
875 );
876 assert_eq!(
877 original_bet.outcome_win_payoff(),
878 inverse_bet.outcome_lose_payoff(),
879 );
880 assert_eq!(
881 original_bet.outcome_lose_payoff(),
882 reverse_bet.outcome_win_payoff(),
883 );
884 assert_eq!(
885 original_bet.outcome_lose_payoff(),
886 inverse_bet.outcome_win_payoff(),
887 );
888 }
889
890 #[rstest]
891 fn test_inverse_probability_to_bet_example2() {
892 let original_bet = probability_to_bet(dec!(0.64), dec!(50.0), OrderSideSpecified::Sell);
893 let inverse_bet =
894 inverse_probability_to_bet(dec!(0.64), dec!(50.0), OrderSideSpecified::Sell);
895
896 assert_eq!(original_bet.stake, dec!(32.0));
897 assert_eq!(original_bet.outcome_win_payoff(), dec!(-18.0));
898 assert_eq!(original_bet.outcome_lose_payoff(), dec!(32.0));
899
900 assert_eq!(inverse_bet.stake, dec!(18.0));
901 assert_eq!(inverse_bet.outcome_win_payoff(), dec!(32.0));
902 assert_eq!(inverse_bet.outcome_lose_payoff(), dec!(-18.0));
903 }
904}