diff --git a/Cargo.lock b/Cargo.lock index 265a5e4..6498876 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -406,6 +406,7 @@ dependencies = [ "serde 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)", "spectral 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "streaming-stats 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1132,6 +1133,14 @@ name = "stable_deref_trait" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "streaming-stats" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "string" version = "0.1.0" @@ -1666,6 +1675,7 @@ dependencies = [ "checksum socket2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "06dc9f86ee48652b7c80f3d254e3b9accb67a928c562c64d10d7b016d3d98dab" "checksum spectral 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ae3c15181f4b14e52eeaac3efaeec4d2764716ce9c86da0c934c3e318649c5ba" "checksum stable_deref_trait 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "15132e0e364248108c5e2c02e3ab539be8d6f5d52a01ca9bbf27ed657316f02b" +"checksum streaming-stats 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4f233aa550ceeb22c47cff12e167f7bc89c03e265e7fcff64b8359bb6799e0f4" "checksum string 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "31f98b200e7caca9efca50fc0aa69cd58a5ec81d5f6e75b2f3ecaad2e998972a" "checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" "checksum syn 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c67da57e61ebc7b7b6fff56bb34440ca3a83db037320b0507af4c10368deda7d" diff --git a/Cargo.toml b/Cargo.toml index 6b41883..a1eb4bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ actix-web = "0.6.10" cute = "0.3.0" serde = "1.0" serde_derive = "1.0" +streaming-stats = "0.2" [dev-dependencies] spectral = "0.6.0" diff --git a/src/accounts/balancer.rs b/src/accounts/balancer.rs index cd8105e..e67fe41 100644 --- a/src/accounts/balancer.rs +++ b/src/accounts/balancer.rs @@ -1,11 +1,13 @@ +use stats::median; use super::*; pub fn run_balancing(portfolio: Portfolio) -> Results { let total_value = portfolio.total_value(); - let prices = c!{ &i.symbol => i.price, for i in portfolio.market.iter() }; let allocations = c!{ s => w * total_value, for (s, w) in portfolio.target.iter() }; let total_shares = portfolio.total_shares(); // Portfolio::validate has already checked for the necessary prices + let prices = c!{ &i.symbol => i.price, for i in portfolio.market.iter() }; + let yields = c!{ &i.symbol => i.div_yield.unwrap_or(0.0), for i in portfolio.market.iter() }; let cash_delta = c!{ s => a - total_shares.get(s).unwrap_or(&0.0) * prices.get(s).unwrap(), for (s, a) in allocations }; @@ -17,6 +19,8 @@ pub fn run_balancing(portfolio: Portfolio) -> Results { symbols_by_price.sort_by(|(_, a), (_, b)| (b.round() as i32).cmp(&(a.round() as i32))); let symbols_by_price: Vec<&String> = symbols_by_price.iter().map(|(s, _)| *s).collect(); + let median_yield = median(portfolio.market.iter().map(|i| i.div_yield.unwrap_or(0.0))); + let mut accounts = portfolio.accounts.to_vec(); accounts.sort_by(|a, b| b.tax_sheltered.cmp(&a.tax_sheltered)); // sheltered accounts first @@ -24,6 +28,7 @@ pub fn run_balancing(portfolio: Portfolio) -> Results { println!("Accounts before action: {:?}", results.positions); println!("Shares delta before action: {:?}", shares_delta); + println!("Median yield={:?}, yields={:?}", median_yield, yields); // first sell shares we're overweight in for (sym, delta) in shares_delta.iter_mut() { @@ -64,14 +69,49 @@ pub fn run_balancing(portfolio: Portfolio) -> Results { loop { let mut none_left = true; - // buy some of each share we need more of + // first allocate new 'high yield' shares into tax-sheltered accounts for (sym, shares) in shares_delta.iter_mut() { + if *shares < 1.0 { + continue; + } + match yields.get(*sym) { + Some(div_yield) => + match median_yield { + Some(median) if *div_yield as f64 > median => (), // success! + _ => continue + } + _ => continue + } + // if we got here the fund is higher-than-median yield let price = *prices.get(*sym).expect("unexpected missing price"); for account in accounts.iter() { - if *shares < 1.0 { + if !account.tax_sheltered { continue; } + match results.buy_maybe(&account.name, sym, price, 1.0) { + Some(_) => { + none_left = false; + *shares -= 1.0; + println!("high-yield: acct={}, bought {}@{}, fc={:?}", account.name, sym, price, results.cash); + } + _ => () + } + } + } + + if !none_left { + continue; + } + + // buy some of each share we need more of + for (sym, shares) in shares_delta.iter_mut() { + if *shares < 1.0 { + continue; + } + let price = *prices.get(*sym).expect("unexpected missing price"); + + for account in accounts.iter() { match results.buy_maybe(&account.name, sym, price, 1.0) { Some(_) => { none_left = false; @@ -97,7 +137,7 @@ pub fn run_balancing(portfolio: Portfolio) -> Results { Some(_) => { none_left = false; println!("extra: acct={}, bought {}@{}, fc={:?}", account.name, sym, price, results.cash); - break; + break; // we found an account to hold the extra share, move on to next fund } _ => () } @@ -321,4 +361,23 @@ mod multiple_accounts { check_shares(&r, "taxed", "B", 0.0); check_shares(&r, "ira", "B", 50.0); } + + #[test] + fn high_yield_tax_sheltered() { + let mut p = build_multi_portfolio(); + p.market.index_mut(0).div_yield = Some(0.04); // A + p.market.index_mut(1).div_yield = Some(0.01); // B + + let r = run_balancing(p); + + assert_that(&r.total_cash).is_close_to(0.0, 0.1); + check_shares(&r, "taxed", "A", 300.0); + check_shares(&r, "ira", "A", 200.0); + + check_shares(&r, "taxed", "B", 50.0); + check_shares(&r, "ira", "B", 0.0); // IRA ends up entirely holding high-yield + + check_allocation(&r, "A", 0.5); + check_allocation(&r, "B", 0.5); + } } diff --git a/src/lib.rs b/src/lib.rs index d3ea17f..cfa4d76 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,8 @@ extern crate serde_derive; #[macro_use(c)] extern crate cute; +extern crate stats; + #[cfg(test)] extern crate spectral;