diff --git a/Cargo.lock b/Cargo.lock index 2a72dd9..1a68503 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,6 +45,54 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "anstream" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "arrayvec" version = "0.7.4" @@ -177,6 +225,46 @@ dependencies = [ "windows-targets 0.52.4", ] +[[package]] +name = "clap" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + [[package]] name = "color-rs" version = "0.7.1" @@ -190,6 +278,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -285,6 +379,12 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -366,6 +466,7 @@ name = "ledgerneo" version = "0.1.0" dependencies = [ "chrono", + "clap", "icu_locid", "ledger-parser", "rust_decimal", @@ -828,6 +929,12 @@ dependencies = [ "serde", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "1.0.109" @@ -934,6 +1041,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "1.4.1" @@ -1018,6 +1131,15 @@ dependencies = [ "windows-targets 0.52.4", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.4", +] + [[package]] name = "windows-targets" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index 9d76bd0..560e359 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,5 @@ ledger-parser = {path="./rust-ledger-parser"} rust_decimal = "1.29.0" spreadsheet-ods = "0.15.0" chrono = "0.4.24" -icu_locid = "1.1.0" \ No newline at end of file +icu_locid = "1.1.0" +clap = { version = "4.0", features = ["derive"] } \ No newline at end of file diff --git a/src/asset_account.rs b/src/asset_account.rs index 8c576f4..b040621 100644 --- a/src/asset_account.rs +++ b/src/asset_account.rs @@ -1,11 +1,8 @@ -use std::borrow::Borrow; -use crate::{CommoditiesPriceOracle, OPENING_DATE}; +use crate::{CommoditiesPriceOracle}; use rust_decimal::Decimal; use std::collections::HashMap; use std::ops::Neg; -use chrono::Month::December; use rust_decimal::prelude::ToPrimitive; -use spreadsheet_ods::text::Date; use crate::books::TransactionReference; #[derive(Clone)] @@ -30,10 +27,11 @@ pub struct AssetAccount { pub(crate) transactions: Vec<(TransactionReference, String, String, Decimal, Decimal, String)>, opening: HashMap, pub(crate) realized_gains: Decimal, + opening_date: String } impl AssetAccount { - pub fn new(name: String) -> AssetAccount { + pub fn new(name: String, opening_date: &String) -> AssetAccount { AssetAccount { name, commodities: HashMap::new(), @@ -41,6 +39,7 @@ impl AssetAccount { running_total: HashMap::new(), transactions: vec![], opening: HashMap::new(), + opening_date: opening_date.clone() } } @@ -48,13 +47,16 @@ impl AssetAccount { let mut totals = vec![]; for (name, commodities) in self.commodities.iter() { if name != "CAD" && name != "$" && name != "StickerSheets" { + println!("[UG] Calculating Unrealized Gains for {}", self.name); let mut unrealized_gain = Decimal::new(0, 0); let historical_cost = commodities.iter().fold(Decimal::new(0,0), |accum, x| { + println!("[UG] Historial {} {}", x.cost_basis , x.quantity); accum + (x.cost_basis * x.quantity) }); let current_cost = commodities.iter().fold(Decimal::new(0,0), |accum, x| { + println!("[UG] Current {} {}", oracle.lookup(&x.name, date) , x.quantity); accum + (oracle.lookup(&x.name, date) * x.quantity) }); @@ -107,7 +109,7 @@ impl AssetAccount { } - pub fn trade(&mut self, txid: TransactionReference, name: String, cost: Decimal, quantity: Decimal, date: &String, oracle: &CommoditiesPriceOracle) -> Vec<(String, Decimal, String, Decimal)>{ + pub fn trade(&mut self, txid: TransactionReference, name: String, cost: Decimal, quantity: Decimal, date: &String, oracle: &mut CommoditiesPriceOracle) -> Vec<(String, Decimal, String, Decimal)>{ let cost_basis = cost; let asset_commodity = AssetCommodity { cost_basis, @@ -119,10 +121,10 @@ impl AssetAccount { false => { self.commodities.insert(name.clone(), vec![]); self.running_total.insert(name.clone(), Decimal::new(0,0)); - if date == OPENING_DATE { - println!("Opening Total: {} {} {}", quantity, name, cost); - self.opening.insert(name.clone(), quantity); - } + if *date == self.opening_date { + println!("Opening Total: {} {} {}", quantity, name, cost); + self.opening.insert(name.clone(), quantity); + } } _ => { } @@ -153,7 +155,6 @@ impl AssetAccount { let mut quantity_needed = quantity.abs(); let mut base_cost = Decimal::new(0,0); - if name != "$" { for commodity in current_commodities.iter_mut() { if commodity.quantity.is_zero() { @@ -162,13 +163,13 @@ impl AssetAccount { if commodity.quantity <= quantity_needed { // use all of this commodity.... println!("Taking {} {} @@ {}", commodity.quantity, commodity.name, commodity.cost_basis); - base_cost += (commodity.quantity * commodity.cost_basis); + base_cost += commodity.quantity * commodity.cost_basis; quantity_needed -= commodity.quantity; commodity.quantity = Decimal::new(0, 0); } else { // only use what we need from this...and then we are done... println!("Taking {} {} @@ {}", quantity_needed, commodity.name, commodity.cost_basis); - base_cost += (quantity_needed * commodity.cost_basis); + base_cost += quantity_needed * commodity.cost_basis; commodity.quantity -= quantity_needed; break; } @@ -176,19 +177,33 @@ impl AssetAccount { break; } } - let oracle_price = oracle.lookup(&name, date); - let arbitrage_difference = oracle_price - cost; + let mut oracle_price = oracle.lookup(&name, date); + let mut buying = false; + let arbitrage_difference = if oracle_price == Decimal::new(1,0) { + buying = true; + println!("[WARNING] NO Price found for {} {} {}", name, date, cost_basis); + // oracle.insert(name.clone(), date.clone(), cost_basis); + Decimal::new(0,0) + } else { + oracle_price - cost + }; + if (base_cost - (cost * quantity.abs())).is_zero() == false { println!("Realized Gains: {} {} {} {} {} {} {} {} {}", self.name, (cost * quantity.abs()) - base_cost, base_cost, cost * quantity.abs(), date, base_cost/quantity, cost, oracle_price, arbitrage_difference); } let arbitrage_expense = (arbitrage_difference * quantity).neg(); - let actual_gain = ((cost * quantity.abs()) - base_cost) + arbitrage_expense; + let mut actual_gain = ((cost * quantity.abs()) - base_cost) + arbitrage_expense; + if buying { + actual_gain = Decimal::new(0,0); + } self.realized_gains += actual_gain; + println!("[DEBUG] {} {} oracle price: {} cost: {} quantity: {} ae: {} {} ", name, base_cost, oracle_price, cost, quantity, arbitrage_expense, actual_gain); + self.transactions.push((txid, date.clone(), name.clone(), cost, quantity, self.name.clone())); let mut meta_transactions = vec![]; - meta_transactions.push((self.name.clone(),cost*quantity, name.clone(), base_cost.neg())); + meta_transactions.push((self.name.clone(),cost*quantity, name.clone(), base_cost.neg() )); if actual_gain.is_zero() == false { if actual_gain.is_sign_positive() { self.transactions.push((txid, date.clone(), format!("$"), Decimal::new(1, 0), actual_gain.neg(), format!("Income:RealizedGains"))); @@ -211,22 +226,22 @@ impl AssetAccount { return meta_transactions; } else { + println!("[UG] DEBUG ASSET TRADE {} {} {}", cost, quantity, name); current_commodities.push(AssetCommodity{ - cost_basis: Decimal::new(1,0), + cost_basis: cost_basis * quantity, quantity: quantity, name: name.clone() }); let mut meta_transactions = vec![]; self.transactions.push((txid, date.clone(), name.clone(), cost, quantity, self.name.clone())); - meta_transactions.push((self.name.clone(), cost*quantity, name.clone(), cost*quantity)); + meta_transactions.push((self.name.clone(), quantity, name.clone(), cost*quantity)); return meta_transactions; } } } } - pub fn deposit(&mut self, txid: TransactionReference, name: String, cost: Decimal, quantity: Decimal, date: &String) { - let cost_basis = cost; + pub fn deposit(&mut self, txid: TransactionReference, name: String, cost: Decimal, quantity: Decimal, date: &String, cost_basis: Decimal) { let asset_commodity = AssetCommodity { cost_basis, quantity, @@ -237,10 +252,10 @@ impl AssetAccount { None => { self.commodities.insert(name.clone(), vec![asset_commodity]); self.running_total.insert(name.clone(), Decimal::new(0,0)); - if date == OPENING_DATE { - println!("Opening: {} {} {}", quantity, name, cost); - self.opening.insert(name.clone(), quantity); - } + if *date == self.opening_date { + println!("Opening: {} {} {}", quantity, name, cost); + self.opening.insert(name.clone(), quantity); + } } Some(account) => account.push(asset_commodity), } diff --git a/src/books.rs b/src/books.rs index 55f9978..c6a3342 100644 --- a/src/books.rs +++ b/src/books.rs @@ -14,15 +14,19 @@ pub struct Books { pub(crate) external_accounts: HashMap, pub(crate) commodities_oracle: CommoditiesPriceOracle, pub general_ledger: Vec<(TransactionReference, String, Vec<(String, String, Decimal, Decimal)>)>, + opening_date: String, + closing_date: String } impl Books { - pub fn new() -> Books { + pub fn new(opening_date: &String, closing_date: &String) -> Books { Books { asset_accounts: HashMap::new(), external_accounts: HashMap::new(), commodities_oracle: CommoditiesPriceOracle::new(), general_ledger: vec![], + opening_date: opening_date.clone(), + closing_date: closing_date.clone() } } @@ -126,7 +130,7 @@ impl Books { false => { self.asset_accounts.insert( account_name.clone(), - AssetAccount::new(account_name.clone()), + AssetAccount::new(account_name.clone(), &self.opening_date), ); } _ => {} @@ -135,7 +139,7 @@ impl Books { let cost_basis = self.commodities_oracle.lookup(commodity_name, tx_date); if commodity_quantity.is_sign_positive() { - asset_account.deposit( tx_ref, commodity_name.clone(), cost_basis, commodity_quantity, tx_date); + asset_account.deposit( tx_ref, commodity_name.clone(), cost_basis, commodity_quantity, tx_date, Decimal::new(1,0)); general_ledger_entry.push((account_name.clone(), commodity_name.clone(), commodity_quantity, commodity_quantity*cost_basis)); } else { let extra_transactions = asset_account.trade( @@ -144,7 +148,7 @@ impl Books { cost_basis, commodity_quantity, tx_date, - &self.commodities_oracle + &mut self.commodities_oracle ); for (eaccount_name, commodity_quantity, commodity, nominal_value) in extra_transactions { @@ -197,8 +201,16 @@ impl Books { let b_account = tx.postings[1].account.clone(); if a.amount.quantity.is_sign_negative() { // Trading a for b - let cost_basis = b.amount.quantity / a.amount.quantity.abs() ; - let extra_transactions = self.asset_accounts.get_mut(&a_account).unwrap().trade(id as TransactionReference,a.amount.commodity.name.clone(), cost_basis, a.amount.quantity, &tx.date.to_string(), &self.commodities_oracle); + let cost_basis = b.amount.quantity / a.amount.quantity.abs() ; + + // Insert this price into the Oracle as the Market Price + if (a.amount.commodity.name == "CAD") { + let trade_basis = a.amount.quantity.abs() / b.amount.quantity; + self.commodities_oracle.insert(b.amount.commodity.name.clone(), tx.date.to_string(), trade_basis); + } + + println!("NOMVAL: {} {} {} {} {} ", cost_basis, a.amount.quantity, a.amount.commodity.name, b.amount.quantity, b.amount.commodity.name); + let extra_transactions = self.asset_accounts.get_mut(&a_account).unwrap().trade(id as TransactionReference,a.amount.commodity.name.clone(), cost_basis, a.amount.quantity, &tx.date.to_string(), &mut self.commodities_oracle); for (eaccount_name, quantity, commodity, nominal_value ) in extra_transactions { if eaccount_name == a_account { general_ledger_entry.push((eaccount_name.clone(), commodity.clone(), quantity, nominal_value)); @@ -206,7 +218,12 @@ impl Books { self.update_account(1, &mut general_ledger_entry, id as TransactionReference, &eaccount_name, &tx.date.to_string(), &format!("$"), nominal_value) } } - self.asset_accounts.get_mut(&b_account).unwrap().deposit(id as TransactionReference, b.amount.commodity.name.clone(), Decimal::new(1,0), b.amount.quantity, &tx.date.to_string()); + + if (a.amount.commodity.name == "CAD") { + self.asset_accounts.get_mut(&b_account).unwrap().deposit(id as TransactionReference, b.amount.commodity.name.clone(), Decimal::new(1,0), b.amount.quantity, &tx.date.to_string(), a.amount.quantity.abs() / b.amount.quantity); + } else { + self.asset_accounts.get_mut(&b_account).unwrap().deposit(id as TransactionReference, b.amount.commodity.name.clone(), Decimal::new(1,0), b.amount.quantity, &tx.date.to_string(), Decimal::new(1,0)); + } general_ledger_entry.push((b_account, b.amount.commodity.name.clone(), b.amount.quantity, Decimal::new(1,0) * b.amount.quantity,)); } else { diff --git a/src/main.rs b/src/main.rs index 87ca926..832d0e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,11 +10,22 @@ pub mod external_account; pub mod oracle; pub mod spreadsheet; -pub const OPENING_DATE : &str = "2023-02-11"; + +use clap::Parser; + +/// Search for a pattern in a file and display the lines that contain it. +#[derive(Parser)] +struct CommandLineParser { + /// The pattern to look for + book_file: String, + start_date: String, + stop_date: String + +} fn main() { - let mut books = Books::new(); - let stop_date = format!("2024-02-10"); - books.load_ledger("2023.dat", &stop_date); - export_to_spreadsheet("test.ods", &books, &format!("{}", OPENING_DATE), &stop_date); + let args = CommandLineParser::parse(); + let mut books = Books::new(&args.start_date, &args.stop_date); + books.load_ledger(&args.book_file, &args.stop_date); + export_to_spreadsheet("test.ods", &books, &args.start_date, &args.stop_date); } diff --git a/src/oracle.rs b/src/oracle.rs index da77d13..cb6044b 100644 --- a/src/oracle.rs +++ b/src/oracle.rs @@ -35,7 +35,8 @@ impl CommoditiesPriceOracle { } Some(price_history) => match price_history.get(date) { None => { - panic!("no date history for {} on {}", commodity, date) + println!("[WARNING] no date history for {} on {}", commodity, date); + return Decimal::new(0,0) } Some(price) => price.clone(), }, diff --git a/src/spreadsheet.rs b/src/spreadsheet.rs index 93c3baf..2e48c93 100644 --- a/src/spreadsheet.rs +++ b/src/spreadsheet.rs @@ -276,9 +276,9 @@ pub fn export_to_spreadsheet(name: &str, books: &Books, opening_date: &String, let index = start_row+(row as u32); let recorded_date = NaiveDate::from_str(date).unwrap(); let nominal_value = if commodity != "CAD" && commodity != "$" && commodity != "StickerSheets" { - cost * quantity + books.commodities_oracle.lookup(commodity, date) * quantity } else { - Decimal::new(1,0 ) * quantity + quantity * Decimal::new(1,0) }; sheet.set_styled_value(index, 0, Value::Number(*txref as f64), &header_style_ref); sheet.set_styled_value(index, 1, Value::DateTime(recorded_date.and_time(NaiveTime::default())), &general_text_style);