use std::collections::{HashSet, HashMap};
use num_traits::Zero;
use crate::brokers::BrokerInfo;
use crate::config::{Config, PortfolioConfig, AssetAllocationConfig};
use crate::core::{EmptyResult, GenericResult};
use crate::currency::Cash;
use crate::currency::converter::CurrencyConverter;
use crate::quotes::Quotes;
use crate::types::{Decimal, TradeType};
use crate::util;
use super::Assets;
pub struct Portfolio {
pub name: String,
pub broker: BrokerInfo,
pub currency: String,
pub min_trade_volume: Decimal,
pub min_cash_assets: Decimal,
pub assets: Vec<AssetAllocation>,
pub current_cash_assets: Decimal,
pub current_net_value: Decimal,
pub target_cash_assets: Decimal,
pub target_net_value: Decimal,
pub commissions: Decimal,
}
impl Portfolio {
pub fn load(
config: &Config, portfolio_config: &PortfolioConfig, assets: Assets,
converter: &CurrencyConverter, quotes: &Quotes
) -> GenericResult<Portfolio> {
let currency = portfolio_config.currency()?;
let broker = portfolio_config.broker.get_info(config, portfolio_config.plan.as_ref())?;
let min_trade_volume = portfolio_config.min_trade_volume.unwrap_or_else(|| dec!(0));
if min_trade_volume.is_sign_negative() {
return Err!("Invalid minimum trade volume value")
}
let min_cash_assets = portfolio_config.min_cash_assets.unwrap_or_else(|| dec!(0));
if min_cash_assets.is_sign_negative() {
return Err!("Invalid minimum free cash assets value")
}
if portfolio_config.assets.is_empty() {
return Err!("The portfolio has no asset allocation configuration");
}
for symbol in portfolio_config.get_stock_symbols() {
quotes.batch(&symbol)?;
}
let cash_assets = assets.cash.total_assets_real_time(¤cy, converter)?;
let mut net_value = cash_assets;
let mut stocks = assets.stocks;
let mut symbols = HashSet::new();
let mut assets_allocation = Vec::new();
for assets_config in &portfolio_config.assets {
let mut asset_allocation = AssetAllocation::load(
&broker, assets_config, ¤cy, &mut symbols, &mut stocks,
converter, quotes)?;
asset_allocation.apply_restrictions(
portfolio_config.restrict_buying, portfolio_config.restrict_selling);
net_value += asset_allocation.current_value;
assets_allocation.push(asset_allocation);
}
let portfolio = Portfolio {
name: portfolio_config.name.clone(),
broker: broker,
currency: currency.to_owned(),
min_trade_volume: min_trade_volume,
min_cash_assets: min_cash_assets,
assets: assets_allocation,
current_cash_assets: cash_assets,
current_net_value: net_value,
target_cash_assets: cash_assets,
target_net_value: net_value,
commissions: dec!(0),
};
check_weights(&portfolio.name, &portfolio.assets)?;
if !stocks.is_empty() {
let mut missing_symbols: Vec<String> = stocks.keys().cloned().collect();
missing_symbols.sort_unstable();
return Err!(
"The portfolio contains stocks that are missing in asset allocation configuration: {}",
missing_symbols.join(", "));
}
Ok(portfolio)
}
pub fn change_commission(&mut self, commission: Decimal) {
self.commissions += commission;
self.target_net_value -= commission;
self.target_cash_assets -= commission;
}
}
pub enum Holding {
Stock(StockHolding),
Group(Vec<AssetAllocation>),
}
pub struct StockHolding {
pub symbol: String,
pub price: Decimal,
pub currency_price: Cash,
pub current_shares: Decimal,
pub target_shares: Decimal,
pub fractional_shares_trading: bool,
}
impl StockHolding {
pub fn trade_granularity(&self) -> Decimal {
self.trade_precision_volume(self.trade_precision())
}
pub fn iterative_trading_granularity(&self, trade_type: TradeType) -> Decimal {
let mut precision = self.trade_precision();
let mut volume = self.trade_precision_volume(precision);
while precision > 0 && volume < dec!(1) {
precision -= 1;
volume = self.trade_precision_volume(precision);
}
if matches!(trade_type,
TradeType::Sell
if self.fractional_shares_trading &&
volume > self.target_shares && !self.target_shares.is_zero()
) {
volume = self.price * self.target_shares;
}
volume
}
fn trade_precision(&self) -> u32 {
if self.fractional_shares_trading {
util::decimal_precision(self.current_shares)
} else {
0
}
}
fn trade_precision_volume(&self, precision: u32) -> Decimal {
self.price * Decimal::new(1, precision)
}
}
pub struct AssetAllocation {
pub name: String,
pub expected_weight: Decimal,
pub restrict_buying: Option<bool>,
pub restrict_selling: Option<bool>,
pub holding: Holding,
pub current_value: Decimal,
pub target_value: Decimal,
pub min_value: Decimal,
pub max_value: Option<Decimal>,
pub buy_blocked: bool,
pub sell_blocked: bool,
}
impl AssetAllocation {
fn load(
broker: &BrokerInfo, config: &AssetAllocationConfig, currency: &str,
symbols: &mut HashSet<String>, stocks: &mut HashMap<String, Decimal>,
converter: &CurrencyConverter, quotes: &Quotes,
) -> GenericResult<AssetAllocation> {
let (holding, current_value) = match (&config.symbol, &config.assets) {
(Some(symbol), None) => {
if !symbols.insert(symbol.clone()) {
return Err!("Invalid asset allocation configuration: Duplicated symbol: {}",
symbol);
}
let currency_price = quotes.get(symbol)?;
let price = converter.real_time_convert_to(currency_price, currency)?;
let shares = stocks.remove(symbol).unwrap_or_else(|| dec!(0));
let current_value = shares * price;
let holding = StockHolding {
symbol: symbol.clone(),
price: price,
currency_price: currency_price,
current_shares: shares,
target_shares: shares,
fractional_shares_trading: broker.fractional_shares_trading,
};
(Holding::Stock(holding), current_value)
},
(None, Some(assets)) => {
let mut holdings = Vec::new();
let mut current_value = dec!(0);
for asset in assets {
let holding = AssetAllocation::load(
broker, asset, currency, symbols, stocks, converter, quotes)?;
current_value += holding.current_value;
holdings.push(holding);
}
check_weights(&config.name, &holdings)?;
(Holding::Group(holdings), current_value)
},
_ => return Err!(
"Invalid {:?} assets configuration: either symbol or assets must be specified",
config.name),
};
let mut asset_allocation = AssetAllocation {
name: config.name.clone(),
expected_weight: config.weight,
restrict_buying: None,
restrict_selling: None,
holding: holding,
current_value: current_value,
target_value: current_value,
min_value: dec!(0),
max_value: None,
buy_blocked: false,
sell_blocked: false,
};
asset_allocation.apply_restrictions(config.restrict_buying, config.restrict_selling);
Ok(asset_allocation)
}
pub fn full_name(&self) -> String {
match self.holding {
Holding::Group(_) => self.name.clone(),
Holding::Stock(ref holding) => format!("{} ({})", self.name, holding.symbol),
}
}
fn apply_restrictions(&mut self, restrict_buying: Option<bool>, restrict_selling: Option<bool>) {
if let Some(restrict) = restrict_buying {
self.apply_buying_restriction(restrict);
}
if let Some(restrict) = restrict_selling {
self.apply_selling_restriction(restrict);
}
}
fn apply_buying_restriction(&mut self, restrict: bool) {
if self.restrict_buying.is_some() {
return
}
self.restrict_buying = Some(restrict);
if let Holding::Group(ref mut assets) = self.holding {
for asset in assets {
asset.apply_buying_restriction(restrict);
}
}
}
fn apply_selling_restriction(&mut self, restrict: bool) {
if self.restrict_selling.is_some() {
return
}
self.restrict_selling = Some(restrict);
if let Holding::Group(ref mut assets) = self.holding {
for asset in assets {
asset.apply_selling_restriction(restrict);
}
}
}
pub fn iterative_trading_granularity(&self, trade_type: TradeType) -> Decimal {
match self.holding {
Holding::Stock(ref holding) => holding.iterative_trading_granularity(trade_type),
Holding::Group(ref holdings) => {
let mut min_granularity = None;
for holding in holdings {
let granularity = holding.iterative_trading_granularity(trade_type);
min_granularity = Some(match min_granularity {
Some(min_granularity) if min_granularity <= granularity => min_granularity,
_ => granularity,
});
}
min_granularity.unwrap()
},
}
}
}
fn check_weights(name: &str, assets: &[AssetAllocation]) -> EmptyResult {
let mut weight = dec!(0);
for asset in assets {
weight += asset.expected_weight;
}
if weight != dec!(1) {
return Err!("{:?} assets have unbalanced weights: {}% total",
name, (weight * dec!(100)).normalize());
}
Ok(())
}