1use std::{
17 collections::{BTreeMap, HashMap},
18 fmt::Debug,
19 sync::Arc,
20};
21
22use nautilus_core::UnixNanos;
23use nautilus_model::{
24 accounts::Account,
25 identifiers::PositionId,
26 position::Position,
27 types::{Currency, Money},
28};
29use rust_decimal::Decimal;
30
31use crate::{
32 Returns,
33 statistic::PortfolioStatistic,
34 statistics::{
35 expectancy::Expectancy, long_ratio::LongRatio, loser_avg::AvgLoser, loser_max::MaxLoser,
36 loser_min::MinLoser, profit_factor::ProfitFactor, returns_avg::ReturnsAverage,
37 returns_avg_loss::ReturnsAverageLoss, returns_avg_win::ReturnsAverageWin,
38 returns_volatility::ReturnsVolatility, risk_return_ratio::RiskReturnRatio,
39 sharpe_ratio::SharpeRatio, sortino_ratio::SortinoRatio, win_rate::WinRate,
40 winner_avg::AvgWinner, winner_max::MaxWinner, winner_min::MinWinner,
41 },
42};
43
44pub type Statistic = Arc<dyn PortfolioStatistic<Item = f64> + Send + Sync>;
45
46#[repr(C)]
52#[derive(Debug)]
53#[cfg_attr(
54 feature = "python",
55 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.analysis")
56)]
57pub struct PortfolioAnalyzer {
58 pub statistics: HashMap<String, Statistic>,
59 pub account_balances_starting: HashMap<Currency, Money>,
60 pub account_balances: HashMap<Currency, Money>,
61 pub positions: Vec<Position>,
62 pub realized_pnls: HashMap<Currency, Vec<(PositionId, f64)>>,
63 pub returns: Returns,
64}
65
66impl Default for PortfolioAnalyzer {
67 fn default() -> Self {
69 let mut analyzer = Self::new();
70 analyzer.register_statistic(Arc::new(MaxWinner {}));
71 analyzer.register_statistic(Arc::new(AvgWinner {}));
72 analyzer.register_statistic(Arc::new(MinWinner {}));
73 analyzer.register_statistic(Arc::new(MinLoser {}));
74 analyzer.register_statistic(Arc::new(AvgLoser {}));
75 analyzer.register_statistic(Arc::new(MaxLoser {}));
76 analyzer.register_statistic(Arc::new(Expectancy {}));
77 analyzer.register_statistic(Arc::new(WinRate {}));
78 analyzer.register_statistic(Arc::new(ReturnsVolatility::new(None)));
79 analyzer.register_statistic(Arc::new(ReturnsAverage {}));
80 analyzer.register_statistic(Arc::new(ReturnsAverageLoss {}));
81 analyzer.register_statistic(Arc::new(ReturnsAverageWin {}));
82 analyzer.register_statistic(Arc::new(SharpeRatio::new(None)));
83 analyzer.register_statistic(Arc::new(SortinoRatio::new(None)));
84 analyzer.register_statistic(Arc::new(ProfitFactor {}));
85 analyzer.register_statistic(Arc::new(RiskReturnRatio {}));
86 analyzer.register_statistic(Arc::new(LongRatio::new(None)));
87 analyzer
88 }
89}
90
91impl PortfolioAnalyzer {
92 #[must_use]
96 pub fn new() -> Self {
97 Self {
98 statistics: HashMap::new(),
99 account_balances_starting: HashMap::new(),
100 account_balances: HashMap::new(),
101 positions: Vec::new(),
102 realized_pnls: HashMap::new(),
103 returns: BTreeMap::new(),
104 }
105 }
106
107 pub fn register_statistic(&mut self, statistic: Statistic) {
109 self.statistics.insert(statistic.name(), statistic);
110 }
111
112 pub fn deregister_statistic(&mut self, statistic: Statistic) {
114 self.statistics.remove(&statistic.name());
115 }
116
117 pub fn deregister_statistics(&mut self) {
119 self.statistics.clear();
120 }
121
122 pub fn reset(&mut self) {
124 self.account_balances_starting.clear();
125 self.account_balances.clear();
126 self.realized_pnls.clear();
127 self.returns.clear();
128 }
129
130 #[must_use]
132 pub fn currencies(&self) -> Vec<&Currency> {
133 self.account_balances.keys().collect()
134 }
135
136 #[must_use]
138 pub fn statistic(&self, name: &str) -> Option<&Statistic> {
139 self.statistics.get(name)
140 }
141
142 #[must_use]
144 pub const fn returns(&self) -> &Returns {
145 &self.returns
146 }
147
148 pub fn calculate_statistics(&mut self, account: &dyn Account, positions: &[Position]) {
150 self.account_balances_starting = account.starting_balances();
151 self.account_balances = account.balances_total();
152 self.realized_pnls.clear();
153 self.returns.clear();
154
155 self.add_positions(positions);
156 }
157
158 pub fn add_positions(&mut self, positions: &[Position]) {
160 self.positions.extend_from_slice(positions);
161 for position in positions {
162 if let Some(ref pnl) = position.realized_pnl {
163 self.add_trade(&position.id, pnl);
164 }
165 self.add_return(
166 position.ts_closed.unwrap_or(UnixNanos::default()),
167 position.realized_return,
168 );
169 }
170 }
171
172 pub fn add_trade(&mut self, position_id: &PositionId, pnl: &Money) {
174 let currency = pnl.currency;
175 let entry = self.realized_pnls.entry(currency).or_default();
176 entry.push((*position_id, pnl.as_f64()));
177 }
178
179 pub fn add_return(&mut self, timestamp: UnixNanos, value: f64) {
181 self.returns
182 .entry(timestamp)
183 .and_modify(|existing_value| *existing_value += value)
184 .or_insert(value);
185 }
186
187 #[must_use]
189 pub fn realized_pnls(&self, currency: Option<&Currency>) -> Option<Vec<(PositionId, f64)>> {
190 if self.realized_pnls.is_empty() {
191 return None;
192 }
193 let currency = currency.or_else(|| self.account_balances.keys().next())?;
194 self.realized_pnls.get(currency).cloned()
195 }
196
197 pub fn total_pnl(
206 &self,
207 currency: Option<&Currency>,
208 unrealized_pnl: Option<&Money>,
209 ) -> Result<f64, &'static str> {
210 if self.account_balances.is_empty() {
211 return Ok(0.0);
212 }
213
214 let currency = currency
215 .or_else(|| self.account_balances.keys().next())
216 .ok_or("Currency not specified for multi-currency portfolio")?;
217
218 if let Some(unrealized_pnl) = unrealized_pnl
219 && unrealized_pnl.currency != *currency
220 {
221 return Err("Unrealized PnL currency does not match specified currency");
222 }
223
224 let account_balance = self
225 .account_balances
226 .get(currency)
227 .ok_or("Specified currency not found in account balances")?;
228
229 let default_money = &Money::new(0.0, *currency);
230 let account_balance_starting = self
231 .account_balances_starting
232 .get(currency)
233 .unwrap_or(default_money);
234
235 let unrealized_pnl_f64 = unrealized_pnl.map_or(0.0, Money::as_f64);
236 Ok((account_balance.as_f64() - account_balance_starting.as_f64()) + unrealized_pnl_f64)
237 }
238
239 pub fn total_pnl_percentage(
248 &self,
249 currency: Option<&Currency>,
250 unrealized_pnl: Option<&Money>,
251 ) -> Result<f64, &'static str> {
252 if self.account_balances.is_empty() {
253 return Ok(0.0);
254 }
255
256 let currency = currency
257 .or_else(|| self.account_balances.keys().next())
258 .ok_or("Currency not specified for multi-currency portfolio")?;
259
260 if let Some(unrealized_pnl) = unrealized_pnl
261 && unrealized_pnl.currency != *currency
262 {
263 return Err("Unrealized PnL currency does not match specified currency");
264 }
265
266 let account_balance = self
267 .account_balances
268 .get(currency)
269 .ok_or("Specified currency not found in account balances")?;
270
271 let default_money = &Money::new(0.0, *currency);
272 let account_balance_starting = self
273 .account_balances_starting
274 .get(currency)
275 .unwrap_or(default_money);
276
277 if account_balance_starting.as_decimal() == Decimal::ZERO {
278 return Ok(0.0);
279 }
280
281 let unrealized_pnl_f64 = unrealized_pnl.map_or(0.0, Money::as_f64);
282 let current = account_balance.as_f64() + unrealized_pnl_f64;
283 let starting = account_balance_starting.as_f64();
284 let difference = current - starting;
285
286 Ok((difference / starting) * 100.0)
287 }
288
289 pub fn get_performance_stats_pnls(
299 &self,
300 currency: Option<&Currency>,
301 unrealized_pnl: Option<&Money>,
302 ) -> Result<HashMap<String, f64>, &'static str> {
303 let mut output = HashMap::new();
304
305 output.insert(
306 "PnL (total)".to_string(),
307 self.total_pnl(currency, unrealized_pnl)?,
308 );
309 output.insert(
310 "PnL% (total)".to_string(),
311 self.total_pnl_percentage(currency, unrealized_pnl)?,
312 );
313
314 if let Some(realized_pnls) = self.realized_pnls(currency) {
315 for (name, stat) in &self.statistics {
316 if let Some(value) = stat.calculate_from_realized_pnls(
317 &realized_pnls
318 .iter()
319 .map(|(_, pnl)| *pnl)
320 .collect::<Vec<f64>>(),
321 ) {
322 output.insert(name.clone(), value);
323 }
324 }
325 }
326
327 Ok(output)
328 }
329
330 #[must_use]
332 pub fn get_performance_stats_returns(&self) -> HashMap<String, f64> {
333 let mut output = HashMap::new();
334
335 for (name, stat) in &self.statistics {
336 if let Some(value) = stat.calculate_from_returns(&self.returns) {
337 output.insert(name.clone(), value);
338 }
339 }
340
341 output
342 }
343
344 #[must_use]
346 pub fn get_performance_stats_general(&self) -> HashMap<String, f64> {
347 let mut output = HashMap::new();
348
349 for (name, stat) in &self.statistics {
350 if let Some(value) = stat.calculate_from_positions(&self.positions) {
351 output.insert(name.clone(), value);
352 }
353 }
354
355 output
356 }
357
358 fn get_max_length_name(&self) -> usize {
360 self.statistics.keys().map(String::len).max().unwrap_or(0)
361 }
362
363 pub fn get_stats_pnls_formatted(
369 &self,
370 currency: Option<&Currency>,
371 unrealized_pnl: Option<&Money>,
372 ) -> Result<Vec<String>, String> {
373 let max_length = self.get_max_length_name();
374 let stats = self.get_performance_stats_pnls(currency, unrealized_pnl)?;
375
376 let mut output = Vec::new();
377 for (k, v) in stats {
378 let padding = if max_length > k.len() {
379 max_length - k.len() + 1
380 } else {
381 1
382 };
383 output.push(format!("{}: {}{:.2}", k, " ".repeat(padding), v));
384 }
385
386 Ok(output)
387 }
388
389 #[must_use]
391 pub fn get_stats_returns_formatted(&self) -> Vec<String> {
392 let max_length = self.get_max_length_name();
393 let stats = self.get_performance_stats_returns();
394
395 let mut output = Vec::new();
396 for (k, v) in stats {
397 let padding = max_length - k.len() + 1;
398 output.push(format!("{}: {}{:.2}", k, " ".repeat(padding), v));
399 }
400
401 output
402 }
403
404 #[must_use]
406 pub fn get_stats_general_formatted(&self) -> Vec<String> {
407 let max_length = self.get_max_length_name();
408 let stats = self.get_performance_stats_general();
409
410 let mut output = Vec::new();
411 for (k, v) in stats {
412 let padding = max_length - k.len() + 1;
413 output.push(format!("{}: {}{}", k, " ".repeat(padding), v));
414 }
415
416 output
417 }
418}
419
420#[cfg(test)]
425mod tests {
426 use std::sync::Arc;
427
428 use nautilus_core::approx_eq;
429 use nautilus_model::{
430 enums::{AccountType, LiquiditySide, OrderSide},
431 events::{AccountState, OrderFilled},
432 identifiers::{
433 AccountId, ClientOrderId,
434 stubs::{instrument_id_aud_usd_sim, strategy_id_ema_cross, trader_id},
435 },
436 instruments::InstrumentAny,
437 types::{AccountBalance, Money, Price, Quantity},
438 };
439 use rstest::rstest;
440
441 use super::*;
442
443 #[derive(Debug)]
445 struct MockStatistic {
446 name: String,
447 }
448
449 impl MockStatistic {
450 fn new(name: &str) -> Self {
451 Self {
452 name: name.to_string(),
453 }
454 }
455 }
456
457 impl PortfolioStatistic for MockStatistic {
458 type Item = f64;
459
460 fn name(&self) -> String {
461 self.name.clone()
462 }
463
464 fn calculate_from_realized_pnls(&self, pnls: &[f64]) -> Option<f64> {
465 Some(pnls.iter().sum())
466 }
467
468 fn calculate_from_returns(&self, returns: &Returns) -> Option<f64> {
469 Some(returns.values().sum())
470 }
471
472 fn calculate_from_positions(&self, positions: &[Position]) -> Option<f64> {
473 Some(positions.len() as f64)
474 }
475 }
476
477 fn create_mock_position(
478 id: String,
479 realized_pnl: f64,
480 realized_return: f64,
481 currency: Currency,
482 ) -> Position {
483 Position {
484 events: Vec::new(),
485 trader_id: trader_id(),
486 strategy_id: strategy_id_ema_cross(),
487 instrument_id: instrument_id_aud_usd_sim(),
488 id: PositionId::new(&id),
489 account_id: AccountId::new("test-account"),
490 opening_order_id: ClientOrderId::default(),
491 closing_order_id: None,
492 entry: OrderSide::NoOrderSide,
493 side: nautilus_model::enums::PositionSide::NoPositionSide,
494 signed_qty: 0.0,
495 quantity: Quantity::default(),
496 peak_qty: Quantity::default(),
497 price_precision: 2,
498 size_precision: 2,
499 multiplier: Quantity::default(),
500 is_inverse: false,
501 base_currency: None,
502 quote_currency: Currency::USD(),
503 settlement_currency: Currency::USD(),
504 ts_init: UnixNanos::default(),
505 ts_opened: UnixNanos::default(),
506 ts_last: UnixNanos::default(),
507 ts_closed: None,
508 duration_ns: 2,
509 avg_px_open: 0.0,
510 avg_px_close: None,
511 realized_return,
512 realized_pnl: Some(Money::new(realized_pnl, currency)),
513 trade_ids: Vec::new(),
514 buy_qty: Quantity::default(),
515 sell_qty: Quantity::default(),
516 commissions: HashMap::new(),
517 }
518 }
519
520 struct MockAccount {
521 starting_balances: HashMap<Currency, Money>,
522 current_balances: HashMap<Currency, Money>,
523 }
524
525 impl Account for MockAccount {
526 fn starting_balances(&self) -> HashMap<Currency, Money> {
527 self.starting_balances.clone()
528 }
529 fn balances_total(&self) -> HashMap<Currency, Money> {
530 self.current_balances.clone()
531 }
532 fn id(&self) -> AccountId {
533 todo!()
534 }
535 fn account_type(&self) -> AccountType {
536 todo!()
537 }
538 fn base_currency(&self) -> Option<Currency> {
539 todo!()
540 }
541 fn is_cash_account(&self) -> bool {
542 todo!()
543 }
544 fn is_margin_account(&self) -> bool {
545 todo!()
546 }
547 fn calculated_account_state(&self) -> bool {
548 todo!()
549 }
550 fn balance_total(&self, _: Option<Currency>) -> Option<Money> {
551 todo!()
552 }
553 fn balance_free(&self, _: Option<Currency>) -> Option<Money> {
554 todo!()
555 }
556 fn balances_free(&self) -> HashMap<Currency, Money> {
557 todo!()
558 }
559 fn balance_locked(&self, _: Option<Currency>) -> Option<Money> {
560 todo!()
561 }
562 fn balances_locked(&self) -> HashMap<Currency, Money> {
563 todo!()
564 }
565 fn last_event(&self) -> Option<AccountState> {
566 todo!()
567 }
568 fn events(&self) -> Vec<AccountState> {
569 todo!()
570 }
571 fn event_count(&self) -> usize {
572 todo!()
573 }
574 fn currencies(&self) -> Vec<Currency> {
575 todo!()
576 }
577 fn balances(&self) -> HashMap<Currency, AccountBalance> {
578 todo!()
579 }
580 fn apply(&mut self, _: AccountState) {
581 todo!()
582 }
583 fn calculate_balance_locked(
584 &mut self,
585 _: InstrumentAny,
586 _: OrderSide,
587 _: Quantity,
588 _: Price,
589 _: Option<bool>,
590 ) -> Result<Money, anyhow::Error> {
591 todo!()
592 }
593 fn calculate_pnls(
594 &self,
595 _: InstrumentAny,
596 _: OrderFilled,
597 _: Option<Position>,
598 ) -> Result<Vec<Money>, anyhow::Error> {
599 todo!()
600 }
601 fn calculate_commission(
602 &self,
603 _: InstrumentAny,
604 _: Quantity,
605 _: Price,
606 _: LiquiditySide,
607 _: Option<bool>,
608 ) -> Result<Money, anyhow::Error> {
609 todo!()
610 }
611
612 fn balance(&self, _: Option<Currency>) -> Option<&AccountBalance> {
613 todo!()
614 }
615
616 fn purge_account_events(&mut self, _: UnixNanos, _: u64) {
617 }
619 }
620
621 #[rstest]
622 fn test_register_and_deregister_statistics() {
623 let mut analyzer = PortfolioAnalyzer::new();
624 let stat: Arc<dyn PortfolioStatistic<Item = f64> + Send + Sync> =
625 Arc::new(MockStatistic::new("test_stat"));
626
627 analyzer.register_statistic(Arc::clone(&stat));
629 assert!(analyzer.statistic("test_stat").is_some());
630
631 analyzer.deregister_statistic(Arc::clone(&stat));
633 assert!(analyzer.statistic("test_stat").is_none());
634
635 let stat1: Arc<dyn PortfolioStatistic<Item = f64> + Send + Sync> =
637 Arc::new(MockStatistic::new("stat1"));
638 let stat2: Arc<dyn PortfolioStatistic<Item = f64> + Send + Sync> =
639 Arc::new(MockStatistic::new("stat2"));
640 analyzer.register_statistic(Arc::clone(&stat1));
641 analyzer.register_statistic(Arc::clone(&stat2));
642 analyzer.deregister_statistics();
643 assert!(analyzer.statistics.is_empty());
644 }
645
646 #[rstest]
647 fn test_calculate_total_pnl() {
648 let mut analyzer = PortfolioAnalyzer::new();
649 let currency = Currency::USD();
650
651 let mut starting_balances = HashMap::new();
653 starting_balances.insert(currency, Money::new(1000.0, currency));
654
655 let mut current_balances = HashMap::new();
656 current_balances.insert(currency, Money::new(1500.0, currency));
657
658 let account = MockAccount {
659 starting_balances,
660 current_balances,
661 };
662
663 analyzer.calculate_statistics(&account, &[]);
664
665 let result = analyzer.total_pnl(Some(¤cy), None).unwrap();
667 assert!(approx_eq!(f64, result, 500.0, epsilon = 1e-9));
668
669 let unrealized_pnl = Money::new(100.0, currency);
671 let result = analyzer
672 .total_pnl(Some(¤cy), Some(&unrealized_pnl))
673 .unwrap();
674 assert!(approx_eq!(f64, result, 600.0, epsilon = 1e-9));
675 }
676
677 #[rstest]
678 fn test_calculate_total_pnl_percentage() {
679 let mut analyzer = PortfolioAnalyzer::new();
680 let currency = Currency::USD();
681
682 let mut starting_balances = HashMap::new();
684 starting_balances.insert(currency, Money::new(1000.0, currency));
685
686 let mut current_balances = HashMap::new();
687 current_balances.insert(currency, Money::new(1500.0, currency));
688
689 let account = MockAccount {
690 starting_balances,
691 current_balances,
692 };
693
694 analyzer.calculate_statistics(&account, &[]);
695
696 let result = analyzer
698 .total_pnl_percentage(Some(¤cy), None)
699 .unwrap();
700 assert!(approx_eq!(f64, result, 50.0, epsilon = 1e-9)); let unrealized_pnl = Money::new(500.0, currency);
704 let result = analyzer
705 .total_pnl_percentage(Some(¤cy), Some(&unrealized_pnl))
706 .unwrap();
707 assert!(approx_eq!(f64, result, 100.0, epsilon = 1e-9)); }
709
710 #[rstest]
711 fn test_add_positions_and_returns() {
712 let mut analyzer = PortfolioAnalyzer::new();
713 let currency = Currency::USD();
714
715 let positions = vec![
716 create_mock_position("AUD/USD".to_owned(), 100.0, 0.1, currency),
717 create_mock_position("AUD/USD".to_owned(), 200.0, 0.2, currency),
718 ];
719
720 analyzer.add_positions(&positions);
721
722 let pnls = analyzer.realized_pnls(Some(¤cy)).unwrap();
724 assert_eq!(pnls.len(), 2);
725 assert!(approx_eq!(f64, pnls[0].1, 100.0, epsilon = 1e-9));
726 assert!(approx_eq!(f64, pnls[1].1, 200.0, epsilon = 1e-9));
727
728 let returns = analyzer.returns();
730 assert_eq!(returns.len(), 1);
731 assert!(approx_eq!(
732 f64,
733 *returns.values().next().unwrap(),
734 0.30000000000000004,
735 epsilon = 1e-9
736 ));
737 }
738
739 #[rstest]
740 fn test_performance_stats_calculation() {
741 let mut analyzer = PortfolioAnalyzer::new();
742 let currency = Currency::USD();
743 let stat: Arc<dyn PortfolioStatistic<Item = f64> + Send + Sync> =
744 Arc::new(MockStatistic::new("test_stat"));
745 analyzer.register_statistic(Arc::clone(&stat));
746
747 let positions = vec![
749 create_mock_position("AUD/USD".to_owned(), 100.0, 0.1, currency),
750 create_mock_position("AUD/USD".to_owned(), 200.0, 0.2, currency),
751 ];
752
753 let mut starting_balances = HashMap::new();
754 starting_balances.insert(currency, Money::new(1000.0, currency));
755
756 let mut current_balances = HashMap::new();
757 current_balances.insert(currency, Money::new(1500.0, currency));
758
759 let account = MockAccount {
760 starting_balances,
761 current_balances,
762 };
763
764 analyzer.calculate_statistics(&account, &positions);
765
766 let pnl_stats = analyzer
768 .get_performance_stats_pnls(Some(¤cy), None)
769 .unwrap();
770 assert!(pnl_stats.contains_key("PnL (total)"));
771 assert!(pnl_stats.contains_key("PnL% (total)"));
772 assert!(pnl_stats.contains_key("test_stat"));
773
774 let return_stats = analyzer.get_performance_stats_returns();
776 assert!(return_stats.contains_key("test_stat"));
777
778 let general_stats = analyzer.get_performance_stats_general();
780 assert!(general_stats.contains_key("test_stat"));
781 }
782
783 #[rstest]
784 fn test_formatted_output() {
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![
792 create_mock_position("AUD/USD".to_owned(), 100.0, 0.1, currency),
793 create_mock_position("AUD/USD".to_owned(), 200.0, 0.2, currency),
794 ];
795
796 let mut starting_balances = HashMap::new();
797 starting_balances.insert(currency, Money::new(1000.0, currency));
798
799 let mut current_balances = HashMap::new();
800 current_balances.insert(currency, Money::new(1500.0, currency));
801
802 let account = MockAccount {
803 starting_balances,
804 current_balances,
805 };
806
807 analyzer.calculate_statistics(&account, &positions);
808
809 let pnl_formatted = analyzer
811 .get_stats_pnls_formatted(Some(¤cy), None)
812 .unwrap();
813 assert!(!pnl_formatted.is_empty());
814 assert!(pnl_formatted.iter().all(|s| s.contains(':')));
815
816 let returns_formatted = analyzer.get_stats_returns_formatted();
817 assert!(!returns_formatted.is_empty());
818 assert!(returns_formatted.iter().all(|s| s.contains(':')));
819
820 let general_formatted = analyzer.get_stats_general_formatted();
821 assert!(!general_formatted.is_empty());
822 assert!(general_formatted.iter().all(|s| s.contains(':')));
823 }
824
825 #[rstest]
826 fn test_reset() {
827 let mut analyzer = PortfolioAnalyzer::new();
828 let currency = Currency::USD();
829
830 let positions = vec![create_mock_position(
831 "AUD/USD".to_owned(),
832 100.0,
833 0.1,
834 currency,
835 )];
836 let mut starting_balances = HashMap::new();
837 starting_balances.insert(currency, Money::new(1000.0, currency));
838 let mut current_balances = HashMap::new();
839 current_balances.insert(currency, Money::new(1500.0, currency));
840
841 let account = MockAccount {
842 starting_balances,
843 current_balances,
844 };
845
846 analyzer.calculate_statistics(&account, &positions);
847
848 analyzer.reset();
849
850 assert!(analyzer.account_balances_starting.is_empty());
851 assert!(analyzer.account_balances.is_empty());
852 assert!(analyzer.realized_pnls.is_empty());
853 assert!(analyzer.returns.is_empty());
854 }
855}