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 = "posei_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 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 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 if unrealized_pnl.currency != *currency {
261 return Err("Unrealized PnL currency does not match specified currency");
262 }
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) -> Arc<dyn PortfolioStatistic<Item = f64> + Send + Sync> {
445 Arc::new(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::new(MockStatistic::new("test_stat"));
619
620 analyzer.register_statistic(Arc::clone(&stat));
622 assert!(analyzer.statistic("test_stat").is_some());
623
624 analyzer.deregister_statistic(Arc::clone(&stat));
626 assert!(analyzer.statistic("test_stat").is_none());
627
628 let stat1 = Arc::new(MockStatistic::new("stat1"));
630 let stat2 = Arc::new(MockStatistic::new("stat2"));
631 analyzer.register_statistic(Arc::clone(&stat1));
632 analyzer.register_statistic(Arc::clone(&stat2));
633 analyzer.deregister_statistics();
634 assert!(analyzer.statistics.is_empty());
635 }
636
637 #[rstest]
638 fn test_calculate_total_pnl() {
639 let mut analyzer = PortfolioAnalyzer::new();
640 let currency = Currency::USD();
641
642 let mut starting_balances = HashMap::new();
644 starting_balances.insert(currency, Money::new(1000.0, currency));
645
646 let mut current_balances = HashMap::new();
647 current_balances.insert(currency, Money::new(1500.0, currency));
648
649 let account = MockAccount {
650 starting_balances,
651 current_balances,
652 };
653
654 analyzer.calculate_statistics(&account, &[]);
655
656 let result = analyzer.total_pnl(Some(¤cy), None).unwrap();
658 assert_eq!(result, 500.0);
659
660 let unrealized_pnl = Money::new(100.0, currency);
662 let result = analyzer
663 .total_pnl(Some(¤cy), Some(&unrealized_pnl))
664 .unwrap();
665 assert_eq!(result, 600.0);
666 }
667
668 #[rstest]
669 fn test_calculate_total_pnl_percentage() {
670 let mut analyzer = PortfolioAnalyzer::new();
671 let currency = Currency::USD();
672
673 let mut starting_balances = HashMap::new();
675 starting_balances.insert(currency, Money::new(1000.0, currency));
676
677 let mut current_balances = HashMap::new();
678 current_balances.insert(currency, Money::new(1500.0, currency));
679
680 let account = MockAccount {
681 starting_balances,
682 current_balances,
683 };
684
685 analyzer.calculate_statistics(&account, &[]);
686
687 let result = analyzer
689 .total_pnl_percentage(Some(¤cy), None)
690 .unwrap();
691 assert_eq!(result, 50.0); let unrealized_pnl = Money::new(500.0, currency);
695 let result = analyzer
696 .total_pnl_percentage(Some(¤cy), Some(&unrealized_pnl))
697 .unwrap();
698 assert_eq!(result, 100.0); }
700
701 #[rstest]
702 fn test_add_positions_and_returns() {
703 let mut analyzer = PortfolioAnalyzer::new();
704 let currency = Currency::USD();
705
706 let positions = vec![
707 create_mock_position("AUD/USD".to_owned(), 100.0, 0.1, currency),
708 create_mock_position("AUD/USD".to_owned(), 200.0, 0.2, currency),
709 ];
710
711 analyzer.add_positions(&positions);
712
713 let pnls = analyzer.realized_pnls(Some(¤cy)).unwrap();
715 assert_eq!(pnls.len(), 2);
716 assert_eq!(pnls[0].1, 100.0);
717 assert_eq!(pnls[1].1, 200.0);
718
719 let returns = analyzer.returns();
721 assert_eq!(returns.len(), 1);
722 assert_eq!(*returns.values().next().unwrap(), 0.30000000000000004);
723 }
724
725 #[rstest]
726 fn test_performance_stats_calculation() {
727 let mut analyzer = PortfolioAnalyzer::new();
728 let currency = Currency::USD();
729 let stat = Arc::new(MockStatistic::new("test_stat"));
730 analyzer.register_statistic(Arc::clone(&stat));
731
732 let positions = vec![
734 create_mock_position("AUD/USD".to_owned(), 100.0, 0.1, currency),
735 create_mock_position("AUD/USD".to_owned(), 200.0, 0.2, currency),
736 ];
737
738 let mut starting_balances = HashMap::new();
739 starting_balances.insert(currency, Money::new(1000.0, currency));
740
741 let mut current_balances = HashMap::new();
742 current_balances.insert(currency, Money::new(1500.0, currency));
743
744 let account = MockAccount {
745 starting_balances,
746 current_balances,
747 };
748
749 analyzer.calculate_statistics(&account, &positions);
750
751 let pnl_stats = analyzer
753 .get_performance_stats_pnls(Some(¤cy), None)
754 .unwrap();
755 assert!(pnl_stats.contains_key("PnL (total)"));
756 assert!(pnl_stats.contains_key("PnL% (total)"));
757 assert!(pnl_stats.contains_key("test_stat"));
758
759 let return_stats = analyzer.get_performance_stats_returns();
761 assert!(return_stats.contains_key("test_stat"));
762
763 let general_stats = analyzer.get_performance_stats_general();
765 assert!(general_stats.contains_key("test_stat"));
766 }
767
768 #[rstest]
769 fn test_formatted_output() {
770 let mut analyzer = PortfolioAnalyzer::new();
771 let currency = Currency::USD();
772 let stat = Arc::new(MockStatistic::new("test_stat"));
773 analyzer.register_statistic(Arc::clone(&stat));
774
775 let positions = vec![
776 create_mock_position("AUD/USD".to_owned(), 100.0, 0.1, currency),
777 create_mock_position("AUD/USD".to_owned(), 200.0, 0.2, currency),
778 ];
779
780 let mut starting_balances = HashMap::new();
781 starting_balances.insert(currency, Money::new(1000.0, currency));
782
783 let mut current_balances = HashMap::new();
784 current_balances.insert(currency, Money::new(1500.0, currency));
785
786 let account = MockAccount {
787 starting_balances,
788 current_balances,
789 };
790
791 analyzer.calculate_statistics(&account, &positions);
792
793 let pnl_formatted = analyzer
795 .get_stats_pnls_formatted(Some(¤cy), None)
796 .unwrap();
797 assert!(!pnl_formatted.is_empty());
798 assert!(pnl_formatted.iter().all(|s| s.contains(':')));
799
800 let returns_formatted = analyzer.get_stats_returns_formatted();
801 assert!(!returns_formatted.is_empty());
802 assert!(returns_formatted.iter().all(|s| s.contains(':')));
803
804 let general_formatted = analyzer.get_stats_general_formatted();
805 assert!(!general_formatted.is_empty());
806 assert!(general_formatted.iter().all(|s| s.contains(':')));
807 }
808
809 #[rstest]
810 fn test_reset() {
811 let mut analyzer = PortfolioAnalyzer::new();
812 let currency = Currency::USD();
813
814 let positions = vec![create_mock_position(
815 "AUD/USD".to_owned(),
816 100.0,
817 0.1,
818 currency,
819 )];
820 let mut starting_balances = HashMap::new();
821 starting_balances.insert(currency, Money::new(1000.0, currency));
822 let mut current_balances = HashMap::new();
823 current_balances.insert(currency, Money::new(1500.0, currency));
824
825 let account = MockAccount {
826 starting_balances,
827 current_balances,
828 };
829
830 analyzer.calculate_statistics(&account, &positions);
831
832 analyzer.reset();
833
834 assert!(analyzer.account_balances_starting.is_empty());
835 assert!(analyzer.account_balances.is_empty());
836 assert!(analyzer.realized_pnls.is_empty());
837 assert!(analyzer.returns.is_empty());
838 }
839}