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 assert!(
83 side == BetSide::Lay,
84 "Liability-based betting is only applicable for Lay side."
85 );
86 let adjusted_volume = liability / (price - Decimal::ONE);
87 Self::new(price, adjusted_volume, side)
88 }
89
90 pub fn exposure(&self) -> Decimal {
94 match self.side {
95 BetSide::Back => self.price * self.stake,
96 BetSide::Lay => -self.price * self.stake,
97 }
98 }
99
100 pub fn liability(&self) -> Decimal {
105 match self.side {
106 BetSide::Back => self.stake,
107 BetSide::Lay => self.stake * (self.price - Decimal::ONE),
108 }
109 }
110
111 pub fn profit(&self) -> Decimal {
115 match self.side {
116 BetSide::Back => self.stake * (self.price - Decimal::ONE),
117 BetSide::Lay => self.stake,
118 }
119 }
120
121 pub fn outcome_win_payoff(&self) -> Decimal {
125 match self.side {
126 BetSide::Back => self.profit(),
127 BetSide::Lay => -self.liability(),
128 }
129 }
130
131 pub fn outcome_lose_payoff(&self) -> Decimal {
135 match self.side {
136 BetSide::Back => -self.liability(),
137 BetSide::Lay => self.profit(),
138 }
139 }
140
141 pub fn hedging_stake(&self, price: Decimal) -> Decimal {
143 match self.side {
144 BetSide::Back => (self.price / price) * self.stake,
145 BetSide::Lay => self.stake / (price / self.price),
146 }
147 }
148
149 #[must_use]
151 pub fn hedging_bet(&self, price: Decimal) -> Self {
152 Self::new(price, self.hedging_stake(price), self.side.opposite())
153 }
154}
155
156impl Display for Bet {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 write!(
160 f,
161 "Bet({:?} @ {:.2} x{:.2})",
162 self.side, self.price, self.stake
163 )
164 }
165}
166
167#[derive(Debug, Clone)]
169#[cfg_attr(
170 feature = "python",
171 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
172)]
173pub struct BetPosition {
174 price: Decimal,
175 exposure: Decimal,
176 realized_pnl: Decimal,
177 bets: Vec<Bet>,
178}
179
180impl Default for BetPosition {
181 fn default() -> Self {
182 Self {
183 price: Decimal::ZERO,
184 exposure: Decimal::ZERO,
185 realized_pnl: Decimal::ZERO,
186 bets: vec![],
187 }
188 }
189}
190
191impl BetPosition {
192 #[must_use]
194 pub fn price(&self) -> Decimal {
195 self.price
196 }
197
198 #[must_use]
200 pub fn exposure(&self) -> Decimal {
201 self.exposure
202 }
203
204 #[must_use]
206 pub fn realized_pnl(&self) -> Decimal {
207 self.realized_pnl
208 }
209
210 #[must_use]
212 pub fn bets(&self) -> &[Bet] {
213 &self.bets
214 }
215
216 pub fn side(&self) -> Option<BetSide> {
220 match self.exposure.cmp(&Decimal::ZERO) {
221 std::cmp::Ordering::Less => Some(BetSide::Lay),
222 std::cmp::Ordering::Greater => Some(BetSide::Back),
223 std::cmp::Ordering::Equal => None,
224 }
225 }
226
227 pub fn as_bet(&self) -> Option<Bet> {
229 self.side().map(|side| {
230 let stake = match side {
231 BetSide::Back => self.exposure / self.price,
232 BetSide::Lay => -self.exposure / self.price,
233 };
234 Bet::new(self.price, stake, side)
235 })
236 }
237
238 pub fn add_bet(&mut self, bet: Bet) {
240 match self.side() {
241 None => self.position_increase(&bet),
242 Some(current_side) => {
243 if current_side == bet.side {
244 self.position_increase(&bet);
245 } else {
246 self.position_decrease(&bet);
247 }
248 }
249 }
250 self.bets.push(bet);
251 }
252
253 pub fn position_increase(&mut self, bet: &Bet) {
255 if self.side().is_none() {
256 self.price = bet.price;
257 }
258 self.exposure += bet.exposure();
259 }
260
261 pub fn position_decrease(&mut self, bet: &Bet) {
267 let abs_bet_exposure = bet.exposure().abs();
268 let abs_self_exposure = self.exposure.abs();
269
270 match abs_bet_exposure.cmp(&abs_self_exposure) {
271 std::cmp::Ordering::Less => {
272 let decreasing_volume = abs_bet_exposure / self.price;
273 let current_side = self.side().unwrap();
274 let decreasing_bet = Bet::new(self.price, decreasing_volume, current_side);
275 let pnl = calc_bets_pnl(&[bet.clone(), decreasing_bet]);
276 self.realized_pnl += pnl;
277 self.exposure += bet.exposure();
278 }
279 std::cmp::Ordering::Greater => {
280 if let Some(self_bet) = self.as_bet() {
281 let pnl = calc_bets_pnl(&[bet.clone(), self_bet]);
282 self.realized_pnl += pnl;
283 }
284 self.price = bet.price;
285 self.exposure += bet.exposure();
286 }
287 std::cmp::Ordering::Equal => {
288 if let Some(self_bet) = self.as_bet() {
289 let pnl = calc_bets_pnl(&[bet.clone(), self_bet]);
290 self.realized_pnl += pnl;
291 }
292 self.price = Decimal::ZERO;
293 self.exposure = Decimal::ZERO;
294 }
295 }
296 }
297
298 pub fn unrealized_pnl(&self, price: Decimal) -> Decimal {
300 if self.side().is_none() {
301 Decimal::ZERO
302 } else if let Some(flattening_bet) = self.flattening_bet(price) {
303 if let Some(self_bet) = self.as_bet() {
304 calc_bets_pnl(&[flattening_bet, self_bet])
305 } else {
306 Decimal::ZERO
307 }
308 } else {
309 Decimal::ZERO
310 }
311 }
312
313 pub fn total_pnl(&self, price: Decimal) -> Decimal {
315 self.realized_pnl + self.unrealized_pnl(price)
316 }
317
318 pub fn flattening_bet(&self, price: Decimal) -> Option<Bet> {
320 self.side().map(|side| {
321 let stake = match side {
322 BetSide::Back => self.exposure / price,
323 BetSide::Lay => -self.exposure / price,
324 };
325 Bet::new(price, stake, side.opposite())
327 })
328 }
329
330 pub fn reset(&mut self) {
332 self.price = Decimal::ZERO;
333 self.exposure = Decimal::ZERO;
334 self.realized_pnl = Decimal::ZERO;
335 }
336}
337
338impl Display for BetPosition {
339 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
340 write!(
341 f,
342 "BetPosition(price: {:.2}, exposure: {:.2}, realized_pnl: {:.2})",
343 self.price, self.exposure, self.realized_pnl
344 )
345 }
346}
347
348pub fn calc_bets_pnl(bets: &[Bet]) -> Decimal {
350 bets.iter()
351 .fold(Decimal::ZERO, |acc, bet| acc + bet.outcome_win_payoff())
352}
353
354pub fn probability_to_bet(probability: Decimal, volume: Decimal, side: OrderSideSpecified) -> Bet {
358 let price = Decimal::ONE / probability;
359 match side {
360 OrderSideSpecified::Buy => Bet::new(price, volume / price, BetSide::Back),
361 OrderSideSpecified::Sell => Bet::new(price, volume / price, BetSide::Lay),
362 }
363}
364
365pub fn inverse_probability_to_bet(
369 probability: Decimal,
370 volume: Decimal,
371 side: OrderSideSpecified,
372) -> Bet {
373 let inverse_probability = Decimal::ONE - probability;
374 let inverse_side = match side {
375 OrderSideSpecified::Buy => OrderSideSpecified::Sell,
376 OrderSideSpecified::Sell => OrderSideSpecified::Buy,
377 };
378 probability_to_bet(inverse_probability, volume, inverse_side)
379}
380
381#[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_back_then_lay() {
599 let mut position = BetPosition::default();
600 let bet1 = Bet::new(dec!(3.0), dec!(100_000), BetSide::Back);
601 let bet2 = Bet::new(dec!(2.0), dec!(10_000), BetSide::Lay);
602 position.add_bet(bet1);
603 position.add_bet(bet2);
604
605 assert_eq!(position.exposure, dec!(280_000.0));
606 assert_eq!(position.realized_pnl(), dec!(3333.333333333333333333333333));
607 assert_eq!(
608 position.unrealized_pnl(dec!(4.0)),
609 dec!(-23333.33333333333333333333334)
610 );
611 }
612
613 #[rstest]
614 fn test_position_lay_then_back() {
615 let mut position = BetPosition::default();
616 let bet1 = Bet::new(dec!(2.0), dec!(10_000), BetSide::Lay);
617 let bet2 = Bet::new(dec!(3.0), dec!(100_000), BetSide::Back);
618 position.add_bet(bet1);
619 position.add_bet(bet2);
620
621 assert_eq!(position.exposure, dec!(280_000.0));
622 assert_eq!(position.realized_pnl(), dec!(190_000));
623 assert_eq!(
624 position.unrealized_pnl(dec!(4.0)),
625 dec!(-23333.33333333333333333333334)
626 );
627 }
628
629 #[rstest]
630 fn test_position_flip() {
631 let mut position = BetPosition::default();
632 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);
635 position.add_bet(lay_bet);
636 assert_eq!(position.side(), Some(BetSide::Lay));
638 assert_eq!(position.exposure, dec!(-100.0));
639 }
640
641 #[rstest]
642 fn test_position_flat() {
643 let mut position = BetPosition::default();
644 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);
647 position.add_bet(lay_bet);
648 assert!(position.side().is_none());
649 assert_eq!(position.exposure, dec!(0.0));
650 }
651
652 #[rstest]
653 fn test_unrealized_pnl_negative() {
654 let mut position = BetPosition::default();
655 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back); position.add_bet(bet);
657 let unrealized_pnl = position.unrealized_pnl(dec!(2.5));
659 assert_eq!(unrealized_pnl, dec!(-20.0));
660 }
661
662 #[rstest]
663 fn test_total_pnl() {
664 let mut position = BetPosition::default();
665 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
666 position.add_bet(bet);
667 position.realized_pnl = dec!(10.0);
668 let total_pnl = position.total_pnl(dec!(2.5));
669 assert_eq!(total_pnl, dec!(-10.0));
671 }
672
673 #[rstest]
674 fn test_flattening_bet_back_profit() {
675 let mut position = BetPosition::default();
676 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
677 position.add_bet(bet);
678 let flattening_bet = position
679 .flattening_bet(dec!(1.6))
680 .expect("expected a flattening bet");
681 assert_eq!(flattening_bet.side, BetSide::Lay);
682 assert_eq!(flattening_bet.stake, dec_str("125"));
683 }
684
685 #[rstest]
686 fn test_flattening_bet_back_hack() {
687 let mut position = BetPosition::default();
688 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Back);
689 position.add_bet(bet);
690 let flattening_bet = position
691 .flattening_bet(dec!(2.5))
692 .expect("expected a flattening bet");
693 assert_eq!(flattening_bet.side, BetSide::Lay);
694 assert_eq!(flattening_bet.stake, dec!(80.0));
696 }
697
698 #[rstest]
699 fn test_flattening_bet_lay() {
700 let mut position = BetPosition::default();
701 let bet = Bet::new(dec!(2.0), dec!(100.0), BetSide::Lay);
702 position.add_bet(bet);
703 let flattening_bet = position
704 .flattening_bet(dec!(1.5))
705 .expect("expected a flattening bet");
706 assert_eq!(flattening_bet.side, BetSide::Back);
707 assert_eq!(flattening_bet.stake.round_dp(8), dec_str("133.33333333"));
708 }
709
710 #[rstest]
711 fn test_realized_pnl_flattening() {
712 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();
715 position.add_bet(back);
716 position.add_bet(lay);
717 assert_eq!(position.realized_pnl, dec!(25.0));
719 }
720
721 #[rstest]
722 fn test_realized_pnl_single_side() {
723 let back = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back);
724 let mut position = BetPosition::default();
725 position.add_bet(back);
726 assert_eq!(position.realized_pnl, dec!(0.0));
728 }
729
730 #[rstest]
731 fn test_realized_pnl_open_position() {
732 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();
735 position.add_bet(back);
736 position.add_bet(lay);
737 assert_eq!(position.realized_pnl, dec!(20.0));
739 }
740
741 #[rstest]
742 fn test_realized_pnl_partial_close() {
743 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();
746 position.add_bet(back);
747 position.add_bet(lay);
748 assert_eq!(position.realized_pnl, dec!(22.0));
750 }
751
752 #[rstest]
753 fn test_realized_pnl_flipping() {
754 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();
757 position.add_bet(back);
758 position.add_bet(lay);
759 assert_eq!(position.realized_pnl, dec!(10.0));
761 }
762
763 #[rstest]
764 fn test_unrealized_pnl_positive() {
765 let back = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back); let mut position = BetPosition::default();
767 position.add_bet(back);
768 let unrealized_pnl = position.unrealized_pnl(dec!(4.0));
769 assert_eq!(unrealized_pnl, dec!(25.0));
771 }
772
773 #[rstest]
774 fn test_total_pnl_with_pnl() {
775 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();
778 position.add_bet(back);
779 position.add_bet(lay);
780 let realized_pnl = position.realized_pnl;
782 let unrealized_pnl = position.unrealized_pnl(dec!(4.0));
783 let total_pnl = position.total_pnl(dec!(4.0));
784 assert_eq!(realized_pnl, dec!(24.0));
785 assert_eq!(unrealized_pnl, dec!(1.0));
786 assert_eq!(total_pnl, dec!(25.0));
787 }
788
789 #[rstest]
790 fn test_open_position_realized_unrealized() {
791 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();
794 position.add_bet(back);
795 position.add_bet(lay);
796 let unrealized_pnl = position.unrealized_pnl(dec!(4.0));
797 assert_eq!(unrealized_pnl, dec!(5.0));
799 }
800
801 #[rstest]
802 fn test_unrealized_no_position() {
803 let back = Bet::new(dec!(5.0), dec!(100.0), BetSide::Lay);
804 let mut position = BetPosition::default();
805 position.add_bet(back);
806 let unrealized_pnl = position.unrealized_pnl(dec!(5.0));
807 assert_eq!(unrealized_pnl, dec!(0.0));
808 }
809
810 #[rstest]
811 fn test_calc_bets_pnl_single_back_bet() {
812 let bet = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back);
813 let pnl = calc_bets_pnl(&[bet]);
814 assert_eq!(pnl, dec!(400.0));
815 }
816
817 #[rstest]
818 fn test_calc_bets_pnl_single_lay_bet() {
819 let bet = Bet::new(dec!(4.0), dec!(100.0), BetSide::Lay);
820 let pnl = calc_bets_pnl(&[bet]);
821 assert_eq!(pnl, dec!(-300.0));
822 }
823
824 #[rstest]
825 fn test_calc_bets_pnl_multiple_bets() {
826 let back_bet = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back);
827 let lay_bet = Bet::new(dec!(4.0), dec!(100.0), BetSide::Lay);
828 let pnl = calc_bets_pnl(&[back_bet, lay_bet]);
829 let expected = dec!(400.0) + dec!(-300.0);
830 assert_eq!(pnl, expected);
831 }
832
833 #[rstest]
834 fn test_calc_bets_pnl_mixed_bets() {
835 let back_bet1 = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back);
836 let back_bet2 = Bet::new(dec!(2.0), dec!(50.0), BetSide::Back);
837 let lay_bet1 = Bet::new(dec!(3.0), dec!(75.0), BetSide::Lay);
838 let pnl = calc_bets_pnl(&[back_bet1, back_bet2, lay_bet1]);
839 let expected = dec!(400.0) + dec!(50.0) + dec!(-150.0);
840 assert_eq!(pnl, expected);
841 }
842
843 #[rstest]
844 fn test_calc_bets_pnl_no_bets() {
845 let bets: Vec<Bet> = vec![];
846 let pnl = calc_bets_pnl(&bets);
847 assert_eq!(pnl, dec!(0.0));
848 }
849
850 #[rstest]
851 fn test_calc_bets_pnl_zero_outcome() {
852 let back_bet = Bet::new(dec!(5.0), dec!(100.0), BetSide::Back);
853 let lay_bet = Bet::new(dec!(5.0), dec!(100.0), BetSide::Lay);
854 let pnl = calc_bets_pnl(&[back_bet, lay_bet]);
855 assert_eq!(pnl, dec!(0.0));
856 }
857
858 #[rstest]
859 fn test_probability_to_bet_back_simple() {
860 let bet = probability_to_bet(dec!(0.50), dec!(50.0), OrderSideSpecified::Buy);
862 let expected = Bet::new(dec!(2.0), dec!(25.0), BetSide::Back);
863 assert_eq!(bet, expected);
864 assert_eq!(bet.outcome_win_payoff(), dec!(25.0));
865 assert_eq!(bet.outcome_lose_payoff(), dec!(-25.0));
866 }
867
868 #[rstest]
869 fn test_probability_to_bet_back_high_prob() {
870 let bet = probability_to_bet(dec!(0.64), dec!(50.0), OrderSideSpecified::Buy);
871 let expected = Bet::new(dec!(1.5625), dec!(32.0), BetSide::Back);
872 assert_eq!(bet, expected);
873 assert_eq!(bet.outcome_win_payoff(), dec!(18.0));
874 assert_eq!(bet.outcome_lose_payoff(), dec!(-32.0));
875 }
876
877 #[rstest]
878 fn test_probability_to_bet_back_low_prob() {
879 let bet = probability_to_bet(dec!(0.40), dec!(50.0), OrderSideSpecified::Buy);
880 let expected = Bet::new(dec!(2.5), dec!(20.0), BetSide::Back);
881 assert_eq!(bet, expected);
882 assert_eq!(bet.outcome_win_payoff(), dec!(30.0));
883 assert_eq!(bet.outcome_lose_payoff(), dec!(-20.0));
884 }
885
886 #[rstest]
887 fn test_probability_to_bet_sell() {
888 let bet = probability_to_bet(dec!(0.80), dec!(50.0), OrderSideSpecified::Sell);
889 let expected = Bet::new(dec_str("1.25"), dec_str("40"), BetSide::Lay);
890 assert_eq!(bet, expected);
891 assert_eq!(bet.outcome_win_payoff(), dec_str("-10"));
892 assert_eq!(bet.outcome_lose_payoff(), dec_str("40"));
893 }
894
895 #[rstest]
896 fn test_inverse_probability_to_bet() {
897 let original_bet = probability_to_bet(dec!(0.80), dec!(100.0), OrderSideSpecified::Sell);
899 let reverse_bet = probability_to_bet(dec!(0.20), dec!(100.0), OrderSideSpecified::Buy);
901 let inverse_bet =
902 inverse_probability_to_bet(dec!(0.80), dec!(100.0), OrderSideSpecified::Sell);
903
904 assert_eq!(
905 original_bet.outcome_win_payoff(),
906 reverse_bet.outcome_lose_payoff(),
907 );
908 assert_eq!(
909 original_bet.outcome_win_payoff(),
910 inverse_bet.outcome_lose_payoff(),
911 );
912 assert_eq!(
913 original_bet.outcome_lose_payoff(),
914 reverse_bet.outcome_win_payoff(),
915 );
916 assert_eq!(
917 original_bet.outcome_lose_payoff(),
918 inverse_bet.outcome_win_payoff(),
919 );
920 }
921
922 #[rstest]
923 fn test_inverse_probability_to_bet_example2() {
924 let original_bet = probability_to_bet(dec!(0.64), dec!(50.0), OrderSideSpecified::Sell);
925 let inverse_bet =
926 inverse_probability_to_bet(dec!(0.64), dec!(50.0), OrderSideSpecified::Sell);
927
928 assert_eq!(original_bet.stake, dec!(32.0));
929 assert_eq!(original_bet.outcome_win_payoff(), dec!(-18.0));
930 assert_eq!(original_bet.outcome_lose_payoff(), dec!(32.0));
931
932 assert_eq!(inverse_bet.stake, dec!(18.0));
933 assert_eq!(inverse_bet.outcome_win_payoff(), dec!(32.0));
934 assert_eq!(inverse_bet.outcome_lose_payoff(), dec!(-18.0));
935 }
936}