investments/analysis/performance/
statistics.rs1use 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(|¤cy| (
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 <o.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 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}