1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
use std::collections::hash_map::Entry; use std::rc::Rc; use crate::broker_statement::BrokerStatement; use crate::config::{Config, PortfolioConfig}; use crate::core::EmptyResult; use crate::currency::Cash; use crate::currency::converter::CurrencyConverter; use crate::db; use crate::quotes::Quotes; use crate::types::Decimal; use self::asset_allocation::Portfolio; use self::assets::Assets; use self::formatting::print_portfolio; mod asset_allocation; mod assets; mod formatting; mod rebalancing; pub fn sync(config: &Config, portfolio_name: &str) -> EmptyResult { let portfolio = config.get_portfolio(portfolio_name)?; let broker = portfolio.broker.get_info(config, portfolio.plan.as_ref())?; let database = db::connect(&config.db_path)?; let statement = BrokerStatement::read( broker, &portfolio.statements, &portfolio.symbol_remapping, &portfolio.instrument_names, portfolio.get_tax_remapping()?, false)?; statement.check_date(); let assets = Assets::new(statement.cash_assets, statement.open_positions); assets.validate(&portfolio)?; assets.save(database, &portfolio.name)?; Ok(()) } pub fn buy(config: &Config, portfolio_name: &str, shares: Decimal, symbol: &str, cash_assets: Decimal) -> EmptyResult { modify_assets(config, portfolio_name, |portfolio, assets| { if portfolio.get_stock_symbols().get(symbol).is_none() { return Err!("Unable to buy {}: it's not specified in asset allocation configuration", symbol); } assets.stocks.entry(symbol.to_owned()) .and_modify(|current_shares| *current_shares = (*current_shares + shares).normalize()) .or_insert(shares); set_cash_assets_impl(portfolio, assets, cash_assets) }) } pub fn sell(config: &Config, portfolio_name: &str, shares: Decimal, symbol: &str, cash_assets: Decimal) -> EmptyResult { modify_assets(config, portfolio_name, |portfolio, assets| { let mut entry = match assets.stocks.entry(symbol.to_owned()) { Entry::Occupied(entry) => entry, Entry::Vacant(_) => return Err!("The portfolio has no open {} positions", symbol), }; let current_shares = entry.get_mut(); if shares > *current_shares { return Err!("Unable to sell {} shares of {}: the portfolio contains only {} shares", shares, symbol, current_shares); } if shares == *current_shares { entry.remove(); } else { *current_shares = (*current_shares - shares).normalize(); } set_cash_assets_impl(portfolio, assets, cash_assets) }) } pub fn set_cash_assets(config: &Config, portfolio_name: &str, cash_assets: Decimal) -> EmptyResult { modify_assets(config, portfolio_name, |portfolio, assets| { set_cash_assets_impl(portfolio, assets, cash_assets) }) } fn modify_assets<F>(config: &Config, portfolio_name: &str, modify: F) -> EmptyResult where F: Fn(&PortfolioConfig, &mut Assets) -> EmptyResult { let portfolio = config.get_portfolio(portfolio_name)?; let database = db::connect(&config.db_path)?; let mut assets = Assets::load(database.clone(), &portfolio.name)?; modify(portfolio, &mut assets)?; assets.save(database, &portfolio.name)?; Ok(()) } fn set_cash_assets_impl(portfolio: &PortfolioConfig, assets: &mut Assets, cash_assets: Decimal) -> EmptyResult { assets.cash.clear(); assets.cash.deposit(Cash::new(portfolio.currency()?, cash_assets)); Ok(()) } pub fn show(config: &Config, portfolio_name: &str, flat: bool) -> EmptyResult { process(config, portfolio_name, false, flat) } pub fn rebalance(config: &Config, portfolio_name: &str, flat: bool) -> EmptyResult { process(config, portfolio_name, true, flat) } fn process(config: &Config, portfolio_name: &str, rebalance: bool, flat: bool) -> EmptyResult { let portfolio_config = config.get_portfolio(portfolio_name)?; let database = db::connect(&config.db_path)?; let quotes = Rc::new(Quotes::new(&config, database.clone())?); let converter = CurrencyConverter::new(database.clone(), Some(quotes.clone()), false); let assets = Assets::load(database, &portfolio_config.name)?; assets.validate(&portfolio_config)?; let mut portfolio = Portfolio::load(config, portfolio_config, assets, &converter, "es)?; if rebalance { rebalancing::rebalance_portfolio(&mut portfolio, &converter)?; } print_portfolio(portfolio, flat); Ok(()) }