Skip to content

Commit

Permalink
Also count I Bounds as US bonds
Browse files Browse the repository at this point in the history
I started buying I Bonds during the years of very high inflation --
since I've no plans to sell these bonds before maturity, count them as
part of my retirement portfolio.

This is a bit hacky, as always, but it works.
  • Loading branch information
DavidCain committed Jun 23, 2024
1 parent a620f76 commit 6d1898b
Show file tree
Hide file tree
Showing 2 changed files with 26 additions and 10 deletions.
4 changes: 4 additions & 0 deletions src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ impl AssetClassifications {
}

pub fn classify(&self, fund_name: &str) -> Result<&AssetClass, UnclassifiedAssetError> {
// Special case -- no need to classify *every* fund as a bond...
if fund_name.starts_with("Series I ") {
return Ok(&AssetClass::USBonds);
}
self.mapping
.get(fund_name)
.ok_or_else(|| UnclassifiedAssetError::new(fund_name))
Expand Down
32 changes: 22 additions & 10 deletions src/gnucash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ impl PriceDatabase {
FROM prices p
JOIN commodities from_c ON p.commodity_guid = from_c.guid
JOIN commodities to_c ON p.currency_guid = to_c.guid
WHERE from_c.namespace = 'FUND'
WHERE from_c.namespace IN ('FUND', 'Series I')
GROUP BY p.commodity_guid;",
)?;

Expand Down Expand Up @@ -638,11 +638,11 @@ impl Account {
fn read_splits_from_sqlite(&mut self, conn: &Connection) -> rusqlite::Result<()> {
let mut stmt = conn.prepare(
"SELECT account_guid,
value_num, value_denom,
quantity_num, quantity_denom
FROM splits
WHERE account_guid = $1
",
value_num, value_denom,
quantity_num, quantity_denom
FROM splits
WHERE account_guid = $1
",
)?;

let splits = stmt.query_map([&self.guid].iter(), |row| {
Expand Down Expand Up @@ -992,20 +992,20 @@ impl Book {
Ok(new_prices)
}

fn get_investment_accounts(conn: &Connection) -> Vec<Account> {
fn get_accounts(conn: &Connection, namespace: &str) -> Vec<Account> {
let mut stmt = conn
.prepare(
"SELECT a.guid, a.name,
-- Commodity for the account
c.guid, c.mnemonic, c.namespace, c.fullname
FROM accounts a
JOIN commodities c ON a.commodity_guid = c.guid
WHERE c.namespace = 'FUND'
WHERE c.namespace = $1
",
)
.expect("Invalid SQL");

stmt.query_map(NO_PARAMS, |row| {
stmt.query_map([namespace], |row| {
let account_guid = row.get(0)?;
let account_name = row.get(1)?;
let commodity =
Expand All @@ -1023,12 +1023,24 @@ impl GnucashFromSqlite for Book {
fn from_sqlite(conn: &Connection, conf: &Config) -> Book {
let mut book = Book::new();

for mut account in Book::get_investment_accounts(conn) {
for mut account in Book::get_accounts(conn, "FUND") {
assert!(account.is_investment());
account.read_splits_from_sqlite(conn).unwrap();
book.add_investment(account);
}

// I Bonds are an interesting case -- they should count as bounds in any
// portfolio, but they also aren't publicly-traded funds (nor is it easy
// to fetch the current value of an I Bond).
//
// To get around all this, I make up ticker names for my I Bonds, then
// just use the Price Editor to input the values from TreasuryDirect.gov
// (every ~year or so, since interest rates are adjusted twice yearly).
for mut account in Book::get_accounts(conn, "Series I") {
account.read_splits_from_sqlite(conn).unwrap();
book.add_investment(account);
}

book.pricedb.populate_from_sqlite(conn).unwrap();
if conf.gnucash.update_prices {
match book.update_commodities(conn) {
Expand Down

0 comments on commit 6d1898b

Please sign in to comment.