360 lines
14 KiB
Rust
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();
|
|
}
|
|
}
|