397 lines
15 KiB
Rust
397 lines
15 KiB
Rust
use crate::books::TransactionReference;
|
|
use crate::CommoditiesPriceOracle;
|
|
use rust_decimal::prelude::ToPrimitive;
|
|
use rust_decimal::Decimal;
|
|
use std::collections::HashMap;
|
|
use std::ops::Neg;
|
|
|
|
#[derive(Clone)]
|
|
pub struct AssetCommodity {
|
|
cost_basis: Decimal,
|
|
quantity: Decimal,
|
|
name: String,
|
|
}
|
|
|
|
impl AssetCommodity {
|
|
pub fn cost_basis_total(&self) -> Decimal {
|
|
self.cost_basis * self.quantity
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct AssetAccount {
|
|
name: String,
|
|
commodities: HashMap<String, Vec<AssetCommodity>>,
|
|
running_total: HashMap<String, Decimal>,
|
|
// txid, date, commodity, quantity, cost,
|
|
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, opening_date: &String) -> AssetAccount {
|
|
AssetAccount {
|
|
name,
|
|
commodities: HashMap::new(),
|
|
realized_gains: Decimal::new(0, 0),
|
|
running_total: HashMap::new(),
|
|
transactions: vec![],
|
|
opening: HashMap::new(),
|
|
opening_date: opening_date.clone(),
|
|
}
|
|
}
|
|
|
|
pub fn unrealized_gains(&self, oracle: &CommoditiesPriceOracle, date: &String) -> Vec<(String, Decimal)> {
|
|
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)
|
|
});
|
|
|
|
unrealized_gain += current_cost - historical_cost;
|
|
totals.push((name.clone(), unrealized_gain))
|
|
}
|
|
}
|
|
totals
|
|
}
|
|
|
|
pub fn total_nominal(&self, oracle: &CommoditiesPriceOracle, date: &String) -> f64 {
|
|
let mut total = 0.0;
|
|
for (commodity_name, quantity) in self.running_total.iter() {
|
|
let mut value = if commodity_name != "CAD" && commodity_name != "$" && commodity_name != "StickerSheets" {
|
|
(oracle.lookup(commodity_name, date) * quantity.abs())
|
|
.to_f64()
|
|
.unwrap()
|
|
} else if commodity_name == "CAD" || commodity_name == "$" {
|
|
quantity.to_f64().unwrap()
|
|
} else {
|
|
0.0
|
|
};
|
|
if self.name.starts_with("Liability") {
|
|
// flip sign
|
|
value = value.neg()
|
|
}
|
|
|
|
total += value;
|
|
}
|
|
total
|
|
}
|
|
|
|
pub fn opening_nominal(&self, oracle: &CommoditiesPriceOracle, date: &String) -> f64 {
|
|
let mut total = 0.0;
|
|
for (commodity_name, quantity) in self.opening.iter() {
|
|
let mut value = if commodity_name != "CAD" && commodity_name != "$" && commodity_name != "StickerSheets" {
|
|
(oracle.lookup(commodity_name, date) * quantity.abs())
|
|
.to_f64()
|
|
.unwrap()
|
|
} else if commodity_name == "CAD" || commodity_name == "$" {
|
|
quantity.to_f64().unwrap()
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
if self.name.starts_with("Liability") {
|
|
// flip sign
|
|
value = value.neg()
|
|
}
|
|
|
|
total += value;
|
|
}
|
|
total
|
|
}
|
|
|
|
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,
|
|
quantity,
|
|
name: name.clone(),
|
|
};
|
|
|
|
match self.commodities.contains_key(&name) {
|
|
false => {
|
|
self.commodities.insert(name.clone(), vec![]);
|
|
self.running_total.insert(name.clone(), Decimal::new(0, 0));
|
|
if *date == self.opening_date {
|
|
println!("Opening Total: {} {} {}", quantity, name, cost);
|
|
self.opening.insert(name.clone(), quantity);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
*self.running_total.get_mut(&name).unwrap() += quantity;
|
|
println!(
|
|
"Running Total: {} {} {} {}",
|
|
quantity,
|
|
name,
|
|
cost,
|
|
*self.running_total.get_mut(&name).unwrap()
|
|
);
|
|
|
|
if self.name.starts_with("Assets") == false {
|
|
self.transactions.push((
|
|
txid,
|
|
date.clone(),
|
|
name.clone(),
|
|
cost,
|
|
quantity,
|
|
self.name.clone(),
|
|
));
|
|
let mut meta_transactions = vec![];
|
|
meta_transactions.push((self.name.clone(), quantity, name, cost * quantity));
|
|
// we don't track cost-basis for liabilities
|
|
return meta_transactions;
|
|
}
|
|
|
|
match self.commodities.get_mut(&name) {
|
|
None => {
|
|
panic!();
|
|
}
|
|
Some(current_commodities) => {
|
|
current_commodities.sort_by(|a, b| a.cost_basis.cmp(&b.cost_basis));
|
|
current_commodities.reverse();
|
|
|
|
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() {
|
|
continue;
|
|
}
|
|
if commodity.quantity <= quantity_needed {
|
|
// use all of this commodity....
|
|
println!(
|
|
"[REALIZED GAIN CALC] Taking {} {} @@ {}",
|
|
commodity.quantity, commodity.name, 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;
|
|
commodity.quantity -= quantity_needed;
|
|
break;
|
|
}
|
|
if quantity_needed.is_zero() {
|
|
break;
|
|
}
|
|
}
|
|
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!(
|
|
"[DEBUG] 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 mut actual_gain = ((cost * quantity.abs()) - base_cost) + arbitrage_expense;
|
|
if buying {
|
|
actual_gain = Decimal::new(0, 0);
|
|
}
|
|
self.realized_gains += actual_gain;
|
|
|
|
println!(
|
|
"[REALIZED GAIN CALC] Realized {} base: {} 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(),
|
|
));
|
|
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"),
|
|
));
|
|
meta_transactions.push((
|
|
format!("Income:RealizedGains"),
|
|
actual_gain.neg(),
|
|
format!("$"),
|
|
actual_gain.neg(),
|
|
))
|
|
} else if actual_gain.is_sign_negative() {
|
|
self.transactions.push((
|
|
txid,
|
|
date.clone(),
|
|
format!("$"),
|
|
Decimal::new(1, 0),
|
|
actual_gain.neg(),
|
|
format!("Expenses:RealizedLoss"),
|
|
));
|
|
meta_transactions.push((
|
|
format!("Expenses:RealizedLoss"),
|
|
actual_gain.neg(),
|
|
format!("$"),
|
|
actual_gain.neg(),
|
|
))
|
|
}
|
|
}
|
|
|
|
if arbitrage_expense.is_zero() == false {
|
|
if arbitrage_expense.is_sign_negative() {
|
|
self.transactions.push((
|
|
txid,
|
|
date.clone(),
|
|
format!("$"),
|
|
Decimal::new(1, 0),
|
|
arbitrage_expense,
|
|
format!("Income:ExchangeArbitrage"),
|
|
));
|
|
meta_transactions.push((
|
|
format!("Income:ExchangeArbitrage"),
|
|
arbitrage_expense,
|
|
format!("$"),
|
|
arbitrage_expense,
|
|
))
|
|
} else if arbitrage_expense.is_sign_positive() {
|
|
self.transactions.push((
|
|
txid,
|
|
date.clone(),
|
|
format!("$"),
|
|
Decimal::new(1, 0),
|
|
arbitrage_expense,
|
|
format!("Expenses:BankingFees:ExchangeArbitrage"),
|
|
));
|
|
meta_transactions.push((
|
|
format!("Expenses:BankingFees:ExchangeArbitrage"),
|
|
arbitrage_expense,
|
|
format!("$"),
|
|
arbitrage_expense,
|
|
))
|
|
}
|
|
}
|
|
|
|
return meta_transactions;
|
|
} else {
|
|
println!("[UG] DEBUG ASSET TRADE {} {} {}", cost, quantity, name);
|
|
current_commodities.push(AssetCommodity {
|
|
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(), quantity, name.clone(), cost * quantity));
|
|
return meta_transactions;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
name: name.clone(),
|
|
};
|
|
|
|
match self.commodities.get_mut(&name) {
|
|
None => {
|
|
self.commodities.insert(name.clone(), vec![asset_commodity]);
|
|
self.running_total.insert(name.clone(), Decimal::new(0, 0));
|
|
if *date == self.opening_date {
|
|
println!(
|
|
"[REALIZED GAIN CALC] Opening: {} {} {}",
|
|
quantity, name, cost
|
|
);
|
|
self.opening.insert(name.clone(), quantity);
|
|
}
|
|
}
|
|
Some(account) => account.push(asset_commodity),
|
|
}
|
|
|
|
self.transactions.push((
|
|
txid,
|
|
date.clone(),
|
|
name.clone(),
|
|
cost,
|
|
quantity,
|
|
self.name.clone(),
|
|
));
|
|
|
|
*self.running_total.get_mut(&name).unwrap() += quantity;
|
|
println!(
|
|
"Running Total: {} {} {} {}",
|
|
quantity,
|
|
name,
|
|
cost,
|
|
*self.running_total.get_mut(&name).unwrap()
|
|
);
|
|
}
|
|
}
|