1
0
Fork 0

Support Buying of Assets / Reporting Nominal Value

This commit is contained in:
Sarah Jamie Lewis 2024-04-26 12:57:59 -07:00
parent 127a16c90c
commit 85477bd8a0
Signed by: sarah
GPG Key ID: F27FD21A270837EF
7 changed files with 208 additions and 41 deletions

122
Cargo.lock generated
View File

@ -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"

View File

@ -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"
icu_locid = "1.1.0"
clap = { version = "4.0", features = ["derive"] }

View File

@ -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<String, Decimal>,
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),
}

View File

@ -14,15 +14,19 @@ pub struct Books {
pub(crate) external_accounts: HashMap<String, ExternalAccount>,
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 {

View File

@ -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);
}

View File

@ -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(),
},

View File

@ -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);