1use std::{collections::BTreeMap, fmt::Debug, sync::Arc};
17
18use ahash::AHashMap;
19use nautilus_core::UnixNanos;
20use nautilus_model::{
21 accounts::Account,
22 identifiers::PositionId,
23 position::Position,
24 types::{Currency, Money},
25};
26use rust_decimal::Decimal;
27
28use crate::{
29 Returns,
30 statistic::PortfolioStatistic,
31 statistics::{
32 expectancy::Expectancy, long_ratio::LongRatio, loser_avg::AvgLoser, loser_max::MaxLoser,
33 loser_min::MinLoser, profit_factor::ProfitFactor, returns_avg::ReturnsAverage,
34 returns_avg_loss::ReturnsAverageLoss, returns_avg_win::ReturnsAverageWin,
35 returns_volatility::ReturnsVolatility, risk_return_ratio::RiskReturnRatio,
36 sharpe_ratio::SharpeRatio, sortino_ratio::SortinoRatio, win_rate::WinRate,
37 winner_avg::AvgWinner, winner_max::MaxWinner, winner_min::MinWinner,
38 },
39};
40
41pub type Statistic = Arc<dyn PortfolioStatistic<Item = f64> + Send + Sync>;
42
43#[repr(C)]
49#[derive(Debug)]
50#[cfg_attr(
51 feature = "python",
52 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.analysis")
53)]
54pub struct PortfolioAnalyzer {
55 pub statistics: AHashMap<String, Statistic>,
56 pub account_balances_starting: AHashMap<Currency, Money>,
57 pub account_balances: AHashMap<Currency, Money>,
58 pub positions: Vec<Position>,
59 pub realized_pnls: AHashMap<Currency, Vec<(PositionId, f64)>>,
60 pub returns: Returns,
61}
62
63impl Default for PortfolioAnalyzer {
64 fn default() -> Self {
66 let mut analyzer = Self::new();
67 analyzer.register_statistic(Arc::new(MaxWinner {}));
68 analyzer.register_statistic(Arc::new(AvgWinner {}));
69 analyzer.register_statistic(Arc::new(MinWinner {}));
70 analyzer.register_statistic(Arc::new(MinLoser {}));
71 analyzer.register_statistic(Arc::new(AvgLoser {}));
72 analyzer.register_statistic(Arc::new(MaxLoser {}));
73 analyzer.register_statistic(Arc::new(Expectancy {}));
74 analyzer.register_statistic(Arc::new(WinRate {}));
75 analyzer.register_statistic(Arc::new(ReturnsVolatility::new(None)));
76 analyzer.register_statistic(Arc::new(ReturnsAverage {}));
77 analyzer.register_statistic(Arc::new(ReturnsAverageLoss {}));
78 analyzer.register_statistic(Arc::new(ReturnsAverageWin {}));
79 analyzer.register_statistic(Arc::new(SharpeRatio::new(None)));
80 analyzer.register_statistic(Arc::new(SortinoRatio::new(None)));
81 analyzer.register_statistic(Arc::new(ProfitFactor {}));
82 analyzer.register_statistic(Arc::new(RiskReturnRatio {}));
83 analyzer.register_statistic(Arc::new(LongRatio::new(None)));
84 analyzer
85 }
86}
87
88impl PortfolioAnalyzer {
89 #[must_use]
93 pub fn new() -> Self {
94 Self {
95 statistics: AHashMap::new(),
96 account_balances_starting: AHashMap::new(),
97 account_balances: AHashMap::new(),
98 positions: Vec::new(),
99 realized_pnls: AHashMap::new(),
100 returns: BTreeMap::new(),
101 }
102 }
103
104 pub fn register_statistic(&mut self, statistic: Statistic) {
106 self.statistics.insert(statistic.name(), statistic);
107 }
108
109 pub fn deregister_statistic(&mut self, statistic: Statistic) {
111 self.statistics.remove(&statistic.name());
112 }
113
114 pub fn deregister_statistics(&mut self) {
116 self.statistics.clear();
117 }
118
119 pub fn reset(&mut self) {
121 self.account_balances_starting.clear();
122 self.account_balances.clear();
123 self.positions.clear();
124 self.realized_pnls.clear();
125 self.returns.clear();
126 }
127
128 #[must_use]
130 pub fn currencies(&self) -> Vec<&Currency> {
131 self.account_balances.keys().collect()
132 }
133
134 #[must_use]
136 pub fn statistic(&self, name: &str) -> Option<&Statistic> {
137 self.statistics.get(name)
138 }
139
140 #[must_use]
142 pub const fn returns(&self) -> &Returns {
143 &self.returns
144 }
145
146 pub fn calculate_statistics(&mut self, account: &dyn Account, positions: &[Position]) {
151 self.account_balances_starting = account.starting_balances().into_iter().collect();
152 self.account_balances = account.balances_total().into_iter().collect();
153 self.positions.clear();
154 self.realized_pnls.clear();
155 self.returns.clear();
156
157 self.add_positions(positions);
158 }
159
160 pub fn add_positions(&mut self, positions: &[Position]) {
162 self.positions.extend_from_slice(positions);
163 for position in positions {
164 if let Some(ref pnl) = position.realized_pnl {
165 self.add_trade(&position.id, pnl);
166 }
167 self.add_return(
168 position.ts_closed.unwrap_or(UnixNanos::default()),
169 position.realized_return,
170 );
171 }
172 }
173
174 pub fn add_trade(&mut self, position_id: &PositionId, pnl: &Money) {
176 let currency = pnl.currency;
177 let entry = self.realized_pnls.entry(currency).or_default();
178 entry.push((*position_id, pnl.as_f64()));
179 }
180
181 pub fn add_return(&mut self, timestamp: UnixNanos, value: f64) {
183 self.returns
184 .entry(timestamp)
185 .and_modify(|existing_value| *existing_value += value)
186 .or_insert(value);
187 }
188
189 #[must_use]
194 pub fn realized_pnls(&self, currency: Option<&Currency>) -> Option<Vec<(PositionId, f64)>> {
195 if self.realized_pnls.is_empty() {
196 return None;
197 }
198
199 let currency = match currency {
201 Some(c) => c,
202 None if self.account_balances.len() == 1 => self.account_balances.keys().next()?,
203 None => return None,
204 };
205
206 self.realized_pnls.get(currency).cloned()
207 }
208
209 pub fn total_pnl(
223 &self,
224 currency: Option<&Currency>,
225 unrealized_pnl: Option<&Money>,
226 ) -> Result<f64, &'static str> {
227 if self.account_balances.is_empty() {
228 return Ok(0.0);
229 }
230
231 let currency = match currency {
233 Some(c) => c,
234 None if self.account_balances.len() == 1 => {
235 self.account_balances.keys().next().expect("len is 1")
237 }
238 None => return Err("Currency must be specified for multi-currency portfolio"),
239 };
240
241 if let Some(unrealized_pnl) = unrealized_pnl
242 && unrealized_pnl.currency != *currency
243 {
244 return Err("Unrealized PnL currency does not match specified currency");
245 }
246
247 let account_balance = self
248 .account_balances
249 .get(currency)
250 .ok_or("Specified currency not found in account balances")?;
251
252 let default_money = &Money::new(0.0, *currency);
253 let account_balance_starting = self
254 .account_balances_starting
255 .get(currency)
256 .unwrap_or(default_money);
257
258 let unrealized_pnl_f64 = unrealized_pnl.map_or(0.0, Money::as_f64);
259 Ok((account_balance.as_f64() - account_balance_starting.as_f64()) + unrealized_pnl_f64)
260 }
261
262 pub fn total_pnl_percentage(
276 &self,
277 currency: Option<&Currency>,
278 unrealized_pnl: Option<&Money>,
279 ) -> Result<f64, &'static str> {
280 if self.account_balances.is_empty() {
281 return Ok(0.0);
282 }
283
284 let currency = match currency {
286 Some(c) => c,
287 None if self.account_balances.len() == 1 => {
288 self.account_balances.keys().next().expect("len is 1")
290 }
291 None => return Err("Currency must be specified for multi-currency portfolio"),
292 };
293
294 if let Some(unrealized_pnl) = unrealized_pnl
295 && unrealized_pnl.currency != *currency
296 {
297 return Err("Unrealized PnL currency does not match specified currency");
298 }
299
300 let account_balance = self
301 .account_balances
302 .get(currency)
303 .ok_or("Specified currency not found in account balances")?;
304
305 let default_money = &Money::new(0.0, *currency);
306 let account_balance_starting = self
307 .account_balances_starting
308 .get(currency)
309 .unwrap_or(default_money);
310
311 if account_balance_starting.as_decimal() == Decimal::ZERO {
312 return Ok(0.0);
313 }
314
315 let unrealized_pnl_f64 = unrealized_pnl.map_or(0.0, Money::as_f64);
316 let current = account_balance.as_f64() + unrealized_pnl_f64;
317 let starting = account_balance_starting.as_f64();
318 let difference = current - starting;
319
320 Ok((difference / starting) * 100.0)
321 }
322
323 pub fn get_performance_stats_pnls(
333 &self,
334 currency: Option<&Currency>,
335 unrealized_pnl: Option<&Money>,
336 ) -> Result<AHashMap<String, f64>, &'static str> {
337 let mut output = AHashMap::new();
338
339 output.insert(
340 "PnL (total)".to_string(),
341 self.total_pnl(currency, unrealized_pnl)?,
342 );
343 output.insert(
344 "PnL% (total)".to_string(),
345 self.total_pnl_percentage(currency, unrealized_pnl)?,
346 );
347
348 if let Some(realized_pnls) = self.realized_pnls(currency) {
349 for (name, stat) in &self.statistics {
350 if let Some(value) = stat.calculate_from_realized_pnls(
351 &realized_pnls
352 .iter()
353 .map(|(_, pnl)| *pnl)
354 .collect::<Vec<f64>>(),
355 ) {
356 output.insert(name.clone(), value);
357 }
358 }
359 }
360
361 Ok(output)
362 }
363
364 #[must_use]
366 pub fn get_performance_stats_returns(&self) -> AHashMap<String, f64> {
367 let mut output = AHashMap::new();
368
369 for (name, stat) in &self.statistics {
370 if let Some(value) = stat.calculate_from_returns(&self.returns) {
371 output.insert(name.clone(), value);
372 }
373 }
374
375 output
376 }
377
378 #[must_use]
380 pub fn get_performance_stats_general(&self) -> AHashMap<String, f64> {
381 let mut output = AHashMap::new();
382
383 for (name, stat) in &self.statistics {
384 if let Some(value) = stat.calculate_from_positions(&self.positions) {
385 output.insert(name.clone(), value);
386 }
387 }
388
389 output
390 }
391
392 fn get_max_length_name(&self) -> usize {
394 self.statistics.keys().map(String::len).max().unwrap_or(0)
395 }
396
397 pub fn get_stats_pnls_formatted(
403 &self,
404 currency: Option<&Currency>,
405 unrealized_pnl: Option<&Money>,
406 ) -> Result<Vec<String>, String> {
407 let max_length = self.get_max_length_name();
408 let stats = self.get_performance_stats_pnls(currency, unrealized_pnl)?;
409
410 let mut entries: Vec<_> = stats.into_iter().collect();
411 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
412
413 let mut output = Vec::new();
414 for (k, v) in entries {
415 let padding = if max_length > k.len() {
416 max_length - k.len() + 1
417 } else {
418 1
419 };
420 output.push(format!("{}: {}{:.2}", k, " ".repeat(padding), v));
421 }
422
423 Ok(output)
424 }
425
426 #[must_use]
428 pub fn get_stats_returns_formatted(&self) -> Vec<String> {
429 let max_length = self.get_max_length_name();
430 let stats = self.get_performance_stats_returns();
431
432 let mut entries: Vec<_> = stats.into_iter().collect();
433 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
434
435 let mut output = Vec::new();
436 for (k, v) in entries {
437 let padding = max_length - k.len() + 1;
438 output.push(format!("{}: {}{:.2}", k, " ".repeat(padding), v));
439 }
440
441 output
442 }
443
444 #[must_use]
446 pub fn get_stats_general_formatted(&self) -> Vec<String> {
447 let max_length = self.get_max_length_name();
448 let stats = self.get_performance_stats_general();
449
450 let mut entries: Vec<_> = stats.into_iter().collect();
451 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
452
453 let mut output = Vec::new();
454 for (k, v) in entries {
455 let padding = max_length - k.len() + 1;
456 output.push(format!("{}: {}{}", k, " ".repeat(padding), v));
457 }
458
459 output
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use std::sync::Arc;
466
467 use ahash::AHashMap;
468 use nautilus_core::approx_eq;
469 use nautilus_model::{
470 enums::{AccountType, InstrumentClass, LiquiditySide, OrderSide, PositionSide},
471 events::{AccountState, OrderFilled},
472 identifiers::{
473 AccountId, ClientOrderId,
474 stubs::{instrument_id_aud_usd_sim, strategy_id_ema_cross, trader_id},
475 },
476 instruments::InstrumentAny,
477 stubs::TestDefault,
478 types::{AccountBalance, Money, Price, Quantity},
479 };
480 use rstest::rstest;
481
482 use super::*;
483
484 #[derive(Debug)]
486 struct MockStatistic {
487 name: String,
488 }
489
490 impl MockStatistic {
491 fn new(name: &str) -> Self {
492 Self {
493 name: name.to_string(),
494 }
495 }
496 }
497
498 impl PortfolioStatistic for MockStatistic {
499 type Item = f64;
500
501 fn name(&self) -> String {
502 self.name.clone()
503 }
504
505 fn calculate_from_realized_pnls(&self, pnls: &[f64]) -> Option<f64> {
506 Some(pnls.iter().sum())
507 }
508
509 fn calculate_from_returns(&self, returns: &Returns) -> Option<f64> {
510 Some(returns.values().sum())
511 }
512
513 fn calculate_from_positions(&self, positions: &[Position]) -> Option<f64> {
514 Some(positions.len() as f64)
515 }
516 }
517
518 fn create_mock_position(
519 id: String,
520 realized_pnl: f64,
521 realized_return: f64,
522 currency: Currency,
523 ) -> Position {
524 Position {
525 events: Vec::new(),
526 adjustments: Vec::new(),
527 trader_id: trader_id(),
528 strategy_id: strategy_id_ema_cross(),
529 instrument_id: instrument_id_aud_usd_sim(),
530 id: PositionId::new(&id),
531 account_id: AccountId::new("test-account"),
532 opening_order_id: ClientOrderId::test_default(),
533 closing_order_id: None,
534 entry: OrderSide::NoOrderSide,
535 side: PositionSide::NoPositionSide,
536 signed_qty: 0.0,
537 quantity: Quantity::default(),
538 peak_qty: Quantity::default(),
539 price_precision: 2,
540 size_precision: 2,
541 multiplier: Quantity::default(),
542 is_inverse: false,
543 is_currency_pair: true,
544 instrument_class: InstrumentClass::Spot,
545 base_currency: None,
546 quote_currency: Currency::USD(),
547 settlement_currency: Currency::USD(),
548 ts_init: UnixNanos::default(),
549 ts_opened: UnixNanos::default(),
550 ts_last: UnixNanos::default(),
551 ts_closed: None,
552 duration_ns: 2,
553 avg_px_open: 0.0,
554 avg_px_close: None,
555 realized_return,
556 realized_pnl: Some(Money::new(realized_pnl, currency)),
557 trade_ids: Vec::new(),
558 buy_qty: Quantity::default(),
559 sell_qty: Quantity::default(),
560 commissions: AHashMap::new(),
561 }
562 }
563
564 struct MockAccount {
565 starting_balances: AHashMap<Currency, Money>,
566 current_balances: AHashMap<Currency, Money>,
567 }
568
569 impl Account for MockAccount {
570 fn starting_balances(&self) -> AHashMap<Currency, Money> {
571 self.starting_balances.clone()
572 }
573 fn balances_total(&self) -> AHashMap<Currency, Money> {
574 self.current_balances.clone()
575 }
576 fn id(&self) -> AccountId {
577 todo!()
578 }
579 fn account_type(&self) -> AccountType {
580 todo!()
581 }
582 fn base_currency(&self) -> Option<Currency> {
583 todo!()
584 }
585 fn is_cash_account(&self) -> bool {
586 todo!()
587 }
588 fn is_margin_account(&self) -> bool {
589 todo!()
590 }
591 fn calculated_account_state(&self) -> bool {
592 todo!()
593 }
594 fn balance_total(&self, _: Option<Currency>) -> Option<Money> {
595 todo!()
596 }
597 fn balance_free(&self, _: Option<Currency>) -> Option<Money> {
598 todo!()
599 }
600 fn balances_free(&self) -> AHashMap<Currency, Money> {
601 todo!()
602 }
603 fn balance_locked(&self, _: Option<Currency>) -> Option<Money> {
604 todo!()
605 }
606 fn balances_locked(&self) -> AHashMap<Currency, Money> {
607 todo!()
608 }
609 fn last_event(&self) -> Option<AccountState> {
610 todo!()
611 }
612 fn events(&self) -> Vec<AccountState> {
613 todo!()
614 }
615 fn event_count(&self) -> usize {
616 todo!()
617 }
618 fn currencies(&self) -> Vec<Currency> {
619 todo!()
620 }
621 fn balances(&self) -> AHashMap<Currency, AccountBalance> {
622 todo!()
623 }
624 fn apply(&mut self, _: AccountState) -> anyhow::Result<()> {
625 todo!()
626 }
627 fn calculate_balance_locked(
628 &mut self,
629 _: InstrumentAny,
630 _: OrderSide,
631 _: Quantity,
632 _: Price,
633 _: Option<bool>,
634 ) -> Result<Money, anyhow::Error> {
635 todo!()
636 }
637 fn calculate_pnls(
638 &self,
639 _: InstrumentAny,
640 _: OrderFilled,
641 _: Option<Position>,
642 ) -> Result<Vec<Money>, anyhow::Error> {
643 todo!()
644 }
645 fn calculate_commission(
646 &self,
647 _: InstrumentAny,
648 _: Quantity,
649 _: Price,
650 _: LiquiditySide,
651 _: Option<bool>,
652 ) -> Result<Money, anyhow::Error> {
653 todo!()
654 }
655
656 fn balance(&self, _: Option<Currency>) -> Option<&AccountBalance> {
657 todo!()
658 }
659
660 fn purge_account_events(&mut self, _: UnixNanos, _: u64) {
661 }
663 }
664
665 #[rstest]
666 fn test_register_and_deregister_statistics() {
667 let mut analyzer = PortfolioAnalyzer::new();
668 let stat: Arc<dyn PortfolioStatistic<Item = f64> + Send + Sync> =
669 Arc::new(MockStatistic::new("test_stat"));
670
671 analyzer.register_statistic(Arc::clone(&stat));
673 assert!(analyzer.statistic("test_stat").is_some());
674
675 analyzer.deregister_statistic(Arc::clone(&stat));
677 assert!(analyzer.statistic("test_stat").is_none());
678
679 let stat1: Arc<dyn PortfolioStatistic<Item = f64> + Send + Sync> =
681 Arc::new(MockStatistic::new("stat1"));
682 let stat2: Arc<dyn PortfolioStatistic<Item = f64> + Send + Sync> =
683 Arc::new(MockStatistic::new("stat2"));
684 analyzer.register_statistic(Arc::clone(&stat1));
685 analyzer.register_statistic(Arc::clone(&stat2));
686 analyzer.deregister_statistics();
687 assert!(analyzer.statistics.is_empty());
688 }
689
690 #[rstest]
691 fn test_calculate_total_pnl() {
692 let mut analyzer = PortfolioAnalyzer::new();
693 let currency = Currency::USD();
694
695 let mut starting_balances = AHashMap::new();
697 starting_balances.insert(currency, Money::new(1000.0, currency));
698
699 let mut current_balances = AHashMap::new();
700 current_balances.insert(currency, Money::new(1500.0, currency));
701
702 let account = MockAccount {
703 starting_balances,
704 current_balances,
705 };
706
707 analyzer.calculate_statistics(&account, &[]);
708
709 let result = analyzer.total_pnl(Some(¤cy), None).unwrap();
711 assert!(approx_eq!(f64, result, 500.0, epsilon = 1e-9));
712
713 let unrealized_pnl = Money::new(100.0, currency);
715 let result = analyzer
716 .total_pnl(Some(¤cy), Some(&unrealized_pnl))
717 .unwrap();
718 assert!(approx_eq!(f64, result, 600.0, epsilon = 1e-9));
719 }
720
721 #[rstest]
722 fn test_calculate_total_pnl_percentage() {
723 let mut analyzer = PortfolioAnalyzer::new();
724 let currency = Currency::USD();
725
726 let mut starting_balances = AHashMap::new();
728 starting_balances.insert(currency, Money::new(1000.0, currency));
729
730 let mut current_balances = AHashMap::new();
731 current_balances.insert(currency, Money::new(1500.0, currency));
732
733 let account = MockAccount {
734 starting_balances,
735 current_balances,
736 };
737
738 analyzer.calculate_statistics(&account, &[]);
739
740 let result = analyzer
742 .total_pnl_percentage(Some(¤cy), None)
743 .unwrap();
744 assert!(approx_eq!(f64, result, 50.0, epsilon = 1e-9)); let unrealized_pnl = Money::new(500.0, currency);
748 let result = analyzer
749 .total_pnl_percentage(Some(¤cy), Some(&unrealized_pnl))
750 .unwrap();
751 assert!(approx_eq!(f64, result, 100.0, epsilon = 1e-9)); }
753
754 #[rstest]
755 fn test_add_positions_and_returns() {
756 let mut analyzer = PortfolioAnalyzer::new();
757 let currency = Currency::USD();
758
759 let positions = vec![
760 create_mock_position("AUD/USD".to_owned(), 100.0, 0.1, currency),
761 create_mock_position("AUD/USD".to_owned(), 200.0, 0.2, currency),
762 ];
763
764 analyzer.add_positions(&positions);
765
766 let pnls = analyzer.realized_pnls(Some(¤cy)).unwrap();
768 assert_eq!(pnls.len(), 2);
769 assert!(approx_eq!(f64, pnls[0].1, 100.0, epsilon = 1e-9));
770 assert!(approx_eq!(f64, pnls[1].1, 200.0, epsilon = 1e-9));
771
772 let returns = analyzer.returns();
774 assert_eq!(returns.len(), 1);
775 assert!(approx_eq!(
776 f64,
777 *returns.values().next().unwrap(),
778 0.30000000000000004,
779 epsilon = 1e-9
780 ));
781 }
782
783 #[rstest]
784 fn test_performance_stats_calculation() {
785 let mut analyzer = PortfolioAnalyzer::new();
786 let currency = Currency::USD();
787 let stat: Arc<dyn PortfolioStatistic<Item = f64> + Send + Sync> =
788 Arc::new(MockStatistic::new("test_stat"));
789 analyzer.register_statistic(Arc::clone(&stat));
790
791 let positions = vec![
793 create_mock_position("AUD/USD".to_owned(), 100.0, 0.1, currency),
794 create_mock_position("AUD/USD".to_owned(), 200.0, 0.2, currency),
795 ];
796
797 let mut starting_balances = AHashMap::new();
798 starting_balances.insert(currency, Money::new(1000.0, currency));
799
800 let mut current_balances = AHashMap::new();
801 current_balances.insert(currency, Money::new(1500.0, currency));
802
803 let account = MockAccount {
804 starting_balances,
805 current_balances,
806 };
807
808 analyzer.calculate_statistics(&account, &positions);
809
810 let pnl_stats = analyzer
812 .get_performance_stats_pnls(Some(¤cy), None)
813 .unwrap();
814 assert!(pnl_stats.contains_key("PnL (total)"));
815 assert!(pnl_stats.contains_key("PnL% (total)"));
816 assert!(pnl_stats.contains_key("test_stat"));
817
818 let return_stats = analyzer.get_performance_stats_returns();
820 assert!(return_stats.contains_key("test_stat"));
821
822 let general_stats = analyzer.get_performance_stats_general();
824 assert!(general_stats.contains_key("test_stat"));
825 }
826
827 #[rstest]
828 fn test_formatted_output() {
829 let mut analyzer = PortfolioAnalyzer::new();
830 let currency = Currency::USD();
831 let stat: Arc<dyn PortfolioStatistic<Item = f64> + Send + Sync> =
832 Arc::new(MockStatistic::new("test_stat"));
833 analyzer.register_statistic(Arc::clone(&stat));
834
835 let positions = vec![
836 create_mock_position("AUD/USD".to_owned(), 100.0, 0.1, currency),
837 create_mock_position("AUD/USD".to_owned(), 200.0, 0.2, currency),
838 ];
839
840 let mut starting_balances = AHashMap::new();
841 starting_balances.insert(currency, Money::new(1000.0, currency));
842
843 let mut current_balances = AHashMap::new();
844 current_balances.insert(currency, Money::new(1500.0, currency));
845
846 let account = MockAccount {
847 starting_balances,
848 current_balances,
849 };
850
851 analyzer.calculate_statistics(&account, &positions);
852
853 let pnl_formatted = analyzer
855 .get_stats_pnls_formatted(Some(¤cy), None)
856 .unwrap();
857 assert!(!pnl_formatted.is_empty());
858 assert!(pnl_formatted.iter().all(|s| s.contains(':')));
859
860 let returns_formatted = analyzer.get_stats_returns_formatted();
861 assert!(!returns_formatted.is_empty());
862 assert!(returns_formatted.iter().all(|s| s.contains(':')));
863
864 let general_formatted = analyzer.get_stats_general_formatted();
865 assert!(!general_formatted.is_empty());
866 assert!(general_formatted.iter().all(|s| s.contains(':')));
867 }
868
869 #[rstest]
870 fn test_reset() {
871 let mut analyzer = PortfolioAnalyzer::new();
872 let currency = Currency::USD();
873
874 let positions = vec![create_mock_position(
875 "AUD/USD".to_owned(),
876 100.0,
877 0.1,
878 currency,
879 )];
880 let mut starting_balances = AHashMap::new();
881 starting_balances.insert(currency, Money::new(1000.0, currency));
882 let mut current_balances = AHashMap::new();
883 current_balances.insert(currency, Money::new(1500.0, currency));
884
885 let account = MockAccount {
886 starting_balances,
887 current_balances,
888 };
889
890 analyzer.calculate_statistics(&account, &positions);
891
892 analyzer.reset();
893
894 assert!(analyzer.account_balances_starting.is_empty());
895 assert!(analyzer.account_balances.is_empty());
896 assert!(analyzer.positions.is_empty());
897 assert!(analyzer.realized_pnls.is_empty());
898 assert!(analyzer.returns.is_empty());
899 }
900
901 #[rstest]
902 fn test_calculate_statistics_clears_previous_positions() {
903 let mut analyzer = PortfolioAnalyzer::new();
904 let currency = Currency::USD();
905
906 let positions1 = vec![create_mock_position(
907 "pos1".to_owned(),
908 100.0,
909 0.1,
910 currency,
911 )];
912 let positions2 = vec![create_mock_position(
913 "pos2".to_owned(),
914 200.0,
915 0.2,
916 currency,
917 )];
918
919 let mut starting_balances = AHashMap::new();
920 starting_balances.insert(currency, Money::new(1000.0, currency));
921 let mut current_balances = AHashMap::new();
922 current_balances.insert(currency, Money::new(1500.0, currency));
923
924 let account = MockAccount {
925 starting_balances,
926 current_balances,
927 };
928
929 analyzer.calculate_statistics(&account, &positions1);
931 assert_eq!(analyzer.positions.len(), 1);
932
933 analyzer.calculate_statistics(&account, &positions2);
935 assert_eq!(analyzer.positions.len(), 1);
936 }
937}