use std::fmt::Write;
use ansi_term::{Style, Color, ANSIString};
use num_traits::Zero;
use crate::currency::Cash;
use crate::types::Decimal;
use crate::util;
use super::asset_allocation::{Portfolio, AssetAllocation, Holding};
pub fn print_portfolio(portfolio: Portfolio, flat: bool) {
let mut assets = portfolio.assets;
if flat {
assets = flatify(assets, dec!(1));
}
print_assets(assets, portfolio.target_net_value - portfolio.min_cash_assets, &portfolio.currency, 0);
println!("\n{} {}", colorify_title("Total value:"),
format_cash(&portfolio.currency, portfolio.target_net_value));
print!("{} {}", colorify_title("Cash assets:"),
format_cash(&portfolio.currency, portfolio.current_cash_assets));
if portfolio.target_cash_assets != portfolio.current_cash_assets {
print!(" -> {}", format_cash(&portfolio.currency, portfolio.target_cash_assets));
}
println!();
if !portfolio.commissions.is_zero() {
println!("{} {}", colorify_title("Commissions:"),
colorify_commission(&format_cash(&portfolio.currency, portfolio.commissions)));
}
}
fn flatify(assets: Vec<AssetAllocation>, expected_weight: Decimal) -> Vec<AssetAllocation> {
let mut flat_assets = Vec::new();
for mut asset in assets {
asset.expected_weight *= expected_weight;
match asset.holding {
Holding::Stock(_) => {
flat_assets.push(asset);
},
Holding::Group(holdings) => {
flat_assets.extend(flatify(holdings, asset.expected_weight));
},
};
}
flat_assets
}
fn print_assets(mut assets: Vec<AssetAllocation>, expected_total_value: Decimal, currency: &str, depth: usize) {
assets.sort_by_key(|asset: &AssetAllocation| -asset.target_value);
for asset in assets {
print_asset(asset, expected_total_value, currency, depth);
}
}
fn print_asset(asset: AssetAllocation, expected_total_value: Decimal, currency: &str, depth: usize) {
let expected_value = expected_total_value * asset.expected_weight;
let mut buffer = String::new();
write!(&mut buffer, "{bullet:>indent$} {name}",
bullet='•', indent=depth * 2 + 1, name= colorify_title(&asset.full_name())).unwrap();
if asset.buy_blocked {
write!(&mut buffer, " {}", colorify_restriction("[buy blocked]")).unwrap();
}
if asset.sell_blocked {
write!(&mut buffer, " {}", colorify_restriction("[sell blocked]")).unwrap();
}
write!(&mut buffer, " -").unwrap();
if let Holding::Stock(ref holding) = asset.holding {
write!(&mut buffer, " {}", format_shares(holding.current_shares, false)).unwrap();
}
write!(&mut buffer, " {current_weight} ({current_value})",
current_weight=format_weight(get_weight(asset.current_value, expected_total_value)),
current_value=format_cash(currency, asset.current_value)).unwrap();
if asset.target_value != asset.current_value {
if let Holding::Stock(ref holding) = asset.holding {
let colorify_func = if holding.target_shares > holding.current_shares {
colorify_buy
} else {
colorify_sell
};
let shares_change = holding.target_shares - holding.current_shares;
let value_change = asset.target_value - asset.current_value;
let changes = format!(
"{shares_change} ({value_change})",
shares_change=format_shares(shares_change, true),
value_change=format_cash(currency, value_change.abs()));
write!(&mut buffer, " {}", colorify_func(&changes)).unwrap();
}
write!(&mut buffer, " → {target_weight} ({target_value})",
target_weight=format_weight(get_weight(asset.target_value, expected_total_value)),
target_value=format_cash(currency, asset.target_value)).unwrap();
}
write!(&mut buffer, " / {expected_weight} ({expected_value})",
expected_weight=format_weight(asset.expected_weight),
expected_value=format_cash(currency, expected_value)).unwrap();
if let Holding::Group(holdings) = asset.holding {
println!("{}:", buffer);
print_assets(holdings, expected_value, currency, depth + 1);
} else {
println!("{}", buffer);
}
}
fn format_cash(currency: &str, amount: Decimal) -> String {
Cash::new(currency, amount).format_rounded()
}
fn format_shares(shares: Decimal, with_sign: bool) -> String {
let shares = shares.normalize();
let symbol = 's';
if with_sign {
format!("{:+}{}", shares, symbol)
} else {
format!("{}{}", shares, symbol)
}
}
fn get_weight(asset_value: Decimal, expected_total_value: Decimal) -> Decimal {
if expected_total_value.is_zero() {
Decimal::max_value()
} else {
asset_value / expected_total_value
}
}
fn format_weight(weight: Decimal) -> String {
if weight == Decimal::max_value() {
s!("∞")
} else {
format!("{}%", util::round(weight * dec!(100), 1))
}
}
fn colorify_title(name: &str) -> ANSIString {
Style::new().bold().paint(name)
}
fn colorify_restriction(message: &str) -> ANSIString {
Color::Blue.paint(message)
}
fn colorify_buy(message: &str) -> ANSIString {
Color::Green.paint(message)
}
fn colorify_sell(message: &str) -> ANSIString {
Color::Red.paint(message)
}
fn colorify_commission(message: &str) -> ANSIString {
Color::Yellow.paint(message)
}