1
0
Fork 1
ledgerneo/src/books.rs

360 lines
14 KiB
Rust

use crate::{AssetAccount, CommoditiesPriceOracle, ExternalAccount};
use chrono::NaiveDate;
use ledger_parser::{LedgerItem, Posting, PostingAmount, Transaction};
use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
use std::collections::{HashMap, HashSet};
use std::fs::read_to_string;
use std::str::FromStr;
pub type TransactionReference = usize;
pub struct Books {
pub(crate) asset_accounts: HashMap<String, AssetAccount>,
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(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(),
}
}
pub fn load_ledger(&mut self, ledger_file: &str, to_date: &String) {
let ast = ledger_parser::parse(&read_to_string(ledger_file).unwrap()).unwrap();
for (id, li) in ast.items.iter().enumerate() {
match li {
LedgerItem::EmptyLine => {}
LedgerItem::LineComment(_) => {}
LedgerItem::Transaction(tx) => {
let mut final_line = None;
let mut total = Decimal::new(0, 0);
let mut commodities = HashSet::new();
let mut general_ledger_entry = vec![];
if tx.date > NaiveDate::from_str(&to_date).unwrap() {
break;
}
if tx.postings.len() == 2 {
match (&tx.postings[0].amount, &tx.postings[1].amount) {
(Some(a), Some(b)) => {
if a.amount.commodity != b.amount.commodity {
// handle trading
self.handle_commodities_exchange(id, &tx);
continue;
}
}
_ => {}
}
}
for posting in &tx.postings {
match &posting.amount {
None => {
final_line = Some(posting.clone());
}
Some(amount) => {
println!(
"{},{},{},{},{}",
id, tx.date, posting.account, amount.amount.commodity.name, amount.amount.quantity
);
total += amount.amount.quantity;
commodities.insert(amount.amount.commodity.name.clone());
self.update_account(
0,
&mut general_ledger_entry,
id as TransactionReference,
&posting.account,
&tx.date.to_string(),
&amount.amount.commodity.name.clone(),
amount.amount.quantity,
);
}
}
}
match final_line {
Some(posting) => {
if commodities.len() != 1 {
panic!("Trades should be handled separately")
}
println!("{},{},{},{}", id, tx.date, posting.account, total);
let commodity: Vec<&String> = commodities.iter().collect();
self.update_account(
0,
&mut general_ledger_entry,
id as TransactionReference,
&posting.account,
&tx.date.to_string(),
commodity[0],
-total,
);
}
_ => {}
}
self.general_ledger.push((
id as TransactionReference,
tx.date.to_string(),
general_ledger_entry,
))
}
LedgerItem::CommodityPrice(cprice) => self.commodities_oracle.insert(
cprice.commodity_name.clone(),
cprice.datetime.date().to_string(),
cprice.amount.quantity,
),
LedgerItem::Include(_) => {}
_ => {}
}
}
}
pub fn update_account(
&mut self,
depth: usize,
general_ledger_entry: &mut Vec<(String, String, Decimal, Decimal)>,
tx_ref: TransactionReference,
account_name: &String,
tx_date: &String,
commodity_name: &String,
commodity_quantity: Decimal,
) {
if account_name.starts_with("Assets:") || account_name.starts_with("Liability:") {
match self.asset_accounts.contains_key(account_name) {
false => {
self.asset_accounts.insert(
account_name.clone(),
AssetAccount::new(account_name.clone(), &self.opening_date),
);
}
_ => {}
}
let asset_account = self.asset_accounts.get_mut(account_name).unwrap();
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,
cost_basis,
);
general_ledger_entry.push((
account_name.clone(),
commodity_name.clone(),
commodity_quantity,
commodity_quantity * cost_basis,
));
} else {
let extra_transactions = asset_account.trade(
tx_ref,
commodity_name.clone(),
cost_basis,
commodity_quantity,
tx_date,
&mut self.commodities_oracle,
);
for (eaccount_name, commodity_quantity, commodity, nominal_value) in extra_transactions {
if *account_name != eaccount_name && depth == 0 {
self.update_account(
1,
general_ledger_entry,
tx_ref,
&eaccount_name.clone(),
tx_date,
&commodity,
nominal_value,
)
} else {
general_ledger_entry.push((
eaccount_name.clone(),
commodity,
commodity_quantity,
nominal_value,
));
}
}
}
} else {
// This is an external account
match self.external_accounts.contains_key(account_name) {
false => {
let external_account = if account_name.starts_with("Income") {
ExternalAccount::new(account_name.clone(), true)
} else if account_name.starts_with("Expenses") {
ExternalAccount::new(account_name.clone(), false)
} else if account_name.starts_with("Correction") {
ExternalAccount::new(account_name.clone(), true)
} else {
ExternalAccount::new(account_name.clone(), false)
};
self.external_accounts
.insert(account_name.clone(), external_account);
}
_ => {}
}
let external_account = self.external_accounts.get_mut(account_name).unwrap();
let cost_basis = self.commodities_oracle.lookup(commodity_name, tx_date);
if !external_account.income_account {
external_account.deposit(tx_ref, commodity_name.clone(), commodity_quantity, tx_date);
general_ledger_entry.push((
account_name.clone(),
commodity_name.clone(),
commodity_quantity,
cost_basis * commodity_quantity,
));
} else {
external_account.withdraw(tx_ref, commodity_name.clone(), commodity_quantity, tx_date);
general_ledger_entry.push((
account_name.clone(),
commodity_name.clone(),
commodity_quantity,
cost_basis * commodity_quantity,
));
}
}
}
fn handle_commodities_exchange(&mut self, id: usize, tx: &Transaction) {
let mut general_ledger_entry = vec![];
let a = tx.postings[0].amount.as_ref().unwrap();
let b = tx.postings[1].amount.as_ref().unwrap();
let a_account = tx.postings[0].account.clone();
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();
// 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,
));
} else {
self.update_account(
1,
&mut general_ledger_entry,
id as TransactionReference,
&eaccount_name,
&tx.date.to_string(),
&format!("$"),
nominal_value,
)
}
}
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 {
panic!("Trade order should have selling first {} {}", a, b);
}
self.general_ledger.push((
id as TransactionReference,
tx.date.to_string(),
general_ledger_entry,
));
}
pub fn summarize_accounts(&self) {
for (account_name, account) in self.external_accounts.iter() {
println!("{}", account_name);
println!("-----");
account.summarize_transactions();
println!()
}
}
pub fn calculate_gains(&self, date: &String) -> f64 {
let mut realized_gains = Decimal::new(0, 0);
let mut unrealized_gains = Decimal::new(0, 0);
for (name, asset_account) in self.asset_accounts.iter() {
println!("{} ", name);
realized_gains += asset_account.realized_gains;
println!(" Realized: {}", asset_account.realized_gains);
for (commodity, total) in asset_account.unrealized_gains(&self.commodities_oracle, &String::from(date)) {
println!(" Unrealized: {} {} ", commodity, total);
unrealized_gains += total;
}
}
println!("Total Realized Gains: {}", realized_gains);
println!("Total Unrealized Gains: {}", unrealized_gains);
println!(
"Total Realized + Unrealized Gains: {}",
unrealized_gains + realized_gains
);
return (unrealized_gains).to_f64().unwrap();
}
}