use anyhow::{ensure, Result};
use crate::{
arguments::Args,
asset::{Allocations, SubAllocations},
holdings::{
AccountHoldings, HoldingType, ShareValues, StockSymbol, VanguardHoldings, VanguardRebalance,
},
};
const HIGH_TO_LOW_RISK: [StockSymbol; 8] = [
StockSymbol::VWO,
StockSymbol::VXUS,
StockSymbol::VB,
StockSymbol::VO,
StockSymbol::VV,
StockSymbol::BNDX,
StockSymbol::VTC,
StockSymbol::VTIP,
];
pub fn to_buy(vanguard_holdings: VanguardHoldings, args: Args) -> Result<VanguardRebalance> {
let mut rebalance = VanguardRebalance::new();
let (traditional_ira_account_option, roth_ira_account_option, target_overall_retirement_option) =
retirement_calc(&vanguard_holdings, args.clone())?;
if let Some(traditional_account) = traditional_ira_account_option {
rebalance.add_account_holdings(traditional_account, HoldingType::TraditionalIra)
}
if let Some(roth_account) = roth_ira_account_option {
rebalance.add_account_holdings(roth_account, HoldingType::RothIra)
}
if let Some(brokerage_holdings) = vanguard_holdings.brokerage_holdings() {
rebalance.add_account_holdings(
brokerage_calc(vanguard_holdings.stock_quotes(), brokerage_holdings, args)?,
HoldingType::Brokerage,
)
}
if let Some(target_overall_retirement) = target_overall_retirement_option {
rebalance.add_retirement_target(target_overall_retirement);
}
Ok(rebalance)
}
fn brokerage_calc(
quotes: ShareValues,
mut brokerage: ShareValues,
args: Args,
) -> Result<AccountHoldings> {
brokerage.add_stock_value(
StockSymbol::VMFXX,
brokerage.stock_value(StockSymbol::VMFXX) + args.brokerage_cash_add,
);
brokerage.add_outside_stock_value(args.brokerage_us_stock_add + args.brokerage_int_stock_add);
brokerage.add_outside_bond_value(args.brokerage_us_bond_add + args.brokerage_int_bond_add);
let asset_allocations = Allocations::custom(
args.percent_stock_brokerage,
args.percent_bond_brokerage,
0.0,
)?;
let sub_allocations = SubAllocations::new_custom(asset_allocations)?;
let target_holdings = ShareValues::new_target(
sub_allocations,
brokerage.total_value(),
args.brokerage_us_stock_add,
args.brokerage_us_bond_add,
args.brokerage_int_stock_add,
args.brokerage_int_bond_add,
);
let difference = target_holdings - brokerage;
let stock_purchase = difference / quotes;
Ok(AccountHoldings::new(
brokerage,
target_holdings,
stock_purchase,
))
}
type TraditionalIraAccount = AccountHoldings;
type RothIraAccount = AccountHoldings;
type TargetOverallRetirement = ShareValues;
fn retirement_calc(
vanguard_holdings: &VanguardHoldings,
args: Args,
) -> Result<(
Option<TraditionalIraAccount>,
Option<RothIraAccount>,
Option<TargetOverallRetirement>,
)> {
let mut traditional_ira_account_option = None;
let mut roth_ira_account_option = None;
let mut target_overall_retirement_option = None;
let mut allocations = Allocations::new();
if let Some(retirement_year) = args.retirement_year_option {
allocations = Allocations::retirement(retirement_year)?;
}
if let Some(stock_percent) = args.percent_stock_retirement_option {
let bond_percent;
if let Some(input_bond_percent) = args.percent_bond_retirement_option {
bond_percent = input_bond_percent;
} else {
bond_percent = 100.0 - stock_percent;
}
allocations = Allocations::custom(stock_percent, bond_percent, 0.0)?;
} else if let Some(bond_percent) = args.percent_bond_retirement_option {
let stock_percent = 100.0 - bond_percent;
allocations = Allocations::custom(stock_percent, bond_percent, 0.0)?;
};
let sub_allocations = SubAllocations::new_custom(allocations)?;
if let Some(mut roth_holdings) = vanguard_holdings.roth_ira_holdings() {
roth_holdings.add_stock_value(
StockSymbol::VMFXX,
roth_holdings.stock_value(StockSymbol::VMFXX) + args.roth_cash_add,
);
if let Some(mut traditional_holdings) = vanguard_holdings.traditional_ira_holdings() {
traditional_holdings.add_stock_value(
StockSymbol::VMFXX,
traditional_holdings.stock_value(StockSymbol::VMFXX) + args.traditional_cash_add,
);
let target_overall_retirement = ShareValues::new_target(
sub_allocations,
roth_holdings.total_value() + traditional_holdings.total_value(),
args.traditional_us_stock_add + args.roth_us_stock_add,
args.traditional_us_bond_add + args.roth_us_bond_add,
args.traditional_int_stock_add + args.roth_int_stock_add,
args.traditional_int_bond_add + args.roth_int_bond_add,
);
target_overall_retirement_option = Some(target_overall_retirement);
let mut roth_total = roth_holdings.total_value();
let mut roth_target = ShareValues::new();
for stock_symbol in HIGH_TO_LOW_RISK {
let value = target_overall_retirement
.stock_value(stock_symbol.clone())
.min(roth_total);
roth_total -= value;
roth_target.add_stock_value(stock_symbol.clone(), value);
if roth_total <= 0.0 {
break;
}
}
ensure!(roth_total == 0.0, "Unexpected leftover roth cash");
ensure!(
roth_target.total_value() > (0.99 * roth_holdings.total_value())
&& roth_target.total_value() < (1.01 * roth_holdings.total_value()),
"Roth target and total do not match\n\nRoth target:\n{}\n\nRoth:\n{}",
roth_target,
roth_holdings
);
let roth_difference = roth_target - roth_holdings;
let roth_purchase = roth_difference / vanguard_holdings.stock_quotes();
let roth_account = AccountHoldings::new(roth_holdings, roth_target, roth_purchase);
let traditional_target = target_overall_retirement - roth_target;
let traditional_difference = traditional_target - traditional_holdings;
let traditional_purchase = traditional_difference / vanguard_holdings.stock_quotes();
let traditional_account = AccountHoldings::new(
traditional_holdings,
traditional_target,
traditional_purchase,
);
roth_ira_account_option = Some(roth_account);
traditional_ira_account_option = Some(traditional_account);
} else {
let roth_target = ShareValues::new_target(
sub_allocations,
roth_holdings.total_value(),
args.roth_us_stock_add,
args.roth_us_bond_add,
args.roth_int_stock_add,
args.roth_int_bond_add,
);
let roth_difference = roth_target - roth_holdings;
let roth_purchase = roth_difference / vanguard_holdings.stock_quotes();
let roth_account = AccountHoldings::new(roth_holdings, roth_target, roth_purchase);
roth_ira_account_option = Some(roth_account);
}
} else if let Some(mut traditional_holdings) = vanguard_holdings.traditional_ira_holdings() {
traditional_holdings.add_stock_value(
StockSymbol::VMFXX,
traditional_holdings.stock_value(StockSymbol::VMFXX) + args.traditional_cash_add,
);
let traditional_target = ShareValues::new_target(
sub_allocations,
traditional_holdings.total_value(),
args.traditional_us_stock_add,
args.traditional_us_bond_add,
args.traditional_int_stock_add,
args.traditional_int_bond_add,
);
let traditional_difference = traditional_target - traditional_holdings;
let traditional_purchase = traditional_difference / vanguard_holdings.stock_quotes();
let traditional_account = AccountHoldings::new(
traditional_holdings,
traditional_target,
traditional_purchase,
);
traditional_ira_account_option = Some(traditional_account)
}
Ok((
traditional_ira_account_option,
roth_ira_account_option,
target_overall_retirement_option,
))
}