nautilus_analysis/
analyzer.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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::{statistic::PortfolioStatistic, Returns};
32
33pub type Statistic = Arc<dyn PortfolioStatistic<Item = f64> + Send + Sync>;
34
35/// Analyzes portfolio performance and calculates various statistics.
36///
37/// The `PortfolioAnalyzer` tracks account balances, positions, and realized PnLs
38/// to provide comprehensive portfolio analysis including returns, PnL calculations,
39/// and customizable statistics.
40#[repr(C)]
41#[derive(Debug)]
42#[cfg_attr(
43    feature = "python",
44    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.analysis")
45)]
46pub struct PortfolioAnalyzer {
47    statistics: HashMap<String, Statistic>,
48    account_balances_starting: HashMap<Currency, Money>,
49    account_balances: HashMap<Currency, Money>,
50    positions: Vec<Position>,
51    realized_pnls: HashMap<Currency, Vec<(PositionId, f64)>>,
52    returns: Returns,
53}
54
55impl Default for PortfolioAnalyzer {
56    /// Creates a new default [`PortfolioAnalyzer`] instance.
57    fn default() -> Self {
58        Self::new()
59    }
60}
61
62impl PortfolioAnalyzer {
63    /// Creates a new [`PortfolioAnalyzer`] instance.
64    ///
65    /// Starts with empty state.
66    #[must_use]
67    pub fn new() -> Self {
68        Self {
69            statistics: HashMap::new(),
70            account_balances_starting: HashMap::new(),
71            account_balances: HashMap::new(),
72            positions: Vec::new(),
73            realized_pnls: HashMap::new(),
74            returns: BTreeMap::new(),
75        }
76    }
77
78    /// Registers a new portfolio statistic for calculation.
79    pub fn register_statistic(&mut self, statistic: Statistic) {
80        self.statistics.insert(statistic.name(), statistic);
81    }
82
83    /// Removes a specific statistic from calculation.
84    pub fn deregister_statistic(&mut self, statistic: Statistic) {
85        self.statistics.remove(&statistic.name());
86    }
87
88    /// Removes all registered statistics.
89    pub fn deregister_statistics(&mut self) {
90        self.statistics.clear();
91    }
92
93    /// Resets all analysis data to initial state.
94    pub fn reset(&mut self) {
95        self.account_balances_starting.clear();
96        self.account_balances.clear();
97        self.realized_pnls.clear();
98        self.returns.clear();
99    }
100
101    /// Returns all tracked currencies.
102    #[must_use]
103    pub fn currencies(&self) -> Vec<&Currency> {
104        self.account_balances.keys().collect()
105    }
106
107    /// Retrieves a specific statistic by name.
108    #[must_use]
109    pub fn statistic(&self, name: &str) -> Option<&Statistic> {
110        self.statistics.get(name)
111    }
112
113    /// Returns all calculated returns.
114    #[must_use]
115    pub const fn returns(&self) -> &Returns {
116        &self.returns
117    }
118
119    /// Calculates statistics based on account and position data.
120    pub fn calculate_statistics(&mut self, account: &dyn Account, positions: &[Position]) {
121        self.account_balances_starting = account.starting_balances();
122        self.account_balances = account.balances_total();
123        self.realized_pnls.clear();
124        self.returns.clear();
125
126        self.add_positions(positions);
127    }
128
129    /// Adds new positions for analysis.
130    pub fn add_positions(&mut self, positions: &[Position]) {
131        self.positions.extend_from_slice(positions);
132        for position in positions {
133            if let Some(ref pnl) = position.realized_pnl {
134                self.add_trade(&position.id, pnl);
135            }
136            self.add_return(
137                position.ts_closed.unwrap_or(UnixNanos::default()),
138                position.realized_return,
139            );
140        }
141    }
142
143    /// Records a trade's `PnL`.
144    pub fn add_trade(&mut self, position_id: &PositionId, pnl: &Money) {
145        let currency = pnl.currency;
146        let entry = self.realized_pnls.entry(currency).or_default();
147        entry.push((*position_id, pnl.as_f64()));
148    }
149
150    /// Records a return at a specific timestamp.
151    pub fn add_return(&mut self, timestamp: UnixNanos, value: f64) {
152        self.returns
153            .entry(timestamp)
154            .and_modify(|existing_value| *existing_value += value)
155            .or_insert(value);
156    }
157
158    /// Retrieves realized `PnLs` for a specific currency.
159    #[must_use]
160    pub fn realized_pnls(&self, currency: Option<&Currency>) -> Option<Vec<(PositionId, f64)>> {
161        if self.realized_pnls.is_empty() {
162            return None;
163        }
164        let currency = currency.or_else(|| self.account_balances.keys().next())?;
165        self.realized_pnls.get(currency).cloned()
166    }
167
168    /// Calculates total `PnL` including unrealized `PnL` if provided.
169    pub fn total_pnl(
170        &self,
171        currency: Option<&Currency>,
172        unrealized_pnl: Option<&Money>,
173    ) -> Result<f64, &'static str> {
174        if self.account_balances.is_empty() {
175            return Ok(0.0);
176        }
177
178        let currency = currency
179            .or_else(|| self.account_balances.keys().next())
180            .ok_or("Currency not specified for multi-currency portfolio")?;
181
182        if let Some(unrealized_pnl) = unrealized_pnl {
183            if unrealized_pnl.currency != *currency {
184                return Err("Unrealized PnL currency does not match specified currency");
185            }
186        }
187
188        let account_balance = self
189            .account_balances
190            .get(currency)
191            .ok_or("Specified currency not found in account balances")?;
192
193        let default_money = &Money::new(0.0, *currency);
194        let account_balance_starting = self
195            .account_balances_starting
196            .get(currency)
197            .unwrap_or(default_money);
198
199        let unrealized_pnl_f64 = unrealized_pnl.map_or(0.0, Money::as_f64);
200        Ok((account_balance.as_f64() - account_balance_starting.as_f64()) + unrealized_pnl_f64)
201    }
202
203    /// Calculates total `PnL` as a percentage of starting balance.
204    pub fn total_pnl_percentage(
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            if unrealized_pnl.currency != *currency {
219                return Err("Unrealized PnL currency does not match specified currency");
220            }
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        if account_balance_starting.as_decimal() == Decimal::ZERO {
235            return Ok(0.0);
236        }
237
238        let unrealized_pnl_f64 = unrealized_pnl.map_or(0.0, Money::as_f64);
239        let current = account_balance.as_f64() + unrealized_pnl_f64;
240        let starting = account_balance_starting.as_f64();
241        let difference = current - starting;
242
243        Ok((difference / starting) * 100.0)
244    }
245
246    /// Gets all PnL-related performance statistics.
247    pub fn get_performance_stats_pnls(
248        &self,
249        currency: Option<&Currency>,
250        unrealized_pnl: Option<&Money>,
251    ) -> Result<HashMap<String, f64>, &'static str> {
252        let mut output = HashMap::new();
253
254        output.insert(
255            "PnL (total)".to_string(),
256            self.total_pnl(currency, unrealized_pnl)?,
257        );
258        output.insert(
259            "PnL% (total)".to_string(),
260            self.total_pnl_percentage(currency, unrealized_pnl)?,
261        );
262
263        if let Some(realized_pnls) = self.realized_pnls(currency) {
264            for (name, stat) in &self.statistics {
265                if let Some(value) = stat.calculate_from_realized_pnls(
266                    &realized_pnls
267                        .iter()
268                        .map(|(_, pnl)| *pnl)
269                        .collect::<Vec<f64>>(),
270                ) {
271                    output.insert(name.clone(), value);
272                }
273            }
274        }
275
276        Ok(output)
277    }
278
279    /// Gets all return-based performance statistics.
280    #[must_use]
281    pub fn get_performance_stats_returns(&self) -> HashMap<String, f64> {
282        let mut output = HashMap::new();
283
284        for (name, stat) in &self.statistics {
285            if let Some(value) = stat.calculate_from_returns(&self.returns) {
286                output.insert(name.clone(), value);
287            }
288        }
289
290        output
291    }
292
293    /// Gets general portfolio statistics.
294    #[must_use]
295    pub fn get_performance_stats_general(&self) -> HashMap<String, f64> {
296        let mut output = HashMap::new();
297
298        for (name, stat) in &self.statistics {
299            if let Some(value) = stat.calculate_from_positions(&self.positions) {
300                output.insert(name.clone(), value);
301            }
302        }
303
304        output
305    }
306
307    /// Calculates the maximum length of statistic names for formatting.
308    fn get_max_length_name(&self) -> usize {
309        self.statistics.keys().map(String::len).max().unwrap_or(0)
310    }
311
312    /// Gets formatted `PnL` statistics as strings.
313    pub fn get_stats_pnls_formatted(
314        &self,
315        currency: Option<&Currency>,
316        unrealized_pnl: Option<&Money>,
317    ) -> Result<Vec<String>, String> {
318        let max_length = self.get_max_length_name();
319        let stats = self.get_performance_stats_pnls(currency, unrealized_pnl)?;
320
321        let mut output = Vec::new();
322        for (k, v) in stats {
323            let padding = if max_length > k.len() {
324                max_length - k.len() + 1
325            } else {
326                1
327            };
328            output.push(format!("{}: {}{:.2}", k, " ".repeat(padding), v));
329        }
330
331        Ok(output)
332    }
333
334    /// Gets formatted return statistics as strings.
335    #[must_use]
336    pub fn get_stats_returns_formatted(&self) -> Vec<String> {
337        let max_length = self.get_max_length_name();
338        let stats = self.get_performance_stats_returns();
339
340        let mut output = Vec::new();
341        for (k, v) in stats {
342            let padding = max_length - k.len() + 1;
343            output.push(format!("{}: {}{:.2}", k, " ".repeat(padding), v));
344        }
345
346        output
347    }
348
349    /// Gets formatted general statistics as strings.
350    #[must_use]
351    pub fn get_stats_general_formatted(&self) -> Vec<String> {
352        let max_length = self.get_max_length_name();
353        let stats = self.get_performance_stats_general();
354
355        let mut output = Vec::new();
356        for (k, v) in stats {
357            let padding = max_length - k.len() + 1;
358            output.push(format!("{}: {}{}", k, " ".repeat(padding), v));
359        }
360
361        output
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use std::sync::Arc;
368
369    use nautilus_model::{
370        enums::{AccountType, LiquiditySide, OrderSide},
371        events::{AccountState, OrderFilled},
372        identifiers::{
373            stubs::{instrument_id_aud_usd_sim, strategy_id_ema_cross, trader_id},
374            AccountId, ClientOrderId,
375        },
376        instruments::InstrumentAny,
377        types::{AccountBalance, Money, Price, Quantity},
378    };
379
380    use super::*;
381
382    /// Mock implementation of `PortfolioStatistic` for testing.
383    #[derive(Debug)]
384    struct MockStatistic {
385        name: String,
386    }
387
388    impl MockStatistic {
389        fn new(name: &str) -> Arc<dyn PortfolioStatistic<Item = f64> + Send + Sync> {
390            Arc::new(Self {
391                name: name.to_string(),
392            })
393        }
394    }
395
396    impl PortfolioStatistic for MockStatistic {
397        type Item = f64;
398
399        fn name(&self) -> String {
400            self.name.clone()
401        }
402
403        fn calculate_from_realized_pnls(&self, pnls: &[f64]) -> Option<f64> {
404            Some(pnls.iter().sum())
405        }
406
407        fn calculate_from_returns(&self, returns: &Returns) -> Option<f64> {
408            Some(returns.values().sum())
409        }
410
411        fn calculate_from_positions(&self, positions: &[Position]) -> Option<f64> {
412            Some(positions.len() as f64)
413        }
414    }
415
416    fn create_mock_position(
417        id: String,
418        realized_pnl: f64,
419        realized_return: f64,
420        currency: Currency,
421    ) -> Position {
422        Position {
423            events: Vec::new(),
424            trader_id: trader_id(),
425            strategy_id: strategy_id_ema_cross(),
426            instrument_id: instrument_id_aud_usd_sim(),
427            id: PositionId::new(&id),
428            account_id: AccountId::new("test-account"),
429            opening_order_id: ClientOrderId::default(),
430            closing_order_id: None,
431            entry: OrderSide::NoOrderSide,
432            side: nautilus_model::enums::PositionSide::NoPositionSide,
433            signed_qty: 0.0,
434            quantity: Quantity::default(),
435            peak_qty: Quantity::default(),
436            price_precision: 2,
437            size_precision: 2,
438            multiplier: Quantity::default(),
439            is_inverse: false,
440            base_currency: None,
441            quote_currency: Currency::USD(),
442            settlement_currency: Currency::USD(),
443            ts_init: UnixNanos::default(),
444            ts_opened: UnixNanos::default(),
445            ts_last: UnixNanos::default(),
446            ts_closed: None,
447            duration_ns: 2,
448            avg_px_open: 0.0,
449            avg_px_close: None,
450            realized_return,
451            realized_pnl: Some(Money::new(realized_pnl, currency)),
452            trade_ids: Vec::new(),
453            buy_qty: Quantity::default(),
454            sell_qty: Quantity::default(),
455            commissions: HashMap::new(),
456        }
457    }
458
459    struct MockAccount {
460        starting_balances: HashMap<Currency, Money>,
461        current_balances: HashMap<Currency, Money>,
462    }
463
464    impl Account for MockAccount {
465        fn starting_balances(&self) -> HashMap<Currency, Money> {
466            self.starting_balances.clone()
467        }
468        fn balances_total(&self) -> HashMap<Currency, Money> {
469            self.current_balances.clone()
470        }
471        fn id(&self) -> AccountId {
472            todo!()
473        }
474        fn account_type(&self) -> AccountType {
475            todo!()
476        }
477        fn base_currency(&self) -> Option<Currency> {
478            todo!()
479        }
480        fn is_cash_account(&self) -> bool {
481            todo!()
482        }
483        fn is_margin_account(&self) -> bool {
484            todo!()
485        }
486        fn calculated_account_state(&self) -> bool {
487            todo!()
488        }
489        fn balance_total(&self, _: Option<Currency>) -> Option<Money> {
490            todo!()
491        }
492        fn balance_free(&self, _: Option<Currency>) -> Option<Money> {
493            todo!()
494        }
495        fn balances_free(&self) -> HashMap<Currency, Money> {
496            todo!()
497        }
498        fn balance_locked(&self, _: Option<Currency>) -> Option<Money> {
499            todo!()
500        }
501        fn balances_locked(&self) -> HashMap<Currency, Money> {
502            todo!()
503        }
504        fn last_event(&self) -> Option<AccountState> {
505            todo!()
506        }
507        fn events(&self) -> Vec<AccountState> {
508            todo!()
509        }
510        fn event_count(&self) -> usize {
511            todo!()
512        }
513        fn currencies(&self) -> Vec<Currency> {
514            todo!()
515        }
516        fn balances(&self) -> HashMap<Currency, AccountBalance> {
517            todo!()
518        }
519        fn apply(&mut self, _: AccountState) {
520            todo!()
521        }
522        fn calculate_balance_locked(
523            &mut self,
524            _: InstrumentAny,
525            _: OrderSide,
526            _: Quantity,
527            _: Price,
528            _: Option<bool>,
529        ) -> Result<Money, anyhow::Error> {
530            todo!()
531        }
532        fn calculate_pnls(
533            &self,
534            _: InstrumentAny,
535            _: OrderFilled,
536            _: Option<Position>,
537        ) -> Result<Vec<Money>, anyhow::Error> {
538            todo!()
539        }
540        fn calculate_commission(
541            &self,
542            _: InstrumentAny,
543            _: Quantity,
544            _: Price,
545            _: LiquiditySide,
546            _: Option<bool>,
547        ) -> Result<Money, anyhow::Error> {
548            todo!()
549        }
550
551        fn balance(&self, _: Option<Currency>) -> Option<&AccountBalance> {
552            todo!()
553        }
554    }
555
556    #[test]
557    fn test_register_and_deregister_statistics() {
558        let mut analyzer = PortfolioAnalyzer::new();
559        let stat = Arc::new(MockStatistic::new("test_stat"));
560
561        // Test registration
562        analyzer.register_statistic(Arc::clone(&stat));
563        assert!(analyzer.statistic("test_stat").is_some());
564
565        // Test deregistration
566        analyzer.deregister_statistic(Arc::clone(&stat));
567        assert!(analyzer.statistic("test_stat").is_none());
568
569        // Test deregister all
570        let stat1 = Arc::new(MockStatistic::new("stat1"));
571        let stat2 = Arc::new(MockStatistic::new("stat2"));
572        analyzer.register_statistic(Arc::clone(&stat1));
573        analyzer.register_statistic(Arc::clone(&stat2));
574        analyzer.deregister_statistics();
575        assert!(analyzer.statistics.is_empty());
576    }
577
578    #[test]
579    fn test_calculate_total_pnl() {
580        let mut analyzer = PortfolioAnalyzer::new();
581        let currency = Currency::USD();
582
583        // Set up mock account data
584        let mut starting_balances = HashMap::new();
585        starting_balances.insert(currency, Money::new(1000.0, currency));
586
587        let mut current_balances = HashMap::new();
588        current_balances.insert(currency, Money::new(1500.0, currency));
589
590        let account = MockAccount {
591            starting_balances,
592            current_balances,
593        };
594
595        analyzer.calculate_statistics(&account, &[]);
596
597        // Test total PnL calculation
598        let result = analyzer.total_pnl(Some(&currency), None).unwrap();
599        assert_eq!(result, 500.0);
600
601        // Test with unrealized PnL
602        let unrealized_pnl = Money::new(100.0, currency);
603        let result = analyzer
604            .total_pnl(Some(&currency), Some(&unrealized_pnl))
605            .unwrap();
606        assert_eq!(result, 600.0);
607    }
608
609    #[test]
610    fn test_calculate_total_pnl_percentage() {
611        let mut analyzer = PortfolioAnalyzer::new();
612        let currency = Currency::USD();
613
614        // Set up mock account data
615        let mut starting_balances = HashMap::new();
616        starting_balances.insert(currency, Money::new(1000.0, currency));
617
618        let mut current_balances = HashMap::new();
619        current_balances.insert(currency, Money::new(1500.0, currency));
620
621        let account = MockAccount {
622            starting_balances,
623            current_balances,
624        };
625
626        analyzer.calculate_statistics(&account, &[]);
627
628        // Test percentage calculation
629        let result = analyzer
630            .total_pnl_percentage(Some(&currency), None)
631            .unwrap();
632        assert_eq!(result, 50.0); // (1500 - 1000) / 1000 * 100
633
634        // Test with unrealized PnL
635        let unrealized_pnl = Money::new(500.0, currency);
636        let result = analyzer
637            .total_pnl_percentage(Some(&currency), Some(&unrealized_pnl))
638            .unwrap();
639        assert_eq!(result, 100.0); // (2000 - 1000) / 1000 * 100
640    }
641
642    #[test]
643    fn test_add_positions_and_returns() {
644        let mut analyzer = PortfolioAnalyzer::new();
645        let currency = Currency::USD();
646
647        let positions = vec![
648            create_mock_position("AUD/USD".to_owned(), 100.0, 0.1, currency),
649            create_mock_position("AUD/USD".to_owned(), 200.0, 0.2, currency),
650        ];
651
652        analyzer.add_positions(&positions);
653
654        // Verify realized PnLs were recorded
655        let pnls = analyzer.realized_pnls(Some(&currency)).unwrap();
656        assert_eq!(pnls.len(), 2);
657        assert_eq!(pnls[0].1, 100.0);
658        assert_eq!(pnls[1].1, 200.0);
659
660        // Verify returns were recorded
661        let returns = analyzer.returns();
662        assert_eq!(returns.len(), 1);
663        assert_eq!(*returns.values().next().unwrap(), 0.30000000000000004);
664    }
665
666    #[test]
667    fn test_performance_stats_calculation() {
668        let mut analyzer = PortfolioAnalyzer::new();
669        let currency = Currency::USD();
670        let stat = Arc::new(MockStatistic::new("test_stat"));
671        analyzer.register_statistic(Arc::clone(&stat));
672
673        // Add some positions
674        let positions = vec![
675            create_mock_position("AUD/USD".to_owned(), 100.0, 0.1, currency),
676            create_mock_position("AUD/USD".to_owned(), 200.0, 0.2, currency),
677        ];
678
679        let mut starting_balances = HashMap::new();
680        starting_balances.insert(currency, Money::new(1000.0, currency));
681
682        let mut current_balances = HashMap::new();
683        current_balances.insert(currency, Money::new(1500.0, currency));
684
685        let account = MockAccount {
686            starting_balances,
687            current_balances,
688        };
689
690        analyzer.calculate_statistics(&account, &positions);
691
692        // Test PnL stats
693        let pnl_stats = analyzer
694            .get_performance_stats_pnls(Some(&currency), None)
695            .unwrap();
696        assert!(pnl_stats.contains_key("PnL (total)"));
697        assert!(pnl_stats.contains_key("PnL% (total)"));
698        assert!(pnl_stats.contains_key("test_stat"));
699
700        // Test returns stats
701        let return_stats = analyzer.get_performance_stats_returns();
702        assert!(return_stats.contains_key("test_stat"));
703
704        // Test general stats
705        let general_stats = analyzer.get_performance_stats_general();
706        assert!(general_stats.contains_key("test_stat"));
707    }
708
709    #[test]
710    fn test_formatted_output() {
711        let mut analyzer = PortfolioAnalyzer::new();
712        let currency = Currency::USD();
713        let stat = Arc::new(MockStatistic::new("test_stat"));
714        analyzer.register_statistic(Arc::clone(&stat));
715
716        let positions = vec![
717            create_mock_position("AUD/USD".to_owned(), 100.0, 0.1, currency),
718            create_mock_position("AUD/USD".to_owned(), 200.0, 0.2, currency),
719        ];
720
721        let mut starting_balances = HashMap::new();
722        starting_balances.insert(currency, Money::new(1000.0, currency));
723
724        let mut current_balances = HashMap::new();
725        current_balances.insert(currency, Money::new(1500.0, currency));
726
727        let account = MockAccount {
728            starting_balances,
729            current_balances,
730        };
731
732        analyzer.calculate_statistics(&account, &positions);
733
734        // Test formatted outputs
735        let pnl_formatted = analyzer
736            .get_stats_pnls_formatted(Some(&currency), None)
737            .unwrap();
738        assert!(!pnl_formatted.is_empty());
739        assert!(pnl_formatted.iter().all(|s| s.contains(':')));
740
741        let returns_formatted = analyzer.get_stats_returns_formatted();
742        assert!(!returns_formatted.is_empty());
743        assert!(returns_formatted.iter().all(|s| s.contains(':')));
744
745        let general_formatted = analyzer.get_stats_general_formatted();
746        assert!(!general_formatted.is_empty());
747        assert!(general_formatted.iter().all(|s| s.contains(':')));
748    }
749
750    #[test]
751    fn test_reset() {
752        let mut analyzer = PortfolioAnalyzer::new();
753        let currency = Currency::USD();
754
755        let positions = vec![create_mock_position(
756            "AUD/USD".to_owned(),
757            100.0,
758            0.1,
759            currency,
760        )];
761        let mut starting_balances = HashMap::new();
762        starting_balances.insert(currency, Money::new(1000.0, currency));
763        let mut current_balances = HashMap::new();
764        current_balances.insert(currency, Money::new(1500.0, currency));
765
766        let account = MockAccount {
767            starting_balances,
768            current_balances,
769        };
770
771        analyzer.calculate_statistics(&account, &positions);
772
773        analyzer.reset();
774
775        assert!(analyzer.account_balances_starting.is_empty());
776        assert!(analyzer.account_balances.is_empty());
777        assert!(analyzer.realized_pnls.is_empty());
778        assert!(analyzer.returns.is_empty());
779    }
780}