use custom_error::custom_error;
use std::{
collections::HashMap,
error::Error,
fmt,
fs::File,
io::{BufRead, BufReader},
ops::{Sub, Add, Div},
vec::Vec,
};
const US_STOCK_FRACTION: f32 = 2.0 / 3.0;
const EACH_US_STOCK: f32 = US_STOCK_FRACTION / 3.0;
const INT_STOCK_FRACTION: f32 = 1.0 / 3.0;
const INT_EMERGING: f32 = INT_STOCK_FRACTION / 3.0;
const INT_TOTAL: f32 = INT_STOCK_FRACTION * 2.0 / 3.0;
const US_BOND_FRACTION: f32 = 2.0 / 3.0;
const US_CORP_BOND_FRACTION: f32 = US_BOND_FRACTION / 2.0;
const US_TOT_BOND_FRACTION: f32 = US_BOND_FRACTION / 2.0;
const INT_BOND_FRACTION: f32 = 1.0 / 3.0;
lazy_static! {
static ref STOCK_DESCRIPTION: HashMap<StockSymbol, &'static str> = {
let mut m = HashMap::new();
m.insert(StockSymbol::VV, "US large cap");
m.insert(StockSymbol::VO, "US mid cap");
m.insert(StockSymbol::VB, "US small cap");
m.insert(StockSymbol::VTC, "US total corporate bond");
m.insert(StockSymbol::BND, "US total bond");
m.insert(StockSymbol::VXUS, "Total international stock");
m.insert(StockSymbol::VWO, "Emerging markets stock");
m.insert(StockSymbol::BNDX, "Total international bond");
m
};
}
#[derive(Clone, Eq, Hash, PartialEq, Debug)]
pub enum StockSymbol {
VXUS,
BNDX,
BND,
VWO,
VO,
VB,
VTC,
VV,
VMFXX,
Empty,
Other(String),
}
impl StockSymbol {
pub fn new(symbol: &str) -> Self {
match symbol {
"VXUS" => StockSymbol::VXUS,
"BNDX" => StockSymbol::BNDX,
"BND" => StockSymbol::BND,
"VWO" => StockSymbol::VWO,
"VO" => StockSymbol::VO,
"VB" => StockSymbol::VB,
"VTC" => StockSymbol::VTC,
"VV" => StockSymbol::VV,
"VMFXX" => StockSymbol::VMFXX,
_ => {
eprintln!("{} is not supported within this algorithm\n", symbol);
StockSymbol::Other(symbol.to_string())
}
}
}
pub fn description(&self) -> String {
let description_option = STOCK_DESCRIPTION.get(self);
if let Some(description) = description_option {
return format!("{:?}: {}", self, description);
} else {
return format!("No description for {:?}", self);
}
}
}
pub fn all_stock_descriptions() -> String {
let mut descriptions = String::new();
for symbol in [
StockSymbol::VV,
StockSymbol::VO,
StockSymbol::VB,
StockSymbol::VTC,
StockSymbol::BND,
StockSymbol::VXUS,
StockSymbol::VWO,
StockSymbol::BNDX,
] {
descriptions.push_str(&symbol.description());
descriptions.push('\n')
}
descriptions.pop();
descriptions
}
#[derive(Clone)]
pub struct StockInfo {
pub account_number: u32,
pub symbol: StockSymbol,
pub share_price: f32,
pub total_value: f32,
account_added: bool,
symbol_added: bool,
share_price_added: bool,
total_value_added: bool,
}
impl StockInfo {
pub fn new() -> Self {
StockInfo {
account_number: 0,
symbol: StockSymbol::Empty,
share_price: 0.0,
total_value: 0.0,
account_added: false,
symbol_added: false,
share_price_added: false,
total_value_added: false,
}
}
pub fn add_account(&mut self, account_number: u32) {
self.account_number = account_number;
self.account_added = true;
}
pub fn add_symbol(&mut self, symbol: StockSymbol) {
self.symbol = symbol;
self.symbol_added = true;
}
pub fn add_share_price(&mut self, share_price: f32) {
self.share_price = share_price;
self.share_price_added = true;
}
pub fn add_total_value(&mut self, total_value: f32) {
self.total_value = total_value;
self.total_value_added = true;
}
pub fn finished(&self) -> bool {
[
self.account_added,
self.symbol_added,
self.share_price_added,
self.total_value_added,
]
.iter()
.all(|value| *value)
}
}
impl Default for StockInfo {
fn default() -> Self {
Self::new()
}
}
pub enum AddType {
StockPrice,
HoldingValue,
}
#[derive(Clone, PartialEq, Debug, Copy)]
pub struct ShareValues {
vxus: f32,
bndx: f32,
bnd: f32,
vwo: f32,
vo: f32,
vb: f32,
vtc: f32,
vv: f32,
vmfxx: f32,
}
impl ShareValues {
pub fn new() -> Self {
ShareValues {
vxus: 0.0,
bndx: 0.0,
bnd: 0.0,
vwo: 0.0,
vo: 0.0,
vb: 0.0,
vtc: 0.0,
vv: 0.0,
vmfxx: 0.0,
}
}
pub fn new_quote() -> Self {
ShareValues {
vxus: 1.0,
bndx: 1.0,
bnd: 1.0,
vwo: 1.0,
vo: 1.0,
vb: 1.0,
vtc: 1.0,
vv: 1.0,
vmfxx: 1.0,
}
}
pub fn new_target(
total_vanguard_value: f32,
percent_bond: f32,
percent_stock: f32,
other_us_stock_value: f32,
other_us_bond_value: f32,
other_int_bond_value: f32,
other_int_stock_value: f32,
) -> Self {
let total_percent = INT_TOTAL * percent_stock / 100.0
+ INT_BOND_FRACTION * percent_bond / 100.0
+ INT_EMERGING * percent_stock / 100.0
+ EACH_US_STOCK * percent_stock / 100.0
+ EACH_US_STOCK * percent_stock / 100.0
+ US_CORP_BOND_FRACTION * percent_bond / 100.0
+ US_CORP_BOND_FRACTION * percent_bond / 100.0
+ EACH_US_STOCK * percent_stock / 100.0;
assert!((0.999..1.001).contains(&total_percent), "Fractions did not add up for brokerage account. The bond to stock ratio is likely off and should add up to 100");
let total_value = total_vanguard_value
+ other_us_stock_value
+ other_us_bond_value
+ other_int_bond_value
+ other_int_stock_value;
let vxus_value =
(total_value * INT_TOTAL * percent_stock / 100.0) - (other_int_stock_value * 2.0 / 3.0);
let bndx_value =
(total_value * INT_BOND_FRACTION * percent_bond / 100.0) - other_int_bond_value;
let bnd_value = (total_value * US_TOT_BOND_FRACTION * percent_bond / 100.0)
- (other_us_bond_value / 2.0);
let vwo_value =
(total_value * INT_EMERGING * percent_stock / 100.0) - (other_int_stock_value / 3.0);
let vo_value =
(total_value * EACH_US_STOCK * percent_stock / 100.0) - (other_us_stock_value / 3.0);
let vb_value =
(total_value * EACH_US_STOCK * percent_stock / 100.0) - (other_us_stock_value / 3.0);
let vtc_value = (total_value * US_CORP_BOND_FRACTION * percent_bond / 100.0)
- (other_us_bond_value / 2.0);
let vv_value =
(total_value * EACH_US_STOCK * percent_stock / 100.0) - (other_us_stock_value / 3.0);
ShareValues {
vxus: vxus_value,
bndx: bndx_value,
bnd: bnd_value,
vwo: vwo_value,
vo: vo_value,
vb: vb_value,
vtc: vtc_value,
vv: vv_value,
vmfxx: 0.0,
}
}
pub fn add_stockinfo_value(&mut self, stock_info: StockInfo, add_type: AddType) {
let value;
match add_type {
AddType::StockPrice => value = stock_info.share_price,
AddType::HoldingValue => value = stock_info.total_value,
}
match stock_info.symbol {
StockSymbol::VXUS => self.vxus = value,
StockSymbol::BNDX => self.bndx = value,
StockSymbol::BND => self.bnd = value,
StockSymbol::VWO => self.vwo = value,
StockSymbol::VO => self.vo = value,
StockSymbol::VB => self.vb = value,
StockSymbol::VTC => self.vtc = value,
StockSymbol::VV => self.vv = value,
StockSymbol::VMFXX => self.vmfxx = value,
StockSymbol::Empty => panic!("Stock symbol not set before adding value"),
StockSymbol::Other(_) => (),
}
}
pub fn add_stock_value(&mut self, stock_symbol: StockSymbol, value: f32) {
match stock_symbol {
StockSymbol::VXUS => self.vxus = value,
StockSymbol::BNDX => self.bndx = value,
StockSymbol::BND => self.bnd = value,
StockSymbol::VWO => self.vwo = value,
StockSymbol::VO => self.vo = value,
StockSymbol::VB => self.vb = value,
StockSymbol::VTC => self.vtc = value,
StockSymbol::VV => self.vv = value,
StockSymbol::VMFXX => self.vmfxx = value,
StockSymbol::Empty => panic!("Stock symbol not set before adding value"),
StockSymbol::Other(_) => (),
}
}
pub fn stock_value(&self, stock_symbol: StockSymbol) -> f32 {
match stock_symbol {
StockSymbol::VXUS => self.vxus,
StockSymbol::BNDX => self.bndx,
StockSymbol::BND => self.bnd,
StockSymbol::VWO => self.vwo,
StockSymbol::VO => self.vo,
StockSymbol::VB => self.vb,
StockSymbol::VTC => self.vtc,
StockSymbol::VV => self.vv,
StockSymbol::VMFXX => self.vmfxx,
StockSymbol::Empty => panic!("Value retrieval not supported for empty stock symbol"),
StockSymbol::Other(symbol) => panic!("Value retrieval not supported for {}", symbol),
}
}
pub fn total_value(&self) -> f32 {
self.vxus
+ self.bndx
+ self.bnd
+ self.vwo
+ self.vo
+ self.vb
+ self.vtc
+ self.vv
+ self.vmfxx
}
pub fn percent_stock_bond(&self, additional_stock: Option<f32>, additional_bond: Option<f32>) -> (f32, f32) {
let mut total_bond = self.bndx + self.bnd + self.vtc;
let mut total_stock = self.vwo + self.vo + self.vb + self.vv + self.vxus;
let mut total = self.total_value() - self.vmfxx;
if let Some(add_stock) = additional_stock {
total_stock += add_stock;
total += add_stock;
}
if let Some(add_bond) = additional_bond {
total_bond += add_bond;
total += add_bond;
}
(total_stock / total * 100.0, total_bond / total * 100.0)
}
}
impl Default for ShareValues {
fn default() -> Self {
Self::new()
}
}
impl Add for ShareValues {
type Output = ShareValues;
fn add(self, other: ShareValues) -> ShareValues {
ShareValues {
vxus: self.vxus + other.vxus,
bndx: self.bndx + other.bndx,
bnd: self.bnd + other.bnd,
vwo: self.vwo + other.vwo,
vo: self.vo + other.vo,
vb: self.vb + other.vb,
vtc: self.vtc + other.vtc,
vv: self.vv + other.vv,
vmfxx: self.vmfxx + other.vmfxx,
}
}
}
impl Sub for ShareValues {
type Output = ShareValues;
fn sub(self, other: ShareValues) -> ShareValues {
ShareValues {
vxus: self.vxus - other.vxus,
bndx: self.bndx - other.bndx,
bnd: self.bnd - other.bnd,
vwo: self.vwo - other.vwo,
vo: self.vo - other.vo,
vb: self.vb - other.vb,
vtc: self.vtc - other.vtc,
vv: self.vv - other.vv,
vmfxx: self.vmfxx - other.vmfxx,
}
}
}
impl Div for ShareValues {
type Output = ShareValues;
fn div(self, other: ShareValues) -> ShareValues {
ShareValues {
vxus: self.vxus / other.vxus,
bndx: self.bndx / other.bndx,
bnd: self.bnd / other.bnd,
vwo: self.vwo / other.vwo,
vo: self.vo / other.vo,
vb: self.vb / other.vb,
vtc: self.vtc / other.vtc,
vv: self.vv / other.vv,
vmfxx: self.vmfxx / other.vmfxx,
}
}
}
impl fmt::Display for ShareValues {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (stock, bond) = self.percent_stock_bond(None, None);
write!(
f,
"\
Symbol Value\n\
--------------------\n\
VV {:.2}\n\
VO {:.2}\n\
VB {:.2}\n\
VTC {:.2}\n\
BND {:.2}\n\
VXUS {:.2}\n\
VWO {:.2}\n\
BNDX {:.2}\n\
--------------------\n\
Cash {:.2}\n\
Total {:.2}\n\
Stock:Bond {:.1}:{:.1}\n\
====================
",
self.vv,
self.vo,
self.vb,
self.vtc,
self.bnd,
self.vxus,
self.vwo,
self.bndx,
self.vmfxx,
self.total_value(),
stock,
bond
)
}
}
pub enum HoldingType {
Brokerage,
TraditionalIra,
RothIra,
}
#[derive(Clone)]
pub struct VanguardHoldings {
brokerage: Option<ShareValues>,
traditional_ira: Option<ShareValues>,
roth_ira: Option<ShareValues>,
quotes: ShareValues,
}
impl VanguardHoldings {
pub fn new(quotes: ShareValues) -> Self {
VanguardHoldings {
brokerage: None,
traditional_ira: None,
roth_ira: None,
quotes,
}
}
pub fn add_holding(&mut self, holding: ShareValues, holding_type: HoldingType) {
match holding_type {
HoldingType::RothIra => self.roth_ira = Some(holding),
HoldingType::Brokerage => self.brokerage = Some(holding),
HoldingType::TraditionalIra => self.traditional_ira = Some(holding),
}
}
pub fn brokerage_holdings(&self) -> Option<ShareValues> {
self.brokerage.clone()
}
pub fn traditional_ira_holdings(&self) -> Option<ShareValues> {
self.traditional_ira.clone()
}
pub fn roth_ira_holdings(&self) -> Option<ShareValues> {
self.roth_ira.clone()
}
pub fn stock_quotes(&self) -> ShareValues {
self.quotes.clone()
}
}
pub struct AccountHoldings {
current: ShareValues,
target: ShareValues,
sale_purchases_needed: ShareValues,
}
impl AccountHoldings {
pub fn new(
current: ShareValues,
target: ShareValues,
sale_purchases_needed: ShareValues,
) -> Self {
AccountHoldings {
current,
target,
sale_purchases_needed,
}
}
}
impl fmt::Display for AccountHoldings {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (current_stock, current_bond) = self.current.percent_stock_bond(None, None);
let current_stock_bond = format!("{:.1}:{:.1}", current_stock, current_bond);
let (target_stock, target_bond) = self.target.percent_stock_bond(None, None);
let target_stock_bond = format!("{:.1}:{:.1}", target_stock, target_bond);
write!(
f,
"Symbol Purchase/Sell Current Target\n\
--------------------------------------------------\n\
VV {:<15.2}${:<15.2}${:<15.2}\n\
VO {:<15.2}${:<15.2}${:<15.2}\n\
VB {:<15.2}${:<15.2}${:<15.2}\n\
VTC {:<15.2}${:<15.2}${:<15.2}\n\
BND {:<15.2}${:<15.2}${:<15.2}\n\
VXUS {:<15.2}${:<15.2}${:<15.2}\n\
VWO {:<15.2}${:<15.2}${:<15.2}\n\
BNDX {:<15.2}${:<15.2}${:<15.2}\n\
--------------------------------------------------\n\
Cash ${:<15.2}${:<15.2}\n\
Total ${:<15.2}\n\
Stock:Bond {:<16}{:<15}\n\
==================================================",
self.sale_purchases_needed.vv,
self.current.vv,
self.target.vv,
self.sale_purchases_needed.vo,
self.current.vo,
self.target.vo,
self.sale_purchases_needed.vb,
self.current.vb,
self.target.vb,
self.sale_purchases_needed.vtc,
self.current.vtc,
self.target.vtc,
self.sale_purchases_needed.bnd,
self.current.bnd,
self.target.bnd,
self.sale_purchases_needed.vxus,
self.current.vxus,
self.target.vxus,
self.sale_purchases_needed.vwo,
self.current.vwo,
self.target.vwo,
self.sale_purchases_needed.bndx,
self.current.bndx,
self.target.bndx,
self.current.vmfxx,
self.target.vmfxx,
self.current.total_value(),
current_stock_bond,
target_stock_bond,
)
}
}
pub struct VanguardRebalance {
brokerage: Option<AccountHoldings>,
traditional_ira: Option<AccountHoldings>,
roth_ira: Option<AccountHoldings>,
retirement_target: Option<ShareValues>
}
impl VanguardRebalance {
pub fn new() -> Self {
VanguardRebalance {
brokerage: None,
traditional_ira: None,
roth_ira: None,
retirement_target: None,
}
}
pub fn add_account_holdings(&mut self, acct_holding: AccountHoldings, acct_type: HoldingType) {
match acct_type {
HoldingType::Brokerage => self.brokerage = Some(acct_holding),
HoldingType::TraditionalIra => self.traditional_ira = Some(acct_holding),
HoldingType::RothIra => self.roth_ira = Some(acct_holding),
}
}
pub fn add_retirement_target(&mut self, retirement_target: ShareValues) {
self.retirement_target = Some(retirement_target);
}
}
impl Default for VanguardRebalance {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for VanguardRebalance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut out_string = String::new();
if let Some(retirement_target_values) = &self.retirement_target {
out_string.push_str(&format!(
"Retirement target:\n{}\n\n",
retirement_target_values
))
}
if let Some(traditional_ira_account) = &self.traditional_ira {
out_string.push_str(&format!(
"Traditional IRA:\n{}\n\n",
traditional_ira_account
))
}
if let Some(roth_ira_account) = &self.roth_ira {
out_string.push_str(&format!("Roth IRA:\n{}\n\n", roth_ira_account))
}
if let Some(brokerage_account) = &self.brokerage {
out_string.push_str(&format!("Brokerage:\n{}\n\n", brokerage_account))
}
write!(f, "{}", out_string.trim_end_matches('\n'))
}
}
custom_error! {AccountNumberError
Brokerage = "Brokerage account number not found within vanguard download file",
TraditionIra = "Traditional IRA account number not found within vanguard download file",
RothIra = "Roth IRA account number not found within vanguard download file",
}
pub fn parse_csv_download(
csv_path: &str,
args: crate::arguments::Args,
) -> Result<VanguardHoldings, Box<dyn Error>> {
let mut header = Vec::new();
let csv_file = File::open(csv_path)?;
let mut accounts: HashMap<u32, ShareValues> = HashMap::new();
let mut quotes = ShareValues::new_quote();
for row_result in BufReader::new(csv_file).lines() {
let row = row_result?;
if row.contains(',') {
if row.contains("Trade Date") {
break;
}
let row_split = row
.split(',')
.map(|value| value.to_string())
.collect::<Vec<String>>();
let mut stock_info = StockInfo::new();
if header.is_empty() {
header = row_split
} else {
for (value, head) in row_split.iter().zip(&header) {
match head.as_str() {
"Account Number" => stock_info.add_account(value.parse::<u32>()?),
"Symbol" => stock_info.add_symbol(StockSymbol::new(value)),
"Share Price" => stock_info.add_share_price(value.parse::<f32>()?),
"Total Value" => stock_info.add_total_value(value.parse::<f32>()?),
_ => continue,
}
}
if stock_info.finished() {
let account_value = accounts
.entry(stock_info.account_number)
.or_insert_with(ShareValues::new);
account_value.add_stockinfo_value(stock_info.clone(), AddType::HoldingValue);
quotes.add_stockinfo_value(stock_info, AddType::StockPrice);
}
}
}
}
let mut brokerage = None;
if let Some(brokerage_acct) = args.brok_acct_option {
if let Some(brokerage_holdings) = accounts.get(&brokerage_acct) {
brokerage = Some(brokerage_holdings.clone())
} else {
return Err(Box::new(AccountNumberError::Brokerage));
}
}
let mut traditional_ira = None;
if let Some(traditional_acct) = args.trad_acct_option {
if let Some(traditional_holdings) = accounts.get(&traditional_acct) {
traditional_ira = Some(traditional_holdings.clone())
} else {
return Err(Box::new(AccountNumberError::TraditionIra));
}
}
let mut roth_ira = None;
if let Some(roth_acct) = args.roth_acct_option {
if let Some(roth_holdings) = accounts.get(&roth_acct) {
roth_ira = Some(roth_holdings.clone())
} else {
return Err(Box::new(AccountNumberError::RothIra));
}
}
Ok(VanguardHoldings {
brokerage,
traditional_ira,
roth_ira,
quotes,
})
}