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