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