1
0
mirror of https://github.com/scoobybejesus/cryptools.git synced 2025-04-18 19:00:27 +00:00
cryptools-mirror/src/crptls_lib/transaction.rs

465 lines
16 KiB
Rust

// Copyright (c) 2017-2019, scoobybejesus
// Redistributions must include the license: https://github.com/scoobybejesus/cryptools/blob/master/LEGAL.txt
use std::rc::{Rc};
use std::cell::{RefCell};
use std::process;
use std::fmt;
use std::collections::{HashMap};
use std::error::Error;
use decimal::d128;
use chrono::NaiveDate;
use serde_derive::{Serialize, Deserialize};
use crate::crptls_lib::account::{Account, Movement, RawAccount};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Transaction {
pub tx_number: u32, // Does NOT start at zero. First txn is 1.
pub date_as_string: String,
pub date: NaiveDate,
pub user_memo: String,
pub proceeds: f32,
pub action_record_idx_vec: Vec<u32>,
}
impl Transaction {
pub fn transaction_type(
&self,
ars: &HashMap<u32, ActionRecord>,
raw_acct_map: &HashMap<u16, RawAccount>,
acct_map: &HashMap<u16, Account>,
) -> Result<TxType, Box<dyn Error>> {
if self.action_record_idx_vec.len() == 1 {
Ok(TxType::Flow)
}
else if self.action_record_idx_vec.len() == 2 {
// This exercise of splitting the strings is because of margin accounts, where BTC borrowed to buy XMR would reflect as BTC_xmr
let first_ar = ars.get(&self.action_record_idx_vec[0]).unwrap();
let second_ar = ars.get(&self.action_record_idx_vec[1]).unwrap();
let first_acct = acct_map.get(&first_ar.account_key).unwrap();
let second_acct = acct_map.get(&second_ar.account_key).unwrap();
let ar1_raw_acct = raw_acct_map.get(&first_acct.raw_key).unwrap();
let ar2_raw_acct = raw_acct_map.get(&second_acct.raw_key).unwrap();
let ar1_ticker_full = ar1_raw_acct.ticker.clone();
let ar2_ticker_full = ar2_raw_acct.ticker.clone();
let ar1_ticker_comp: Vec<&str> = ar1_ticker_full.split('_').collect();
let ar2_ticker_comp: Vec<&str> = ar2_ticker_full.split('_').collect();
let ar1_ticker = ar1_ticker_comp[0];
let ar2_ticker = ar2_ticker_comp[0];
if first_ar.direction() == second_ar.direction() {
println!("FATAL: TxType: Found transaction with two actionRecords with the same polarity: \n{:#?}", self);
process::exit(1);
}
if ar1_ticker == ar2_ticker {
if ar1_raw_acct.is_margin != ar2_raw_acct.is_margin {
Ok(TxType::Flow)
}
else {
Ok(TxType::ToSelf)
}
}
else {
Ok(TxType::Exchange)
}
}
else if self.action_record_idx_vec.len() > 2 {
println!("FATAL: TxType: Found transaction with too many actionRecords: \n{:#?}", self);
process::exit(1);
}
else {
println!("FATAL: TxType: Found transaction with no actionRecords: \n{:#?}", self);
process::exit(1);
}
}
pub fn marginness(
&self,
ars: &HashMap<u32, ActionRecord>,
raw_acct_map: &HashMap<u16, RawAccount>,
acct_map: &HashMap<u16, Account>
) -> TxHasMargin {
if self.action_record_idx_vec.len() == 1 {
let ar = ars.get(&self.action_record_idx_vec[0]).unwrap();
let acct = acct_map.get(&ar.account_key).unwrap();
let raw_acct = raw_acct_map.get(&acct.raw_key).unwrap();
if raw_acct.is_margin {
TxHasMargin::OneAR
} else {
TxHasMargin::NoARs
}
} else {
if self.action_record_idx_vec.len() != 2 {
println!("FATAL: Each transaction may have up to two actionrecords, and there are {} actionrecords in transaction:\n{:#?}",
self.action_record_idx_vec.len(), self);
}
let first_ar = match ars.get(&self.action_record_idx_vec[0]) {
Some(x) => x,
None => {
println!("FATAL: ActionRecord not found for: \n{:#?}", self);
process::exit(1)}
};
let second_ar = match ars.get(&self.action_record_idx_vec[1]) {
Some(x) => x,
None => {
println!("FATAL: ActionRecord not found for: \n{:#?}", self);
process::exit(1)}
};
let first_acct = acct_map.get(&first_ar.account_key).unwrap();
let second_acct = acct_map.get(&second_ar.account_key).unwrap();
let first_raw_acct = &raw_acct_map.get(&first_acct.raw_key).unwrap();
let second_raw_acct = &raw_acct_map.get(&second_acct.raw_key).unwrap();
if first_raw_acct.is_margin {
if second_raw_acct.is_margin {TxHasMargin::TwoARs} else {TxHasMargin::OneAR}
} else if second_raw_acct.is_margin {TxHasMargin::OneAR} else {TxHasMargin::NoARs}
}
}
pub fn get_base_and_quote_raw_acct_keys(
&self,
ars: &HashMap<u32, ActionRecord>,
raw_accts: &HashMap<u16, RawAccount>,
acct_map: &HashMap<u16, Account>
) -> Result<(u16, u16), Box<dyn Error>> {
assert_eq!(self.transaction_type(ars, raw_accts, acct_map)?, TxType::Exchange,
"This can only be called on exchange transactions.");
let first_ar = ars.get(&self.action_record_idx_vec[0]).unwrap();
let second_ar = ars.get(&self.action_record_idx_vec[1]).unwrap();
let first_acct = acct_map.get(&first_ar.account_key).unwrap();
let second_acct = acct_map.get(&second_ar.account_key).unwrap();
let first_acct_raw_key = first_acct.raw_key;
let second_acct_raw_key = second_acct.raw_key;
let first_raw_acct = raw_accts.get(&first_acct_raw_key).unwrap();
let second_raw_acct = raw_accts.get(&second_acct_raw_key).unwrap();
assert_eq!(first_raw_acct.is_margin, true, "First actionrecord wasn't a margin account. Both must be.");
assert_eq!(second_raw_acct.is_margin, true, "Second actionrecord wasn't a margin account. Both must be.");
let quote: u16;
let base: u16;
if first_raw_acct.ticker.contains('_') {
quote = first_acct_raw_key;
base = second_acct_raw_key;
Ok((base, quote))
} else if second_raw_acct.ticker.contains('_') {
base = first_acct_raw_key;
quote = second_acct_raw_key;
Ok((base, quote))
} else {
println!("FATAL: {}", VariousErrors::MarginNoUnderbar);
std::process::exit(1);
}
}
pub fn get_outgoing_exchange_and_flow_mvmts(
&self,
user_home_currency: &str,
ars: &HashMap<u32, ActionRecord>,
raw_acct_map: &HashMap<u16, RawAccount>,
acct_map: &HashMap<u16, Account>,
txns_map: &HashMap<u32, Transaction>,
) -> Result<Vec<Rc<Movement>>, Box<dyn Error>> {
let mut flow_or_outgoing_exchange_movements = [].to_vec();
for ar_num in self.action_record_idx_vec.iter() {
let ar = ars.get(ar_num).unwrap();
let acct = acct_map.get(&ar.account_key).unwrap();
let raw_acct = raw_acct_map.get(&acct.raw_key).unwrap();
if !raw_acct.is_home_currency(user_home_currency) & !raw_acct.is_margin {
let movements = ar.get_mvmts_in_ar_in_lot_date_order(acct_map, txns_map);
match self.transaction_type(ars, raw_acct_map, acct_map)? {
TxType::Exchange => {
if Polarity::Outgoing == ar.direction() {
for mvmt in movements.iter() {
flow_or_outgoing_exchange_movements.push(mvmt.clone());
}
}
}
TxType::Flow => {
for mvmt in movements.iter() {
flow_or_outgoing_exchange_movements.push(mvmt.clone());
}
}
_ => {}
}
}
}
Ok(flow_or_outgoing_exchange_movements)
}
pub fn both_exch_ars_are_non_home_curr(
&self,
ars: &HashMap<u32, ActionRecord>,
raw_acct_map: &HashMap<u16, RawAccount>,
acct_map: &HashMap<u16, Account>,
home_currency: &str,
) -> Result<bool, Box<dyn Error>> {
assert_eq!(self.action_record_idx_vec.len(), (2 as usize));
let og_ar = ars.get(&self.action_record_idx_vec.first().unwrap()).unwrap();
let ic_ar = ars.get(&self.action_record_idx_vec.last().unwrap()).unwrap();
let og_acct = acct_map.get(&og_ar.account_key).unwrap();
let ic_acct = acct_map.get(&ic_ar.account_key).unwrap();
let raw_og_acct = raw_acct_map.get(&og_acct.raw_key).unwrap();
let raw_ic_acct = raw_acct_map.get(&ic_acct.raw_key).unwrap();
let og_is_home_curr = raw_og_acct.is_home_currency(&home_currency);
let ic_is_home_curr = raw_ic_acct.is_home_currency(&home_currency);
let both_are_non_home_curr = !ic_is_home_curr && !og_is_home_curr;
Ok(both_are_non_home_curr)
}
pub fn get_auto_memo(
&self,
ars: &HashMap<u32, ActionRecord>,
raw_accts: &HashMap<u16, RawAccount>,
acct_map: &HashMap<u16, Account>,
home_currency: &str,
) -> Result<String, Box<dyn Error>> {
let auto_memo = if self.action_record_idx_vec.len() == 2 {
let tx_type = self.transaction_type(ars, raw_accts, acct_map)?;
let marginness = self.marginness(ars, raw_accts, acct_map);
if (marginness == TxHasMargin::NoARs) | (marginness == TxHasMargin::TwoARs) {
let og_amt = ars.get(&self.action_record_idx_vec[0]).unwrap().amount;
let og_acct_key = ars.get(&self.action_record_idx_vec[0]).unwrap().account_key;
let og_acct = acct_map.get(&og_acct_key).unwrap();
let og_raw_acct = raw_accts.get(&og_acct.raw_key).unwrap();
let og_ticker = &og_raw_acct.ticker;
let ic_amt = ars.get(&self.action_record_idx_vec[1]).unwrap().amount;
let ic_acct_key = ars.get(&self.action_record_idx_vec[1]).unwrap().account_key;
let ic_acct = acct_map.get(&ic_acct_key).unwrap();
let ic_raw_acct = raw_accts.get(&ic_acct.raw_key).unwrap();
let ic_ticker = &ic_raw_acct.ticker;
let og_amt_and_ticker;
if og_raw_acct.is_home_currency(home_currency) {
og_amt_and_ticker = format!("{:.2} {}",
og_amt.to_string().as_str().parse::<f32>()?, og_ticker
);
} else {
og_amt_and_ticker = format!("{} {}", og_amt, og_ticker);
}
let ic_amt_and_ticker;
if ic_raw_acct.is_home_currency(home_currency) {
ic_amt_and_ticker = format!("{:.2} {}",
ic_amt.to_string().as_str().parse::<f32>()?, ic_ticker
);
} else {
ic_amt_and_ticker = format!("{} {}", ic_amt, ic_ticker);
}
if tx_type == TxType::Exchange {
format!("Paid {} for {}, valued at {:.2} {}.",
og_amt_and_ticker, ic_amt_and_ticker,
self.proceeds.to_string().as_str().parse::<f32>()?, home_currency)
} else {
format!("Transferred {} to another account. Received {}, likely after a transaction fee.",
og_amt_and_ticker, ic_amt_and_ticker)
}
} else {
format!("Margin profit or loss valued at {:.2} {}.",
self.proceeds.to_string().as_str().parse::<f32>()?, home_currency)
}
} else {
let amt = ars.get(&self.action_record_idx_vec[0]).unwrap().amount;
let acct_key = ars.get(&self.action_record_idx_vec[0]).unwrap().account_key;
let acct = acct_map.get(&acct_key).unwrap();
let raw_acct = raw_accts.get(&acct.raw_key).unwrap();
let ticker = &raw_acct.ticker;
if amt > d128!(0.0) {
format!("Received {} {} valued at {:.2} {}.", amt, ticker,
self.proceeds.to_string().as_str().parse::<f32>()?, home_currency)
} else {
format!("Spent {} {} valued at {:.2} {}.", amt, ticker,
self.proceeds.to_string().as_str().parse::<f32>()?, home_currency)
}
};
Ok(auto_memo)
}
}
#[derive(Clone, Debug)]
pub struct ActionRecord {
pub account_key: u16,
pub amount: d128,
pub tx_key: u32,
pub self_ar_key: u32,
pub movements: RefCell<Vec<Rc<Movement>>>,
}
impl ActionRecord {
pub fn direction(&self) -> Polarity {
if self.amount < d128!(0.0) { Polarity::Outgoing}
else { Polarity::Incoming }
}
pub fn cost_basis_in_ar(&self) -> d128 {
let mut cb = d128!(0);
for mvmt in self.movements.borrow().iter() {
cb += mvmt.cost_basis.get()
}
cb.abs()
}
// pub fn is_quote_acct_for_margin_exch(
// &self,
// raw_accts: &HashMap<u16, RawAccount>,
// acct_map: &HashMap<u16, Account>
// ) -> bool {
// let acct = acct_map.get(&self.account_key).unwrap();
// let raw_acct = raw_accts.get(&acct.raw_key).unwrap();
// raw_acct.ticker.contains('_')
// }
/// Iterates through every `Lot` in the `list_of_lots` of the `ActionRecord`'s `Account`
/// until it finds all the `Movements` - cloning each along the way - and then returns
/// a `Vec` of `Rc<Movements>`.
///
/// Note that a `Lot`'s `date`, and generally its `basis_date` too, will increase
/// chronologically along with the `Lot`'s `lot_num` which is just it's `index` in the
/// `list_of_lots` plus `1`. Exceptions will occur, because `Lot`s are permanently
/// ordered by their creation date (`date`), so later `Lot`s may have earlier `basis_date`s
/// by virtue of them being the result of a `ToSelf` type `Transaction` that transferred
/// old "coins."
pub fn get_mvmts_in_ar_in_lot_date_order(
&self,
acct_map: &HashMap<u16, Account>,
txns_map: &HashMap<u32, Transaction>,
) -> Vec<Rc<Movement>> {
let txn = txns_map.get(&self.tx_key).unwrap();
let mut movements_in_ar = [].to_vec();
let acct = acct_map.get(&self.account_key).unwrap();
let target = self.amount;
let mut measure = d128!(0);
for lot in acct.list_of_lots.borrow().iter() {
for mvmt in lot.movements.borrow().iter() {
if (mvmt.date) <= txn.date && mvmt.action_record_key == self.self_ar_key {
measure += mvmt.amount;
movements_in_ar.push(mvmt.clone());
if measure == target { return movements_in_ar }
}
}
}
println!("ERROR: This should never print.");
movements_in_ar
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum TxType {
Exchange,
ToSelf,
Flow,
}
impl fmt::Display for TxType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
TxType::Exchange => write!(f, "Exchange"),
TxType::ToSelf => write!(f, "ToSelf"),
TxType::Flow => write!(f, "Flow"),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum TxHasMargin {
NoARs,
OneAR,
TwoARs,
}
impl fmt::Display for TxHasMargin {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
TxHasMargin::NoARs => write!(f, "No actionrecord is a margin account"),
TxHasMargin::OneAR => write!(f, "One actionrecord is a margin account"),
TxHasMargin::TwoARs => write!(f, "Two actionrecords are margin accounts"),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Polarity {
Outgoing,
Incoming,
}
impl fmt::Display for Polarity {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Polarity::Outgoing => write!(f, "Outgoing"),
Polarity::Incoming => write!(f, "Incoming"),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum VariousErrors {
MarginNoUnderbar,
}
impl fmt::Display for VariousErrors {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
VariousErrors::MarginNoUnderbar => write!(f,
"Neither account ticker contained an underbar '_', so the quote account couldn't be determined.
For example, for the 'USD/EUR' pair, USD is the base account and EUR is the quote account.
In order for this software to function correctly, the quote ticker should be denoted as 'EUR_usd'.")
}
}
}