use std::collections::{HashMap, BTreeMap};
use log::{self, log_enabled, trace};
use num_traits::Zero;
use crate::broker_statement::BrokerStatement;
use crate::config::PortfolioConfig;
use crate::core::{EmptyResult, GenericResult};
use crate::currency::Cash;
use crate::currency::converter::CurrencyConverter;
use crate::formatting;
use crate::localities::Country;
use crate::taxes::NetTaxCalculator;
use crate::types::{Date, Decimal};
use crate::util;
use super::deposit_emulator::{Transaction, InterestPeriod};
use super::deposit_performance;
use super::portfolio_analysis::{
PortfolioPerformanceAnalysis, InstrumentPerformanceAnalysis, IncomeStructure};
pub struct PortfolioPerformanceAnalyser<'a> {
today: Date,
country: &'a Country,
currency: &'a str,
converter: &'a CurrencyConverter,
include_closed_positions: bool,
transactions: Vec<Transaction>,
income_structure: IncomeStructure,
instruments: Option<BTreeMap<String, StockDepositView>>,
current_assets: Decimal,
}
impl <'a> PortfolioPerformanceAnalyser<'a> {
pub fn new(
country: &'a Country, currency: &'a str, converter: &'a CurrencyConverter,
include_closed_positions: bool,
) -> PortfolioPerformanceAnalyser<'a> {
PortfolioPerformanceAnalyser {
today: util::today(),
country,
currency,
converter,
include_closed_positions,
transactions: Vec::new(),
income_structure: Default::default(),
instruments: Some(BTreeMap::new()),
current_assets: dec!(0),
}
}
pub fn add(&mut self, portfolio: &PortfolioConfig, statement: &BrokerStatement) -> EmptyResult {
if !statement.open_positions.is_empty() {
return Err!(
"Unable to calculate current assets: The broker statement has open positions");
}
trace!("Deposit emulator transactions for {:?}:", portfolio.name);
self.process_deposits_and_withdrawals(statement)?;
self.process_positions(statement, portfolio)?;
self.process_dividends(statement, portfolio)?;
self.process_interest(statement, portfolio)?;
self.process_tax_deductions(portfolio)?;
self.process_cash_assets(statement)?;
for (symbol, deposit_view) in self.instruments.as_mut().unwrap().iter_mut() {
if deposit_view.name.is_none() {
deposit_view.name.replace(statement.get_instrument_name(&symbol));
}
}
Ok(())
}
pub fn analyse(mut self) -> GenericResult<PortfolioPerformanceAnalysis> {
let mut instrument_performance = BTreeMap::new();
self.calculate_open_position_periods()?;
for (symbol, deposit_view) in self.instruments.take().unwrap() {
if deposit_view.closed && !self.include_closed_positions {
continue;
}
let analysis = self.analyse_instrument_performance(&symbol, deposit_view)?;
assert!(instrument_performance.insert(symbol, analysis).is_none());
}
let portfolio_performance = self.analyse_portfolio_performance()?;
self.income_structure.net_profit = portfolio_performance.net_profit();
Ok(PortfolioPerformanceAnalysis {
income_structure: self.income_structure,
instruments: instrument_performance,
portfolio: portfolio_performance,
})
}
fn analyse_instrument_performance(
&mut self, symbol: &str, mut deposit_view: StockDepositView
) -> GenericResult<InstrumentPerformanceAnalysis> {
deposit_view.transactions.sort_by_key(|transaction| transaction.date);
let (interest, difference) = deposit_performance::compare_to_bank_deposit(
&deposit_view.transactions, &deposit_view.interest_periods, dec!(0))?;
deposit_performance::check_emulation_precision(
symbol, self.currency, deposit_view.last_sell_volume.unwrap(), difference)?;
let name = deposit_view.name.unwrap();
let days = get_total_activity_duration(&deposit_view.interest_periods);
let mut investments = dec!(0);
let mut result = dec!(0);
for transaction in &deposit_view.transactions {
if transaction.amount.is_sign_positive() {
investments += transaction.amount;
} else {
result += -transaction.amount;
}
}
Ok(InstrumentPerformanceAnalysis {
name, days, investments, result, interest,
inactive: deposit_view.closed,
})
}
fn analyse_portfolio_performance(&mut self) -> GenericResult<InstrumentPerformanceAnalysis> {
if self.transactions.is_empty() {
return Err!("The portfolio has no activity yet");
}
self.transactions.sort_by_key(|transaction| transaction.date);
let activity_periods = vec![InterestPeriod::new(
self.transactions.first().unwrap().date, self.today)];
let (interest, difference) = deposit_performance::compare_to_bank_deposit(
&self.transactions, &activity_periods, self.current_assets)?;
deposit_performance::check_emulation_precision(
"portfolio", self.currency, self.current_assets, difference)?;
let days = get_total_activity_duration(&activity_periods);
let investments = self.transactions.iter()
.map(|transaction| transaction.amount)
.sum();
Ok(InstrumentPerformanceAnalysis {
name: s!("Portfolio"),
days, investments,
result: self.current_assets,
interest,
inactive: false
})
}
fn calculate_open_position_periods(&mut self) -> EmptyResult {
struct OpenPosition {
start_date: Date,
quantity: Decimal,
}
trace!("Open positions periods:");
for (symbol, deposit_view) in self.instruments.as_mut().unwrap() {
if deposit_view.trades.is_empty() {
return Err!("Got an unexpected transaction for {} which has no trades", symbol)
}
let mut open_position = None;
for (&date, &quantity) in &deposit_view.trades {
let current = open_position.get_or_insert_with(|| {
OpenPosition {
start_date: date,
quantity: dec!(0),
}
});
current.quantity += quantity;
if current.quantity > dec!(0) {
continue;
} else if current.quantity < dec!(0) {
return Err!(
"Error while processing {} sell operations: Got a negative balance on {}",
symbol, formatting::format_date(date));
}
let start_date = current.start_date;
let end_date = if date == start_date {
date.succ()
} else {
date
};
match deposit_view.interest_periods.last_mut() {
Some(ref mut period) if period.end >= start_date => {
assert_eq!(period.end, start_date);
assert!(period.end < end_date);
period.end = end_date;
},
_ => deposit_view.interest_periods.push(InterestPeriod::new(start_date, end_date)),
};
open_position = None;
}
if open_position.is_some() {
return Err!(
"The portfolio contains unsold {} stocks when sellout simulation is expected",
symbol);
}
assert!(!deposit_view.interest_periods.is_empty());
if log_enabled!(log::Level::Trace) {
let periods = deposit_view.interest_periods.iter()
.map(|period| format!(
"{} - {}", formatting::format_date(period.start),
formatting::format_date(period.end)))
.collect::<Vec<_>>()
.join(", ");
trace!("* {}: {}", symbol, periods);
}
}
Ok(())
}
fn process_deposits_and_withdrawals(&mut self, statement: &BrokerStatement) -> EmptyResult {
for mut cash_flow in statement.cash_flows.iter().cloned() {
if cash_flow.cash.is_positive() {
cash_flow.cash.amount += statement.broker.get_deposit_commission(cash_flow)?;
}
let amount = self.converter.convert_to(cash_flow.date, cash_flow.cash, self.currency)?;
trace!("* {} {}: {}", if amount.is_sign_positive() {
"Deposit"
} else {
"Withdrawal"
}, formatting::format_date(cash_flow.date), amount.normalize());
self.transaction(cash_flow.date, amount);
}
Ok(())
}
fn process_positions(&mut self, statement: &BrokerStatement, portfolio: &PortfolioConfig) -> EmptyResult {
let mut taxes = NetTaxCalculator::new(self.country.clone(), portfolio.tax_payment_day);
let mut stock_taxes = HashMap::new();
for trade in &statement.stock_buys {
let multiplier = statement.stock_splits.get_multiplier(
&trade.symbol, trade.conclusion_date, self.today);
let commission = self.converter.convert_to(
trade.conclusion_date, trade.commission, self.currency)?;
self.income_structure.commissions += commission;
let mut assets = self.converter.convert_to(
trade.execution_date, trade.volume, self.currency)?;
assets += commission;
let deposit_view = self.get_deposit_view(&trade.symbol);
deposit_view.trade(trade.conclusion_date, multiplier * trade.quantity);
deposit_view.transaction(trade.conclusion_date, assets);
}
for trade in &statement.stock_sells {
let multiplier = statement.stock_splits.get_multiplier(
&trade.symbol, trade.conclusion_date, self.today);
let assets = self.converter.convert_to(
trade.execution_date, trade.volume, self.currency)?;
let commission = self.converter.convert_to(
trade.conclusion_date, trade.commission, self.currency)?;
self.income_structure.commissions += commission;
{
let deposit_view = self.get_deposit_view(&trade.symbol);
deposit_view.trade(trade.conclusion_date, multiplier * -trade.quantity);
deposit_view.transaction(trade.conclusion_date, -assets);
deposit_view.transaction(trade.conclusion_date, commission);
deposit_view.last_sell_volume.replace(assets);
if trade.emulation {
deposit_view.closed = false;
}
}
let (tax_year, _) = portfolio.tax_payment_day.get(trade.execution_date, true);
let details = trade.calculate(&self.country, tax_year, self.converter)?;
let local_profit = details.local_profit.amount;
stock_taxes.entry(&trade.symbol)
.or_insert_with(|| NetTaxCalculator::new(self.country.clone(), portfolio.tax_payment_day))
.add_profit(trade.execution_date, local_profit);
taxes.add_profit(trade.execution_date, local_profit);
}
for (&symbol, symbol_taxes) in stock_taxes.iter() {
for (&tax_payment_date, &tax_to_pay) in symbol_taxes.get_taxes().iter() {
if let Some(amount) = self.map_tax_to_deposit_amount(tax_payment_date, tax_to_pay)? {
trace!("* {} selling {} tax: {}",
symbol, formatting::format_date(tax_payment_date), amount);
self.get_deposit_view(symbol).transaction(tax_payment_date, amount);
}
}
}
for (&tax_payment_date, &tax_to_pay) in taxes.get_taxes().iter() {
if let Some(amount) = self.map_tax_to_deposit_amount(tax_payment_date, tax_to_pay)? {
trace!("* Stock selling {} tax: {}", formatting::format_date(tax_payment_date), amount);
self.transaction(tax_payment_date, amount);
self.income_structure.taxes += amount;
}
}
Ok(())
}
fn process_dividends(&mut self, statement: &BrokerStatement, portfolio: &PortfolioConfig) -> EmptyResult {
for dividend in &statement.dividends {
let income = dividend.amount.sub(dividend.paid_tax).map_err(|e| format!(
"{}: The tax is paid in currency different from the dividend currency: {}",
dividend.description(), e))?;
let income = self.converter.convert_to(dividend.date, income, self.currency)?;
self.get_deposit_view(÷nd.issuer).transaction(dividend.date, -income);
self.income_structure.dividends += income;
let tax_to_pay = dividend.tax_to_pay(&self.country, self.converter)?;
let (_, tax_payment_date) = portfolio.tax_payment_day.get(dividend.date, false);
if let Some(amount) = self.map_tax_to_deposit_amount(tax_payment_date, tax_to_pay)? {
trace!("* {} {} dividend {} tax: {}",
dividend.issuer, formatting::format_date(dividend.date),
formatting::format_date(tax_payment_date), amount);
self.get_deposit_view(÷nd.issuer).transaction(tax_payment_date, amount);
self.transaction(tax_payment_date, amount);
self.income_structure.taxes += amount;
}
}
Ok(())
}
fn process_interest(&mut self, statement: &BrokerStatement, portfolio: &PortfolioConfig) -> EmptyResult {
for interest in &statement.idle_cash_interest {
self.income_structure.interest += self.converter.convert_to(
interest.date, interest.amount, self.currency)?;
let tax_to_pay = interest.tax_to_pay(&self.country, self.converter)?;
let (_, tax_payment_date) = portfolio.tax_payment_day.get(interest.date, false);
if let Some(amount) = self.map_tax_to_deposit_amount(tax_payment_date, tax_to_pay)? {
trace!("* {} idle cash interest {} tax: {}",
formatting::format_date(interest.date),
formatting::format_date(tax_payment_date), amount);
self.transaction(tax_payment_date, amount);
self.income_structure.taxes += amount;
}
}
Ok(())
}
fn process_tax_deductions(&mut self, portfolio: &PortfolioConfig) -> EmptyResult {
for &(date, amount) in &portfolio.tax_deductions {
let amount = self.converter.convert(self.country.currency, self.currency, date, amount)?;
trace!("* Tax deduction {}: {}", formatting::format_date(date), -amount);
self.transaction(date, -amount);
self.income_structure.tax_deductions += amount;
}
Ok(())
}
fn process_cash_assets(&mut self, statement: &BrokerStatement) -> EmptyResult {
self.current_assets += statement.cash_assets.total_assets_real_time(
self.currency, self.converter)?;
Ok(())
}
fn get_deposit_view(&mut self, symbol: &str) -> &mut StockDepositView {
self.instruments.as_mut().unwrap()
.entry(symbol.to_owned())
.or_insert_with(StockDepositView::new)
}
fn transaction(&mut self, date: Date, amount: Decimal) {
self.transactions.push(Transaction::new(date, amount));
}
fn map_tax_to_deposit_amount(&self, tax_payment_date: Date, tax_to_pay: Decimal) -> GenericResult<Option<Decimal>> {
if tax_to_pay.is_zero() {
return Ok(None);
}
assert!(tax_to_pay.is_sign_positive());
let tax_to_pay = Cash::new(self.country.currency, tax_to_pay);
let conversion_date = if tax_payment_date > self.today {
self.today
} else {
tax_payment_date
};
Ok(Some(self.converter.convert_to(conversion_date, tax_to_pay, self.currency)?))
}
}
struct StockDepositView {
name: Option<String>,
trades: BTreeMap<Date, Decimal>,
transactions: Vec<Transaction>,
interest_periods: Vec<InterestPeriod>,
last_sell_volume: Option<Decimal>,
closed: bool,
}
impl StockDepositView {
fn new() -> StockDepositView {
StockDepositView {
name: None,
trades: BTreeMap::new(),
transactions: Vec::new(),
interest_periods: Vec::new(),
last_sell_volume: None,
closed: true,
}
}
fn trade(&mut self, date: Date, quantity: Decimal) {
self.trades.entry(date)
.and_modify(|total| *total += quantity)
.or_insert(quantity);
}
fn transaction(&mut self, date: Date, amount: Decimal) {
self.transactions.push(Transaction::new(date, amount))
}
}
fn get_total_activity_duration(periods: &[InterestPeriod]) -> u32 {
periods.iter().map(InterestPeriod::days).sum()
}