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