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_core::approx_eq;
424 use nautilus_model::{
425 enums::{AccountType, LiquiditySide, OrderSide},
426 events::{AccountState, OrderFilled},
427 identifiers::{
428 AccountId, ClientOrderId,
429 stubs::{instrument_id_aud_usd_sim, strategy_id_ema_cross, trader_id},
430 },
431 instruments::InstrumentAny,
432 types::{AccountBalance, Money, Price, Quantity},
433 };
434 use rstest::rstest;
435
436 use super::*;
437
438 #[derive(Debug)]
440 struct MockStatistic {
441 name: String,
442 }
443
444 impl MockStatistic {
445 fn new(name: &str) -> Self {
446 Self {
447 name: name.to_string(),
448 }
449 }
450 }
451
452 impl PortfolioStatistic for MockStatistic {
453 type Item = f64;
454
455 fn name(&self) -> String {
456 self.name.clone()
457 }
458
459 fn calculate_from_realized_pnls(&self, pnls: &[f64]) -> Option<f64> {
460 Some(pnls.iter().sum())
461 }
462
463 fn calculate_from_returns(&self, returns: &Returns) -> Option<f64> {
464 Some(returns.values().sum())
465 }
466
467 fn calculate_from_positions(&self, positions: &[Position]) -> Option<f64> {
468 Some(positions.len() as f64)
469 }
470 }
471
472 fn create_mock_position(
473 id: String,
474 realized_pnl: f64,
475 realized_return: f64,
476 currency: Currency,
477 ) -> Position {
478 Position {
479 events: Vec::new(),
480 trader_id: trader_id(),
481 strategy_id: strategy_id_ema_cross(),
482 instrument_id: instrument_id_aud_usd_sim(),
483 id: PositionId::new(&id),
484 account_id: AccountId::new("test-account"),
485 opening_order_id: ClientOrderId::default(),
486 closing_order_id: None,
487 entry: OrderSide::NoOrderSide,
488 side: nautilus_model::enums::PositionSide::NoPositionSide,
489 signed_qty: 0.0,
490 quantity: Quantity::default(),
491 peak_qty: Quantity::default(),
492 price_precision: 2,
493 size_precision: 2,
494 multiplier: Quantity::default(),
495 is_inverse: false,
496 base_currency: None,
497 quote_currency: Currency::USD(),
498 settlement_currency: Currency::USD(),
499 ts_init: UnixNanos::default(),
500 ts_opened: UnixNanos::default(),
501 ts_last: UnixNanos::default(),
502 ts_closed: None,
503 duration_ns: 2,
504 avg_px_open: 0.0,
505 avg_px_close: None,
506 realized_return,
507 realized_pnl: Some(Money::new(realized_pnl, currency)),
508 trade_ids: Vec::new(),
509 buy_qty: Quantity::default(),
510 sell_qty: Quantity::default(),
511 commissions: HashMap::new(),
512 }
513 }
514
515 struct MockAccount {
516 starting_balances: HashMap<Currency, Money>,
517 current_balances: HashMap<Currency, Money>,
518 }
519
520 impl Account for MockAccount {
521 fn starting_balances(&self) -> HashMap<Currency, Money> {
522 self.starting_balances.clone()
523 }
524 fn balances_total(&self) -> HashMap<Currency, Money> {
525 self.current_balances.clone()
526 }
527 fn id(&self) -> AccountId {
528 todo!()
529 }
530 fn account_type(&self) -> AccountType {
531 todo!()
532 }
533 fn base_currency(&self) -> Option<Currency> {
534 todo!()
535 }
536 fn is_cash_account(&self) -> bool {
537 todo!()
538 }
539 fn is_margin_account(&self) -> bool {
540 todo!()
541 }
542 fn calculated_account_state(&self) -> bool {
543 todo!()
544 }
545 fn balance_total(&self, _: Option<Currency>) -> Option<Money> {
546 todo!()
547 }
548 fn balance_free(&self, _: Option<Currency>) -> Option<Money> {
549 todo!()
550 }
551 fn balances_free(&self) -> HashMap<Currency, Money> {
552 todo!()
553 }
554 fn balance_locked(&self, _: Option<Currency>) -> Option<Money> {
555 todo!()
556 }
557 fn balances_locked(&self) -> HashMap<Currency, Money> {
558 todo!()
559 }
560 fn last_event(&self) -> Option<AccountState> {
561 todo!()
562 }
563 fn events(&self) -> Vec<AccountState> {
564 todo!()
565 }
566 fn event_count(&self) -> usize {
567 todo!()
568 }
569 fn currencies(&self) -> Vec<Currency> {
570 todo!()
571 }
572 fn balances(&self) -> HashMap<Currency, AccountBalance> {
573 todo!()
574 }
575 fn apply(&mut self, _: AccountState) {
576 todo!()
577 }
578 fn calculate_balance_locked(
579 &mut self,
580 _: InstrumentAny,
581 _: OrderSide,
582 _: Quantity,
583 _: Price,
584 _: Option<bool>,
585 ) -> Result<Money, anyhow::Error> {
586 todo!()
587 }
588 fn calculate_pnls(
589 &self,
590 _: InstrumentAny,
591 _: OrderFilled,
592 _: Option<Position>,
593 ) -> Result<Vec<Money>, anyhow::Error> {
594 todo!()
595 }
596 fn calculate_commission(
597 &self,
598 _: InstrumentAny,
599 _: Quantity,
600 _: Price,
601 _: LiquiditySide,
602 _: Option<bool>,
603 ) -> Result<Money, anyhow::Error> {
604 todo!()
605 }
606
607 fn balance(&self, _: Option<Currency>) -> Option<&AccountBalance> {
608 todo!()
609 }
610
611 fn purge_account_events(&mut self, _: UnixNanos, _: u64) {
612 }
614 }
615
616 #[rstest]
617 fn test_register_and_deregister_statistics() {
618 let mut analyzer = PortfolioAnalyzer::new();
619 let stat: Arc<dyn PortfolioStatistic<Item = f64> + Send + Sync> =
620 Arc::new(MockStatistic::new("test_stat"));
621
622 analyzer.register_statistic(Arc::clone(&stat));
624 assert!(analyzer.statistic("test_stat").is_some());
625
626 analyzer.deregister_statistic(Arc::clone(&stat));
628 assert!(analyzer.statistic("test_stat").is_none());
629
630 let stat1: Arc<dyn PortfolioStatistic<Item = f64> + Send + Sync> =
632 Arc::new(MockStatistic::new("stat1"));
633 let stat2: Arc<dyn PortfolioStatistic<Item = f64> + Send + Sync> =
634 Arc::new(MockStatistic::new("stat2"));
635 analyzer.register_statistic(Arc::clone(&stat1));
636 analyzer.register_statistic(Arc::clone(&stat2));
637 analyzer.deregister_statistics();
638 assert!(analyzer.statistics.is_empty());
639 }
640
641 #[rstest]
642 fn test_calculate_total_pnl() {
643 let mut analyzer = PortfolioAnalyzer::new();
644 let currency = Currency::USD();
645
646 let mut starting_balances = HashMap::new();
648 starting_balances.insert(currency, Money::new(1000.0, currency));
649
650 let mut current_balances = HashMap::new();
651 current_balances.insert(currency, Money::new(1500.0, currency));
652
653 let account = MockAccount {
654 starting_balances,
655 current_balances,
656 };
657
658 analyzer.calculate_statistics(&account, &[]);
659
660 let result = analyzer.total_pnl(Some(¤cy), None).unwrap();
662 assert!(approx_eq!(f64, result, 500.0, epsilon = 1e-9));
663
664 let unrealized_pnl = Money::new(100.0, currency);
666 let result = analyzer
667 .total_pnl(Some(¤cy), Some(&unrealized_pnl))
668 .unwrap();
669 assert!(approx_eq!(f64, result, 600.0, epsilon = 1e-9));
670 }
671
672 #[rstest]
673 fn test_calculate_total_pnl_percentage() {
674 let mut analyzer = PortfolioAnalyzer::new();
675 let currency = Currency::USD();
676
677 let mut starting_balances = HashMap::new();
679 starting_balances.insert(currency, Money::new(1000.0, currency));
680
681 let mut current_balances = HashMap::new();
682 current_balances.insert(currency, Money::new(1500.0, currency));
683
684 let account = MockAccount {
685 starting_balances,
686 current_balances,
687 };
688
689 analyzer.calculate_statistics(&account, &[]);
690
691 let result = analyzer
693 .total_pnl_percentage(Some(¤cy), None)
694 .unwrap();
695 assert!(approx_eq!(f64, result, 50.0, epsilon = 1e-9)); let unrealized_pnl = Money::new(500.0, currency);
699 let result = analyzer
700 .total_pnl_percentage(Some(¤cy), Some(&unrealized_pnl))
701 .unwrap();
702 assert!(approx_eq!(f64, result, 100.0, epsilon = 1e-9)); }
704
705 #[rstest]
706 fn test_add_positions_and_returns() {
707 let mut analyzer = PortfolioAnalyzer::new();
708 let currency = Currency::USD();
709
710 let positions = vec![
711 create_mock_position("AUD/USD".to_owned(), 100.0, 0.1, currency),
712 create_mock_position("AUD/USD".to_owned(), 200.0, 0.2, currency),
713 ];
714
715 analyzer.add_positions(&positions);
716
717 let pnls = analyzer.realized_pnls(Some(¤cy)).unwrap();
719 assert_eq!(pnls.len(), 2);
720 assert!(approx_eq!(f64, pnls[0].1, 100.0, epsilon = 1e-9));
721 assert!(approx_eq!(f64, pnls[1].1, 200.0, epsilon = 1e-9));
722
723 let returns = analyzer.returns();
725 assert_eq!(returns.len(), 1);
726 assert!(approx_eq!(
727 f64,
728 *returns.values().next().unwrap(),
729 0.30000000000000004,
730 epsilon = 1e-9
731 ));
732 }
733
734 #[rstest]
735 fn test_performance_stats_calculation() {
736 let mut analyzer = PortfolioAnalyzer::new();
737 let currency = Currency::USD();
738 let stat: Arc<dyn PortfolioStatistic<Item = f64> + Send + Sync> =
739 Arc::new(MockStatistic::new("test_stat"));
740 analyzer.register_statistic(Arc::clone(&stat));
741
742 let positions = vec![
744 create_mock_position("AUD/USD".to_owned(), 100.0, 0.1, currency),
745 create_mock_position("AUD/USD".to_owned(), 200.0, 0.2, currency),
746 ];
747
748 let mut starting_balances = HashMap::new();
749 starting_balances.insert(currency, Money::new(1000.0, currency));
750
751 let mut current_balances = HashMap::new();
752 current_balances.insert(currency, Money::new(1500.0, currency));
753
754 let account = MockAccount {
755 starting_balances,
756 current_balances,
757 };
758
759 analyzer.calculate_statistics(&account, &positions);
760
761 let pnl_stats = analyzer
763 .get_performance_stats_pnls(Some(¤cy), None)
764 .unwrap();
765 assert!(pnl_stats.contains_key("PnL (total)"));
766 assert!(pnl_stats.contains_key("PnL% (total)"));
767 assert!(pnl_stats.contains_key("test_stat"));
768
769 let return_stats = analyzer.get_performance_stats_returns();
771 assert!(return_stats.contains_key("test_stat"));
772
773 let general_stats = analyzer.get_performance_stats_general();
775 assert!(general_stats.contains_key("test_stat"));
776 }
777
778 #[rstest]
779 fn test_formatted_output() {
780 let mut analyzer = PortfolioAnalyzer::new();
781 let currency = Currency::USD();
782 let stat: Arc<dyn PortfolioStatistic<Item = f64> + Send + Sync> =
783 Arc::new(MockStatistic::new("test_stat"));
784 analyzer.register_statistic(Arc::clone(&stat));
785
786 let positions = vec![
787 create_mock_position("AUD/USD".to_owned(), 100.0, 0.1, currency),
788 create_mock_position("AUD/USD".to_owned(), 200.0, 0.2, currency),
789 ];
790
791 let mut starting_balances = HashMap::new();
792 starting_balances.insert(currency, Money::new(1000.0, currency));
793
794 let mut current_balances = HashMap::new();
795 current_balances.insert(currency, Money::new(1500.0, currency));
796
797 let account = MockAccount {
798 starting_balances,
799 current_balances,
800 };
801
802 analyzer.calculate_statistics(&account, &positions);
803
804 let pnl_formatted = analyzer
806 .get_stats_pnls_formatted(Some(¤cy), None)
807 .unwrap();
808 assert!(!pnl_formatted.is_empty());
809 assert!(pnl_formatted.iter().all(|s| s.contains(':')));
810
811 let returns_formatted = analyzer.get_stats_returns_formatted();
812 assert!(!returns_formatted.is_empty());
813 assert!(returns_formatted.iter().all(|s| s.contains(':')));
814
815 let general_formatted = analyzer.get_stats_general_formatted();
816 assert!(!general_formatted.is_empty());
817 assert!(general_formatted.iter().all(|s| s.contains(':')));
818 }
819
820 #[rstest]
821 fn test_reset() {
822 let mut analyzer = PortfolioAnalyzer::new();
823 let currency = Currency::USD();
824
825 let positions = vec![create_mock_position(
826 "AUD/USD".to_owned(),
827 100.0,
828 0.1,
829 currency,
830 )];
831 let mut starting_balances = HashMap::new();
832 starting_balances.insert(currency, Money::new(1000.0, currency));
833 let mut current_balances = HashMap::new();
834 current_balances.insert(currency, Money::new(1500.0, currency));
835
836 let account = MockAccount {
837 starting_balances,
838 current_balances,
839 };
840
841 analyzer.calculate_statistics(&account, &positions);
842
843 analyzer.reset();
844
845 assert!(analyzer.account_balances_starting.is_empty());
846 assert!(analyzer.account_balances.is_empty());
847 assert!(analyzer.realized_pnls.is_empty());
848 assert!(analyzer.returns.is_empty());
849 }
850}