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