investments/analysis/performance/
statistics.rs

1use std::collections::BTreeMap;
2
3use log::warn;
4
5use crate::brokers::Broker;
6use crate::core::EmptyResult;
7use crate::currency::{self, Cash};
8use crate::localities::Country;
9use crate::taxes::{LtoDeduction, NetLtoDeduction, TaxCalculator};
10use crate::types::Decimal;
11
12use super::types::{PerformanceAnalysisMethod, PortfolioPerformanceAnalysis};
13
14pub struct PortfolioStatistics {
15    country: Country,
16    pub currencies: Vec<PortfolioCurrencyStatistics>,
17    pub asset_groups: BTreeMap<String, AssetGroup>,
18    pub lto: Option<LtoStatistics>,
19}
20
21pub struct LtoStatistics {
22    pub applied: BTreeMap<i32, NetLtoDeduction>,
23    pub projected: LtoDeduction,
24}
25
26impl PortfolioStatistics {
27    pub fn new(country: Country) -> PortfolioStatistics {
28        PortfolioStatistics {
29            country: country.clone(),
30            currencies: ["USD", "RUB"].iter().map(|&currency| (
31                PortfolioCurrencyStatistics {
32                    currency: currency.to_owned(),
33
34                    assets: BTreeMap::new(),
35                    brokers: BTreeMap::new(),
36
37                    virtual_performance: None,
38                    real_performance: None,
39                    inflation_adjusted_performance: None,
40
41                    projected_taxes: dec!(0),
42                    projected_tax_deductions: dec!(0),
43                    projected_commissions: dec!(0),
44                }
45            )).collect(),
46            asset_groups: BTreeMap::new(),
47            lto: None,
48        }
49    }
50
51    pub fn print(&self, method: PerformanceAnalysisMethod) {
52        let lto = self.lto.as_ref().unwrap();
53
54        if method.tax_aware() {
55            for (year, result) in &lto.applied {
56                if !result.loss.is_zero() {
57                    warn!("Long-term ownership tax deduction loss in {}: {}.",
58                          year, self.country.cash(result.loss));
59                }
60
61                if !result.applied_above_limit.is_zero() {
62                    warn!("Long-term ownership tax deductions applied in {} have exceeded the total limit by {}.",
63                          year, self.country.cash(result.applied_above_limit));
64                }
65            }
66        }
67
68        for statistics in &self.currencies {
69            statistics.performance(method).print(&format!(
70                "Average rate of return from cash investments in {}", &statistics.currency));
71        }
72
73        if method.tax_aware() && !lto.projected.deduction.is_zero() {
74            lto.projected.print("Projected LTO deduction")
75        }
76    }
77
78    pub fn process<F>(&mut self, mut handler: F) -> EmptyResult
79        where F: FnMut(&mut PortfolioCurrencyStatistics) -> EmptyResult
80    {
81        for statistics in &mut self.currencies {
82            handler(statistics)?;
83        }
84
85        Ok(())
86    }
87
88    pub fn commit(self) -> Self {
89        let asset_groups = self.asset_groups.into_iter().map(|(name, value)| {
90            (name, value.commit())
91        }).collect();
92
93        PortfolioStatistics {
94            country: self.country,
95            currencies: self.currencies.into_iter().map(PortfolioCurrencyStatistics::commit).collect(),
96            asset_groups,
97            lto: self.lto,
98        }
99    }
100}
101
102pub struct PortfolioCurrencyStatistics {
103    pub currency: String,
104
105    // Use BTreeMap to get consistent metrics order
106    pub assets: BTreeMap<String, BTreeMap<String, Asset>>,
107    pub brokers: BTreeMap<Broker, Decimal>,
108
109    pub virtual_performance: Option<PortfolioPerformanceAnalysis>,
110    pub real_performance: Option<PortfolioPerformanceAnalysis>,
111    pub inflation_adjusted_performance: Option<PortfolioPerformanceAnalysis>,
112
113    pub projected_taxes: Decimal,
114    pub projected_tax_deductions: Decimal,
115    pub projected_commissions: Decimal,
116}
117
118impl PortfolioCurrencyStatistics {
119    pub fn add_assets(&mut self, portfolio: &str, broker: Broker, instrument: &str, amount: Decimal, net_amount: Decimal) {
120        let instrument = self.assets.entry(instrument.to_owned()).or_default();
121
122        instrument.entry(portfolio.to_owned()).or_default().add(&Asset {
123            value: amount,
124            net_value: net_amount,
125        });
126
127        *self.brokers.entry(broker).or_default() += amount;
128    }
129
130    pub fn performance(&self, method: PerformanceAnalysisMethod) -> &PortfolioPerformanceAnalysis {
131        match method {
132            PerformanceAnalysisMethod::Virtual => &self.virtual_performance,
133            PerformanceAnalysisMethod::Real => &self.real_performance,
134            PerformanceAnalysisMethod::InflationAdjusted => &self.inflation_adjusted_performance,
135        }.as_ref().unwrap()
136    }
137
138    pub fn set_performance(&mut self, method: PerformanceAnalysisMethod, performance: PortfolioPerformanceAnalysis) {
139        let container = match method {
140            PerformanceAnalysisMethod::Virtual => &mut self.virtual_performance,
141            PerformanceAnalysisMethod::Real => &mut self.real_performance,
142            PerformanceAnalysisMethod::InflationAdjusted => &mut self.inflation_adjusted_performance,
143        };
144        assert!(container.replace(performance).is_none());
145    }
146
147    fn commit(self) -> Self {
148        let assets = self.assets.into_iter().map(|(instrument, portfolios)| {
149            let portfolios = portfolios.into_iter().map(|(portfolio, asset)| {
150                (portfolio, asset.commit())
151            }).collect();
152
153            (instrument, portfolios)
154        }).collect();
155
156        let brokers = self.brokers.into_iter().map(|(broker, value)| {
157            (broker, currency::round(value))
158        }).collect();
159
160        PortfolioCurrencyStatistics {
161            currency: self.currency,
162
163            assets, brokers,
164
165            virtual_performance: self.virtual_performance.map(PortfolioPerformanceAnalysis::commit),
166            real_performance: self.real_performance.map(PortfolioPerformanceAnalysis::commit),
167            inflation_adjusted_performance: self.inflation_adjusted_performance.map(PortfolioPerformanceAnalysis::commit),
168
169            projected_taxes: currency::round(self.projected_taxes),
170            projected_tax_deductions: currency::round(self.projected_tax_deductions),
171            projected_commissions: currency::round(self.projected_commissions),
172        }
173    }
174}
175
176pub struct AssetGroup {
177    pub taxes: TaxCalculator,
178    pub net_value: Vec<Cash>,
179}
180
181impl AssetGroup {
182    fn commit(self) -> Self {
183        AssetGroup {
184            taxes: self.taxes,
185            net_value: self.net_value.into_iter().map(Cash::round).collect(),
186        }
187    }
188}
189
190#[derive(Default)]
191pub struct Asset {
192    pub value: Decimal,
193    pub net_value: Decimal,
194}
195
196impl Asset {
197    pub fn add(&mut self, other: &Asset) {
198        self.value += other.value;
199        self.net_value += other.net_value;
200    }
201
202    fn commit(self) -> Self {
203        Asset {
204            value: currency::round(self.value),
205            net_value: currency::round(self.net_value),
206        }
207    }
208}