|
|
|
@ -14,37 +14,6 @@ use crate::crptls_lib::account::{Account, RawAccount, Lot, Movement};
|
|
|
|
|
use crate::crptls_lib::costing_method::{InventoryCostingMethod};
|
|
|
|
|
use crate::crptls_lib::decimal_utils::{round_d128_1e8};
|
|
|
|
|
|
|
|
|
|
/// This is probably the most important function in the whole program. Based on the data in the CSV Input File,
|
|
|
|
|
/// the `account`s and `transaction`s will be created. Once the `account`s and `transaction`s have been created, both
|
|
|
|
|
/// are passed to this function for `lot` processing. The `lot` processing rules can be deduced from reading the code,
|
|
|
|
|
/// though that implies there are no mistakes (i.e., a mistake in the code would mean that the intent of the code cannot
|
|
|
|
|
/// necessarily be deduced). To remove any doubt, the logic below will be made [hopefully] clear by documentation.
|
|
|
|
|
///
|
|
|
|
|
/// The first thing to bear in mind is that this function should not be thought of as generalizable to an interactive
|
|
|
|
|
/// program; rather, the only data this program requires is that which is included in the CSV Input File, and accordingly
|
|
|
|
|
/// the program will iterate through that data deterministically to produce the results that it produces. On top of that,
|
|
|
|
|
/// the program is assumed to produce correct results based on a correct CSV Input File. Accordingly, this code is designed
|
|
|
|
|
/// to fail loud and fast if it encounters unexpected things. Logging, particularly in this function, would be wise.
|
|
|
|
|
///
|
|
|
|
|
/// Second, the goal of this function is to be able to properly record all `lot`s and `lot` `movements` in a single
|
|
|
|
|
/// iteration. This means there is a strict order of operations required in some parts. For example, in the case of a
|
|
|
|
|
/// like-kind exchange `transaction` in which the basis dates and amounts of the outgoing `action record` must be transferred
|
|
|
|
|
/// to the basis dates and amounts of the corresponding incoming `action record`, the transaction's outgoing `action record`
|
|
|
|
|
/// therefore must be processed before that `transction`'s incoming `action record`. For that reason, a `transaction`'s
|
|
|
|
|
/// vector of `action record` indices is ordered with outgoing `action record`s always being first.
|
|
|
|
|
///
|
|
|
|
|
/// Third, `lot`s and `movement`s are created sequentially and kept in that order. Accordingly, an outgoing `movement` will
|
|
|
|
|
/// reduce the last `lot` first. If there is not enough in the last `lot`, the remainder will then be applied to the `lot`
|
|
|
|
|
/// before that, and the `lot` before that, and so on (recursively), until the amount in the `action record` has been
|
|
|
|
|
/// fully recorded as `lot` `movement`s. This is known as last in, first out (LIFO). Please refer to the `InventoryCostingMethod`
|
|
|
|
|
/// enum for the available choices if LIFO is not desirable. If a different `inventory costing method` is chosen,
|
|
|
|
|
/// the vector of indices is re-ordered to accomodate the paradigm that the `action record` amount will always be applied
|
|
|
|
|
/// to the last `lot` (i.e., if e.g. FIFO is chosen, then the index for the last `lot` will be 0 instead of length - 1.).
|
|
|
|
|
/// See below `vec_of_ordered_index_values`.
|
|
|
|
|
///
|
|
|
|
|
/// Fourth, this function does not contemplate any income/expense/gain/loss at all. It is solely an exercise in determining
|
|
|
|
|
/// and solidifying how to split (if needed) the amount in each `action record` into `movement`s that post to the appropriate
|
|
|
|
|
/// `lot`s. Conceptually, each `account` has a list of `lot`s, and each `lot` has a list of `movement`s.
|
|
|
|
|
pub(crate) fn create_lots_and_movements(
|
|
|
|
|
settings: &ImportProcessParameters,
|
|
|
|
|
raw_acct_map: &HashMap<u16, RawAccount>,
|
|
|
|
@ -60,25 +29,14 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
let like_kind_cutoff_date = settings.lk_cutoff_date;
|
|
|
|
|
let lk_basis_date_preserved = settings.lk_basis_date_preserved;
|
|
|
|
|
|
|
|
|
|
// This is set automatically based on how like-kind `exchange` `transaction`s work, but it could be left to user choice, in theory.
|
|
|
|
|
let multiple_incoming_mvmts_per_ar_due_to_lk = lk_basis_date_preserved;
|
|
|
|
|
let multiple_incoming_mvmts_per_ar = lk_basis_date_preserved;
|
|
|
|
|
|
|
|
|
|
// On with the creating of lots and movements.
|
|
|
|
|
let length = txns_map.len();
|
|
|
|
|
|
|
|
|
|
// Transactions are stored in a HashMap, and they are ordered sequentially starting at 1, so we iterate through
|
|
|
|
|
// that range and use the corresponding `num` to get each transaction.
|
|
|
|
|
for num in 1..=length {
|
|
|
|
|
|
|
|
|
|
let txn_num = num as u32;
|
|
|
|
|
let txn = txns_map.get(&(txn_num)).expect("Couldn't get txn. Tx num invalid?");
|
|
|
|
|
|
|
|
|
|
// The first type of transaction we consider are those where both `action record`s have an `account` that
|
|
|
|
|
// is a margin `account`. If so, it is an `exchange` `transaction`. `Exchange` `transaction`s for margin
|
|
|
|
|
// `account`s don't create a new lot for every increase. Rather, it keeps one lot per "close," which is
|
|
|
|
|
// to say that pair of margin `account` `lot`s will be used until a profit or loss is realized as a result
|
|
|
|
|
// of zeroing out both the margin `account`s by both closing the margin position AND making a transfer
|
|
|
|
|
// between the margin quote `account` and the corresponding spot `account` such that both margin `account`s
|
|
|
|
|
// now have a zero balance.
|
|
|
|
|
if txn.marginness(&ar_map, &raw_acct_map, &acct_map) == TxHasMargin::TwoARs {
|
|
|
|
|
assert_eq!(txn.transaction_type(&ar_map, &raw_acct_map, &acct_map)?, TxType::Exchange);
|
|
|
|
|
assert_eq!(txn.action_record_idx_vec.len(), 2);
|
|
|
|
@ -87,8 +45,6 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
let base_acct = acct_map.get(&the_raw_pair_keys.0).expect("Couldn't get acct. Raw pair keys invalid?");
|
|
|
|
|
let quote_acct = acct_map.get(&the_raw_pair_keys.1).expect("Couldn't get acct. Raw pair keys invalid?");
|
|
|
|
|
|
|
|
|
|
// This seems trivial, but there can be a series of buys and sells within a margin trade before the
|
|
|
|
|
// trade is closed for a profit or loss, so this ensures we know which `action record` is which.
|
|
|
|
|
let (base_ar_idx, quote_ar_idx) = get_base_and_quote_ar_idxs(
|
|
|
|
|
the_raw_pair_keys,
|
|
|
|
|
&txn,
|
|
|
|
@ -97,27 +53,19 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
&acct_map
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Unlike all logic following this `TxHasMargin::TwoARs` section, both `action record`s are handled at once.
|
|
|
|
|
let base_ar = ar_map.get(&base_ar_idx).unwrap();
|
|
|
|
|
let quote_ar = ar_map.get("e_ar_idx).unwrap();
|
|
|
|
|
|
|
|
|
|
let mut base_acct_lot_list = base_acct.list_of_lots.borrow_mut();
|
|
|
|
|
let mut quote_acct_lot_list = quote_acct.list_of_lots.borrow_mut();
|
|
|
|
|
|
|
|
|
|
// The number of `lot`s between the quote and base `account`s should always be equal, and there is
|
|
|
|
|
// an error in the code if they are not, so it is meant to panic if so.
|
|
|
|
|
let base_number_of_lots = base_acct_lot_list.len() as u32;
|
|
|
|
|
let quote_number_of_lots = quote_acct_lot_list.len() as u32;
|
|
|
|
|
assert_eq!(base_number_of_lots, quote_number_of_lots, "");
|
|
|
|
|
|
|
|
|
|
// The value is set just below. We use this to determine whether to create a new `lot` for each `account`.
|
|
|
|
|
let acct_balances_are_zero: bool;
|
|
|
|
|
|
|
|
|
|
// Though checking both is redundant, we keep the redundancy for code clarity sake. If true, the implication
|
|
|
|
|
// is that there has already been activity in the margin `account`s in this `transaction`.
|
|
|
|
|
if !base_acct_lot_list.is_empty() && !quote_acct_lot_list.is_empty() {
|
|
|
|
|
// Since we know there has been activity, we set the bool variable above according to whether the `account`
|
|
|
|
|
// balances are both zero.
|
|
|
|
|
let base_balance_is_zero = base_acct_lot_list.last().unwrap().get_sum_of_amts_in_lot() == d128!(0);
|
|
|
|
|
let quote_balance_is_zero = quote_acct_lot_list.last().unwrap().get_sum_of_amts_in_lot() == d128!(0);
|
|
|
|
|
if base_balance_is_zero && quote_balance_is_zero {
|
|
|
|
@ -125,9 +73,6 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
} else {
|
|
|
|
|
acct_balances_are_zero = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If there has supposedly been no activity in the two margin `account`s in this `transaction`, we double
|
|
|
|
|
// check that there are no `lot`s associated with either `account`, and then set the bool variable accordingly.
|
|
|
|
|
} else {
|
|
|
|
|
assert_eq!(true, base_acct_lot_list.is_empty(),
|
|
|
|
|
"One margin account's list_of_lots is empty, but its pair's isn't.");
|
|
|
|
@ -136,14 +81,12 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
acct_balances_are_zero = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The `lot` for each `account` is allocated here, and their values are set within other scopes underneath
|
|
|
|
|
let base_lot: Rc<Lot>;
|
|
|
|
|
let quote_lot: Rc<Lot>;
|
|
|
|
|
|
|
|
|
|
// If both `account`s have zero balances, new `lot`s are created. The variables created above will take
|
|
|
|
|
// the assignment.
|
|
|
|
|
if acct_balances_are_zero {
|
|
|
|
|
base_lot = Rc::new(
|
|
|
|
|
base_lot =
|
|
|
|
|
Rc::new(
|
|
|
|
|
Lot {
|
|
|
|
|
date_as_string: txn.date_as_string.clone(),
|
|
|
|
|
date_of_first_mvmt_in_lot: txn.date,
|
|
|
|
@ -152,8 +95,10 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
account_key: the_raw_pair_keys.0,
|
|
|
|
|
movements: RefCell::new([].to_vec()),
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
quote_lot = Rc::new(
|
|
|
|
|
)
|
|
|
|
|
;
|
|
|
|
|
quote_lot =
|
|
|
|
|
Rc::new(
|
|
|
|
|
Lot {
|
|
|
|
|
date_as_string: txn.date_as_string.clone(),
|
|
|
|
|
date_of_first_mvmt_in_lot: txn.date,
|
|
|
|
@ -162,18 +107,13 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
account_key: the_raw_pair_keys.1,
|
|
|
|
|
movements: RefCell::new([].to_vec()),
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// If at least one `account` has a balance, then each must have at least one `lot`, and those are the `lot`s
|
|
|
|
|
// that will be assigned to the variables above. If a `lot` can't be found, the data is wrong or the code is
|
|
|
|
|
// wrong, and the program should panic.
|
|
|
|
|
)
|
|
|
|
|
;
|
|
|
|
|
} else {
|
|
|
|
|
base_lot = base_acct_lot_list.last().expect("Couldn't get lot. Base acct lot list empty?").clone();
|
|
|
|
|
quote_lot = quote_acct_lot_list.last().expect("Couldn't get lot. Quote acct lot list empty?").clone();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now that each of the `lot`s is chosen, the `movement`s can be created (which contain the `lot` number)
|
|
|
|
|
// and pushed onto their respective `lot`s.
|
|
|
|
|
let base_mvmt = Movement {
|
|
|
|
|
amount: base_ar.amount,
|
|
|
|
|
date_as_string: txn.date_as_string.clone(),
|
|
|
|
@ -206,19 +146,12 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
};
|
|
|
|
|
wrap_mvmt_and_push(quote_mvmt, "e_ar, "e_lot, &chosen_home_currency, &raw_acct_map, &acct_map);
|
|
|
|
|
|
|
|
|
|
// Self-explanatory. If new `lot`s were created, those `lot`s need to be pushed onto the `account`s.
|
|
|
|
|
if acct_balances_are_zero {
|
|
|
|
|
base_acct_lot_list.push(base_lot);
|
|
|
|
|
quote_acct_lot_list.push(quote_lot);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Once the `movement`s have been created and pushed to the appropriate `lot` (and the `lot` pushed to the appropriate
|
|
|
|
|
// `account` if need be), then the transaction has been processed, and it can move onto the next.
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
// If this isn't a margin `exchange` `transaction`, then the lot rules are different, and it continues below.
|
|
|
|
|
} else {
|
|
|
|
|
// Unlike all logic above, in the `TxHasMargin::TwoARs` section, each `action record` is handled one at a time.
|
|
|
|
|
for ar_num in txn.action_record_idx_vec.iter() {
|
|
|
|
|
let ar = ar_map.get(ar_num).unwrap();
|
|
|
|
|
|
|
|
|
@ -226,16 +159,10 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
let raw_acct = raw_acct_map.get(&acct.raw_key).unwrap();
|
|
|
|
|
let length_of_list_of_lots = acct.list_of_lots.borrow().len();
|
|
|
|
|
|
|
|
|
|
// Each home currency `account` contains a single `lot`. There is no restriction on its balance, unlike for
|
|
|
|
|
// crypto `account`s which must always have a non-negative balance (except for margin `account`s, where one `account`
|
|
|
|
|
// must necessarily go negative as the other goes postive).
|
|
|
|
|
if raw_acct.is_home_currency(&chosen_home_currency) {
|
|
|
|
|
let lot;
|
|
|
|
|
let new_lot_created;
|
|
|
|
|
|
|
|
|
|
// If there is no `lot`, create a new one. If there is one, use it.
|
|
|
|
|
if length_of_list_of_lots == 0 {
|
|
|
|
|
lot = Rc::new(
|
|
|
|
|
let lot =
|
|
|
|
|
Rc::new(
|
|
|
|
|
Lot {
|
|
|
|
|
date_as_string: txn.date_as_string.clone(),
|
|
|
|
|
date_of_first_mvmt_in_lot: txn.date,
|
|
|
|
@ -244,16 +171,8 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
account_key: acct.raw_key,
|
|
|
|
|
movements: RefCell::new([].to_vec()),
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
new_lot_created = true;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
assert_eq!(1, length_of_list_of_lots); // Only true for home currency
|
|
|
|
|
lot = acct.list_of_lots.borrow_mut()[0 as usize].clone();
|
|
|
|
|
new_lot_created = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Then create the movement and push it.
|
|
|
|
|
)
|
|
|
|
|
;
|
|
|
|
|
let whole_mvmt = Movement {
|
|
|
|
|
amount: ar.amount,
|
|
|
|
|
date_as_string: txn.date_as_string.clone(),
|
|
|
|
@ -269,35 +188,39 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
cost_basis_lk: Cell::new(d128!(0.0)),
|
|
|
|
|
};
|
|
|
|
|
wrap_mvmt_and_push(whole_mvmt, &ar, &lot, &chosen_home_currency, &raw_acct_map, &acct_map);
|
|
|
|
|
|
|
|
|
|
// If there is a new `lot`, push it onto the `account`
|
|
|
|
|
if new_lot_created { acct.list_of_lots.borrow_mut().push(lot); }
|
|
|
|
|
|
|
|
|
|
// Whether incoming or outgoing, the home currency `action record` is now recorded correctly, and then
|
|
|
|
|
// onto the next `action record` or `transaction`.
|
|
|
|
|
acct.list_of_lots.borrow_mut().push(lot);
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
// Below here, every `action record`'s `account` is not home currency, so the program must know whether
|
|
|
|
|
// `action record` is incoming/outgoing and whether the `transaction` `TxType` is `Exchange`/`ToSelf`/`Flow`.
|
|
|
|
|
else {
|
|
|
|
|
assert_eq!(1, length_of_list_of_lots); // Only true for home currency
|
|
|
|
|
let lot = acct.list_of_lots.borrow_mut()[0 as usize].clone();
|
|
|
|
|
let whole_mvmt = Movement {
|
|
|
|
|
amount: ar.amount,
|
|
|
|
|
date_as_string: txn.date_as_string.clone(),
|
|
|
|
|
date: txn.date,
|
|
|
|
|
transaction_key: txn_num,
|
|
|
|
|
action_record_key: *ar_num,
|
|
|
|
|
cost_basis: Cell::new(d128!(0.0)),
|
|
|
|
|
ratio_of_amt_to_incoming_mvmts_in_a_r: d128!(1.0),
|
|
|
|
|
ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)),
|
|
|
|
|
lot_num: lot.lot_number,
|
|
|
|
|
proceeds: Cell::new(d128!(0.0)),
|
|
|
|
|
proceeds_lk: Cell::new(d128!(0.0)),
|
|
|
|
|
cost_basis_lk: Cell::new(d128!(0.0)),
|
|
|
|
|
};
|
|
|
|
|
wrap_mvmt_and_push(whole_mvmt, &ar, &lot, &chosen_home_currency, &raw_acct_map, &acct_map);
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Note: a_r is not in home currency if here or below
|
|
|
|
|
let polarity = ar.direction();
|
|
|
|
|
let tx_type = txn.transaction_type(&ar_map, &raw_acct_map, &acct_map)?;
|
|
|
|
|
|
|
|
|
|
// The `action record` handling is different depending on whether it's incoming or outgoing.
|
|
|
|
|
match polarity {
|
|
|
|
|
Polarity::Outgoing => {
|
|
|
|
|
// println!("Txn: {}, outgoing {:?}-type of {} {}",
|
|
|
|
|
// txn.tx_number, txn.transaction_type(), ar.amount, acct.ticker);
|
|
|
|
|
//
|
|
|
|
|
// For an outgoing `action record` with a margin `account`, it can be deduced that there is a corresponding
|
|
|
|
|
// incoming `action record` with a non-margin `account.` This setup (two `action record`s where one's `account`
|
|
|
|
|
// is home currency and the other's isn't) is referred to in this context as a dual-`action record` `flow`
|
|
|
|
|
// `transaction`. In this case (with an outgoing `action record` with the margin `account`), it is a margin
|
|
|
|
|
// profit `transaction` since the corresponding incoming `action record` increases a non-margin `account` balance.
|
|
|
|
|
//
|
|
|
|
|
// In order to withdraw margin profits, the margin base `account` must have a zero balance, and the margin quote
|
|
|
|
|
// `account` must have a positive balance. We know, therefore, that the `account` of this `action record` is the
|
|
|
|
|
// quote `account`, and the `lot` treatment is simple. A single `movement` posts to the active `lot` in this
|
|
|
|
|
// `account`, presumably (but not definitely) zeroing it out.
|
|
|
|
|
if raw_acct.is_margin {
|
|
|
|
|
let this_acct = acct_map.get(&ar.account_key).unwrap();
|
|
|
|
|
let lot = this_acct.list_of_lots.borrow().last()
|
|
|
|
@ -318,9 +241,6 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
};
|
|
|
|
|
wrap_mvmt_and_push(whole_mvmt, &ar, &lot, &chosen_home_currency, &raw_acct_map, &acct_map);
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
// For an outgoing `action record` with a non-margin `account`, this is where it is determined how to split
|
|
|
|
|
// the amount (if needed) into `movements` that "fit into" `lot`s.
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
if acct.list_of_lots.borrow().len() == 0 {
|
|
|
|
@ -330,7 +250,7 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
|
|
|
|
|
let list_of_lots_to_use = acct.list_of_lots.clone();
|
|
|
|
|
|
|
|
|
|
// The following returns a Vec to be iterated from beginning to end. It provides the index for the desired `lot`.
|
|
|
|
|
// the following returns vec to be iterated from beginning to end, which provides the index for the correct lot
|
|
|
|
|
let vec_of_ordered_index_values = match chosen_costing_method {
|
|
|
|
|
InventoryCostingMethod::LIFObyLotCreationDate => {
|
|
|
|
|
get_lifo_by_creation_date(&list_of_lots_to_use.borrow())}
|
|
|
|
@ -347,7 +267,8 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
for (idx, _lot) in list_of_lots.iter().enumerate() {
|
|
|
|
|
vec_of_indexes.insert(0, idx)
|
|
|
|
|
}
|
|
|
|
|
vec_of_indexes
|
|
|
|
|
let vec = vec_of_indexes;
|
|
|
|
|
vec
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_lifo_by_lot_basis_date(list_of_lots: &Ref<Vec<Rc<Lot>>>) -> Vec<usize> {
|
|
|
|
@ -364,7 +285,8 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
for (idx, _lot) in reordered_vec.iter().enumerate() {
|
|
|
|
|
vec_of_indexes.insert(0, idx)
|
|
|
|
|
}
|
|
|
|
|
vec_of_indexes
|
|
|
|
|
let vec = vec_of_indexes;
|
|
|
|
|
vec
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_fifo_by_creation_date(list_of_lots: &Ref<Vec<Rc<Lot>>>) -> Vec<usize> {
|
|
|
|
@ -372,7 +294,8 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
for (idx, _lot) in list_of_lots.iter().enumerate() {
|
|
|
|
|
vec_of_indexes.push(idx)
|
|
|
|
|
}
|
|
|
|
|
vec_of_indexes
|
|
|
|
|
let vec = vec_of_indexes;
|
|
|
|
|
vec
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_fifo_by_lot_basis_date(list_of_lots: &Ref<Vec<Rc<Lot>>>) -> Vec<usize> {
|
|
|
|
@ -389,21 +312,13 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
for (idx, _lot) in reordered_vec.iter().enumerate() {
|
|
|
|
|
vec_of_indexes.push(idx)
|
|
|
|
|
}
|
|
|
|
|
vec_of_indexes
|
|
|
|
|
let vec = vec_of_indexes;
|
|
|
|
|
vec
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: Consider whether a for-loop can track the index more cleanly
|
|
|
|
|
// Now that the index values of each `lot` are in the appropriate order, the starting point (index 0)
|
|
|
|
|
// and the starting lot_index can be chosen in preparation for the recursive `fit_into_lots` function.
|
|
|
|
|
// If the tentative `movement` must be reduced to fit into the tentative `lot`, a revised `movement` will be created
|
|
|
|
|
// using an amount that will be reduced to the exact amount to fit into the `lot`. After the revised `movement`
|
|
|
|
|
// is pushed to the `lot`, the index position will be incremented to provide a new `lot_index`, a new tentative
|
|
|
|
|
// `lot` will be chosen, and the remainder of the amount will be used in a new tentative `movement`, and so on
|
|
|
|
|
// until the entire `action record` amount has been put into a `movement` and posted to a `lot`.
|
|
|
|
|
let index_position: usize = 0;
|
|
|
|
|
let lot_index = vec_of_ordered_index_values[index_position];
|
|
|
|
|
|
|
|
|
|
// Now that the tentative `lot` can be chosen, it is, and a tentative `movement` is created.
|
|
|
|
|
let lot_to_use = list_of_lots_to_use.borrow()[lot_index].clone();
|
|
|
|
|
let whole_mvmt = Movement {
|
|
|
|
|
amount: ar.amount,
|
|
|
|
@ -420,7 +335,6 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
cost_basis_lk: Cell::new(d128!(0.0)),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Beginning here, it will recursively attempt to fit the outgoing amount into `lot`s.
|
|
|
|
|
fit_into_lots(
|
|
|
|
|
txn_num,
|
|
|
|
|
*ar_num,
|
|
|
|
@ -433,43 +347,21 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
&raw_acct_map,
|
|
|
|
|
&acct_map,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Once the `action record`'s outgoing amount has been "consumed", the recording of this
|
|
|
|
|
// `action record` is complete.
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Incoming `action records` have different requirements for posting to `lot`s. Unlike for outgoing
|
|
|
|
|
// `action records`, there is often no need to consider how to fit these into lots because in most cases
|
|
|
|
|
// the amount of an incoming `action record` will be in a single movement that posts to a new `lot`.
|
|
|
|
|
// There are three exceptions to this which add many lines of code that aren't terribly easy to read. :)
|
|
|
|
|
// Exception #1: `ToSelf` transactions. Cost basis and basis date must be preserved for currency
|
|
|
|
|
// owned by the user and transferred to another one of their accounts.
|
|
|
|
|
// Exception #2: Like-kind `exchange` `transaction`s must preserve the basis and the basis date
|
|
|
|
|
// of the corresponding outgoing `action record`.
|
|
|
|
|
// Exception #3: Dual-`action record` `flow` `transaction`s that occur during a period of like-kind
|
|
|
|
|
// `exchange` will also inherit an implied/imputed basis date based on the date of the 'buys' in the
|
|
|
|
|
// base margin `account`. The special treatment occurs for an incoming `flow` `action record` whose
|
|
|
|
|
// `account` is non-margin.
|
|
|
|
|
Polarity::Incoming => {
|
|
|
|
|
// println!("Txn: {}, Incoming {:?}-type of {} {}",
|
|
|
|
|
// txn.tx_number, txn.transaction_type(), ar.amount, acct.ticker);
|
|
|
|
|
match tx_type {
|
|
|
|
|
TxType::Flow => {
|
|
|
|
|
let lot: Rc<Lot>;
|
|
|
|
|
let mvmt: Movement;
|
|
|
|
|
// For an incoming `flow` `action record` with a margin account, the implication is that
|
|
|
|
|
// this is a margin loss `transaction`. The corresponding outgoing `flow` `action record`
|
|
|
|
|
// is where the loss is reflected. This `action record` is simply reflecting the transfer
|
|
|
|
|
// of funds into the quote margin account, presumably paying off the loan and bringing it
|
|
|
|
|
// to a zero balance.
|
|
|
|
|
if raw_acct.is_margin {
|
|
|
|
|
let this_acct = acct_map.get(&ar.account_key).unwrap();
|
|
|
|
|
let lot_list = this_acct.list_of_lots.borrow_mut();
|
|
|
|
|
lot = lot_list.last().unwrap().clone();
|
|
|
|
|
|
|
|
|
|
mvmt = Movement {
|
|
|
|
|
let mvmt = Movement {
|
|
|
|
|
amount: ar.amount,
|
|
|
|
|
date_as_string: txn.date_as_string.clone(),
|
|
|
|
|
date: txn.date,
|
|
|
|
@ -484,21 +376,12 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
cost_basis_lk: Cell::new(d128!(0.0)),
|
|
|
|
|
};
|
|
|
|
|
wrap_mvmt_and_push(mvmt, &ar, &lot, &chosen_home_currency, &raw_acct_map, &acct_map);
|
|
|
|
|
|
|
|
|
|
// Since a margin account is being posted new, a new lot is not created. Once the `movement`
|
|
|
|
|
// has been pushed to the `lot`, the recording of the `action record` is complete, and it's
|
|
|
|
|
// onto the next
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
// Now the incoming `flow` `action record`s with a non-margin account are handled.
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
// The base case is a single-`action record` `flow` `transaction` where a `lot` is created (assigned),
|
|
|
|
|
// a `movement` is created (assigned), and the `movement` is pushed to the `lot`. Note that the `lot` variable
|
|
|
|
|
// was allocated above, and this `if` section of code merely assigns this `lot` to that variable.
|
|
|
|
|
// The `lot` isn't pushed to the `account` until after this whole `if/else` section.
|
|
|
|
|
let mvmt: Movement;
|
|
|
|
|
if txn.action_record_idx_vec.len() == 1 {
|
|
|
|
|
lot = Rc::new(
|
|
|
|
|
lot =
|
|
|
|
|
Rc::new(
|
|
|
|
|
Lot {
|
|
|
|
|
date_as_string: txn.date_as_string.clone(),
|
|
|
|
|
date_of_first_mvmt_in_lot: txn.date,
|
|
|
|
@ -508,7 +391,8 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
account_key: acct.raw_key,
|
|
|
|
|
movements: RefCell::new([].to_vec()),
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
)
|
|
|
|
|
;
|
|
|
|
|
mvmt = Movement {
|
|
|
|
|
amount: ar.amount,
|
|
|
|
|
date_as_string: txn.date_as_string.clone(),
|
|
|
|
@ -523,30 +407,12 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
proceeds_lk: Cell::new(d128!(0.0)),
|
|
|
|
|
cost_basis_lk: Cell::new(d128!(0.0)),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// The more complicated case is the dual-`action record` `flow` `transaction`.
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
// A `flow` `transaction` usually has 1 `action record`. In this special case, it'll have 2, but no more.
|
|
|
|
|
assert_eq!(txn.action_record_idx_vec.len(), 2);
|
|
|
|
|
|
|
|
|
|
// The theory in this `if` block is that a series of margin trades culminating in a margin profit during
|
|
|
|
|
// a period of like-kind exchange treatment should/could carry their basis and basis date, just like a traditional
|
|
|
|
|
// trade would. The software allocates the size of the new `movement`s proportionally based on the size of every
|
|
|
|
|
// margin buy in the `lot` in relation to all the margin buys in the the `lot`; and for each `movement` that it
|
|
|
|
|
// creates, that new `movement` is given the basis date of the respective margin-buy's `movement`.
|
|
|
|
|
// (For those savvy, you noted that since margin trades produce no gain/loss, there is no basis to inherit.)
|
|
|
|
|
if multiple_incoming_mvmts_per_ar_due_to_lk && txn.date <= like_kind_cutoff_date {
|
|
|
|
|
|
|
|
|
|
// First, two variables are allocated to hold some intermediate results that will be used to determine the
|
|
|
|
|
// size of `movement`(s) and how many `lot`s are needed.
|
|
|
|
|
// The `positive_mvmt_list` is for accumulating the margin-buy `movement`(s) that occurred during the course
|
|
|
|
|
// of the margin trade that is now ending in a profit. And `total_positive_amounts` accounts for the total
|
|
|
|
|
// amount of those margin-buys.
|
|
|
|
|
// create list of incoming/positive amounts(mvmts) in margin lot, add them
|
|
|
|
|
let mut positive_mvmt_list: Vec<Rc<Movement>> = [].to_vec();
|
|
|
|
|
let mut total_positive_amounts = d128!(0);
|
|
|
|
|
|
|
|
|
|
// This is necessary to find the base account, because the margin-buys are reflected in this account.
|
|
|
|
|
let (base_acct_key, quote_acct_key) = get_base_and_quote_acct_for_dual_actionrecord_flow_tx(
|
|
|
|
|
txn_num,
|
|
|
|
|
&ar_map,
|
|
|
|
@ -557,31 +423,23 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
|
|
|
|
|
let base_acct = acct_map.get(&base_acct_key).unwrap();
|
|
|
|
|
let base_acct_lot = base_acct.list_of_lots.borrow().last().unwrap().clone();
|
|
|
|
|
// It should be apparent that the relevant `lot` has been selected, and its `movement` are now iterated
|
|
|
|
|
// over for capturing its `movement`s (for their date) and adding up their amounts.
|
|
|
|
|
for base_acct_mvmt in base_acct_lot.movements.borrow().iter() {
|
|
|
|
|
if base_acct_mvmt.amount > d128!(0) {
|
|
|
|
|
// TODO: generalize this to work with margin shorts as well
|
|
|
|
|
for mvmt in base_acct_lot.movements.borrow().iter() {
|
|
|
|
|
if mvmt.amount > d128!(0) {
|
|
|
|
|
// println!("In lot# {}, positive mvmt amount: {} {},",
|
|
|
|
|
// base_acct_lot.lot_number,
|
|
|
|
|
// mvmt.borrow().amount,
|
|
|
|
|
// base_acct_lot.account.raw.ticker);
|
|
|
|
|
total_positive_amounts += base_acct_mvmt.amount;
|
|
|
|
|
positive_mvmt_list.push(base_acct_mvmt.clone())
|
|
|
|
|
total_positive_amounts += mvmt.amount;
|
|
|
|
|
positive_mvmt_list.push(mvmt.clone())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// These variables track relevant usage in the following for-loop. These are used after the for-loop
|
|
|
|
|
// when creating the final `movement`.
|
|
|
|
|
let mut amounts_used = d128!(0);
|
|
|
|
|
let mut percentages_used = d128!(0);
|
|
|
|
|
|
|
|
|
|
// Here, the margin-buys are iterated over while creating proportionally-sized new `movement`s.
|
|
|
|
|
// Note that this for-loop excludes the final positive `movement` because rounding must be taken into
|
|
|
|
|
// account (the effect of rounding must be eliminated) when determining the amount of the final `movement`.
|
|
|
|
|
// The `inner_lot` and `inner_mvmt` were named this was to reflect they are created and wrapped/pushed
|
|
|
|
|
// only inside this iteration of `positive_mvmt_list`.
|
|
|
|
|
for pos_mvmt in positive_mvmt_list.iter().take(positive_mvmt_list.len()-1) {
|
|
|
|
|
let inner_lot = Rc::new(
|
|
|
|
|
let inner_lot =
|
|
|
|
|
Rc::new(
|
|
|
|
|
Lot {
|
|
|
|
|
date_as_string: txn.date_as_string.clone(),
|
|
|
|
|
date_of_first_mvmt_in_lot: txn.date,
|
|
|
|
@ -613,20 +471,19 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
percentages_used += percentage_used;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now that the intermediate `lot`s and `movement`s have been taken care of, the `lot` and `movement` that were
|
|
|
|
|
// allocated after matching on `flow` can be assigned the following values, and which will be wrapped and
|
|
|
|
|
// pushed further down.
|
|
|
|
|
let final_pos_mvmt = positive_mvmt_list.last().expect("After exluding last mvmt from for-loop above, expected last mvmt.");
|
|
|
|
|
lot = Rc::new(
|
|
|
|
|
let final_mvmt = positive_mvmt_list.last().unwrap();
|
|
|
|
|
lot =
|
|
|
|
|
Rc::new(
|
|
|
|
|
Lot {
|
|
|
|
|
date_as_string: txn.date_as_string.clone(),
|
|
|
|
|
date_of_first_mvmt_in_lot: txn.date,
|
|
|
|
|
date_for_basis_purposes: final_pos_mvmt.date,
|
|
|
|
|
date_for_basis_purposes: final_mvmt.date,
|
|
|
|
|
lot_number: acct.list_of_lots.borrow().len() as u32 + 1,
|
|
|
|
|
account_key: acct.raw_key,
|
|
|
|
|
movements: RefCell::new([].to_vec()),
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
)
|
|
|
|
|
;
|
|
|
|
|
mvmt = Movement {
|
|
|
|
|
amount: round_d128_1e8(&(ar.amount - amounts_used)),
|
|
|
|
|
date_as_string: txn.date_as_string.clone(),
|
|
|
|
@ -641,56 +498,13 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
proceeds_lk: Cell::new(d128!(0.0)),
|
|
|
|
|
cost_basis_lk: Cell::new(d128!(0.0)),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Back to "base case" style treatment, if this is an incoming dual-`action record` `flow` `transaction`, but either
|
|
|
|
|
// (a) like-kind `exchange` treatment was not elected or (b) the `transaction` date is after the like-kind treatment period,
|
|
|
|
|
// then just a single `movement` is created for eventual pushing into a single new `lot`.
|
|
|
|
|
} else {
|
|
|
|
|
lot = Rc::new(
|
|
|
|
|
Lot {
|
|
|
|
|
date_as_string: txn.date_as_string.clone(),
|
|
|
|
|
date_of_first_mvmt_in_lot: txn.date,
|
|
|
|
|
date_for_basis_purposes: txn.date,
|
|
|
|
|
lot_number: length_of_list_of_lots as u32 + 1,
|
|
|
|
|
account_key: acct.raw_key,
|
|
|
|
|
movements: RefCell::new([].to_vec()),
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
mvmt = Movement {
|
|
|
|
|
amount: ar.amount,
|
|
|
|
|
date_as_string: txn.date_as_string.clone(),
|
|
|
|
|
date: txn.date,
|
|
|
|
|
transaction_key: txn_num,
|
|
|
|
|
action_record_key: *ar_num,
|
|
|
|
|
cost_basis: Cell::new(d128!(0.0)),
|
|
|
|
|
ratio_of_amt_to_incoming_mvmts_in_a_r: d128!(1.0),
|
|
|
|
|
ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)),
|
|
|
|
|
lot_num: lot.lot_number,
|
|
|
|
|
proceeds: Cell::new(d128!(0.0)),
|
|
|
|
|
proceeds_lk: Cell::new(d128!(0.0)),
|
|
|
|
|
cost_basis_lk: Cell::new(d128!(0.0)),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Here, finally, the lot and movement allocated at the top of `match TxType::Flow` have been set
|
|
|
|
|
// and can be wrapped/pushed, at which point this `action record` is complete and it's onto the next.
|
|
|
|
|
wrap_mvmt_and_push(mvmt, &ar, &lot, &chosen_home_currency, &raw_acct_map, &acct_map);
|
|
|
|
|
acct.list_of_lots.borrow_mut().push(lot);
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
TxType::Exchange => {
|
|
|
|
|
|
|
|
|
|
// These will only initialize if the outer `if` or inner `if` resolve to false
|
|
|
|
|
let whole_mvmt;
|
|
|
|
|
let lot;
|
|
|
|
|
|
|
|
|
|
// The first check is for like-kind exchange treatment is applicable to the `transaction`:
|
|
|
|
|
if multiple_incoming_mvmts_per_ar_due_to_lk && (txn.date <= like_kind_cutoff_date) {
|
|
|
|
|
|
|
|
|
|
// If lk is applicable, determine whether to `process_multiple..`,
|
|
|
|
|
// based on if each `action record` has a home currency `account`.
|
|
|
|
|
let both_are_non_home_curr: bool;
|
|
|
|
|
let og_ar = ar_map.get(txn.action_record_idx_vec.first().unwrap()).unwrap();
|
|
|
|
|
let og_acct = acct_map.get(&og_ar.account_key).unwrap();
|
|
|
|
@ -700,7 +514,7 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
both_are_non_home_curr = !og_raw_acct.is_home_currency(&chosen_home_currency)
|
|
|
|
|
&& !ic_raw_acct.is_home_currency(&chosen_home_currency);
|
|
|
|
|
|
|
|
|
|
if both_are_non_home_curr {
|
|
|
|
|
if both_are_non_home_curr && multiple_incoming_mvmts_per_ar && (txn.date <= like_kind_cutoff_date) {
|
|
|
|
|
process_multiple_incoming_lots_and_mvmts(
|
|
|
|
|
txn_num,
|
|
|
|
|
&og_ar,
|
|
|
|
@ -713,40 +527,11 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
&ar_map,
|
|
|
|
|
);
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
// If lk treatment is applicable but one `account` is home currency, then use a single `lot` and `movement`
|
|
|
|
|
} else {
|
|
|
|
|
lot = Rc::new(
|
|
|
|
|
Lot {
|
|
|
|
|
date_as_string: txn.date_as_string.clone(),
|
|
|
|
|
date_of_first_mvmt_in_lot: txn.date,
|
|
|
|
|
date_for_basis_purposes: txn.date,
|
|
|
|
|
lot_number: length_of_list_of_lots as u32 + 1,
|
|
|
|
|
account_key: acct.raw_key,
|
|
|
|
|
movements: RefCell::new([].to_vec()),
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
whole_mvmt = Movement {
|
|
|
|
|
amount: ar.amount,
|
|
|
|
|
date_as_string: txn.date_as_string.clone(),
|
|
|
|
|
date: txn.date,
|
|
|
|
|
transaction_key: txn_num,
|
|
|
|
|
action_record_key: *ar_num,
|
|
|
|
|
cost_basis: Cell::new(d128!(0.0)),
|
|
|
|
|
ratio_of_amt_to_incoming_mvmts_in_a_r: d128!(1.0),
|
|
|
|
|
ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)),
|
|
|
|
|
lot_num: lot.lot_number,
|
|
|
|
|
proceeds: Cell::new(d128!(0.0)),
|
|
|
|
|
proceeds_lk: Cell::new(d128!(0.0)),
|
|
|
|
|
cost_basis_lk: Cell::new(d128!(0.0)),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For an incoming `action record` in an `exchange` `transaction` where there's no like-kind
|
|
|
|
|
// treatment, simply create a new `lot`, create a new `movement`, and wrap/push.
|
|
|
|
|
else {
|
|
|
|
|
lot = Rc::new(
|
|
|
|
|
let lot =
|
|
|
|
|
Rc::new(
|
|
|
|
|
Lot {
|
|
|
|
|
date_as_string: txn.date_as_string.clone(),
|
|
|
|
|
date_of_first_mvmt_in_lot: txn.date,
|
|
|
|
@ -755,8 +540,9 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
account_key: acct.raw_key,
|
|
|
|
|
movements: RefCell::new([].to_vec()),
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
whole_mvmt = Movement {
|
|
|
|
|
)
|
|
|
|
|
;
|
|
|
|
|
let whole_mvmt = Movement {
|
|
|
|
|
amount: ar.amount,
|
|
|
|
|
date_as_string: txn.date_as_string.clone(),
|
|
|
|
|
date: txn.date,
|
|
|
|
@ -770,22 +556,15 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
proceeds_lk: Cell::new(d128!(0.0)),
|
|
|
|
|
cost_basis_lk: Cell::new(d128!(0.0)),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
// The `lot` and `whole_mvmt` variables have been initialized/assigned
|
|
|
|
|
wrap_mvmt_and_push(whole_mvmt, &ar, &lot, &chosen_home_currency, &raw_acct_map, &acct_map);
|
|
|
|
|
acct.list_of_lots.borrow_mut().push(lot);
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
TxType::ToSelf => {
|
|
|
|
|
|
|
|
|
|
// Based on experience, and considering how `transaction`s are constructed, this should never happen.
|
|
|
|
|
if raw_acct.is_margin {
|
|
|
|
|
println!("FATAL: Consult developer. Found margin actionrecord in ToSelf transaction:\n{:#?}", txn);
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
|
|
|
|
|
// When transferring to oneself, the amounts should carry over proportionally (considering the incoming `movement`
|
|
|
|
|
// is likely to be less than the outgoing `movement` due to transaction fees), as should the basis date of each of the
|
|
|
|
|
// outgoing `movement`s.
|
|
|
|
|
} else {
|
|
|
|
|
process_multiple_incoming_lots_and_mvmts(
|
|
|
|
|
txn_num,
|
|
|
|
@ -801,27 +580,29 @@ pub(crate) fn create_lots_and_movements(
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
} // end for match::TxType
|
|
|
|
|
} // end for Polarity::Incoming
|
|
|
|
|
} // end for match::Polarity
|
|
|
|
|
} // end for ar in txn.actionrecords (ar_num in tx.ar_idx_vec)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} // end for ar in txn.actionrecords
|
|
|
|
|
} // end of tx does not have marginness of TwoARs
|
|
|
|
|
} // end for txn in transactions (txn_num in txn_map.len())
|
|
|
|
|
} // end for txn in transactions
|
|
|
|
|
Ok(txns_map)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Preface: this ONLY works for a dual-`action record` `transaction` when the `account` of the incoming
|
|
|
|
|
/// `action record` is a non-margin `account`. Also, we know that the corresponding outgoing `action
|
|
|
|
|
/// record` is the quote account (logically, it must be).
|
|
|
|
|
/// Chooses the outgoing `ActionRecord`. Gets a `Vec` of its `Movement`s. Chooses its
|
|
|
|
|
/// first/earliest `Movement`. Gets that `Movement`'s `ActionRecord`. Gets that
|
|
|
|
|
/// `ActionRecord`'s `Account`. Gets the first `Lot` from that `Account`'s
|
|
|
|
|
/// `list_of_lots`. Get the `Movement`s from that `Lot`. Takes the first `Movement`
|
|
|
|
|
/// and gets that `Movement`'s `Transaction`. Gets base and quote keys from that.println!
|
|
|
|
|
///
|
|
|
|
|
/// This function works off of an incoming `action record` for a non-margin account. It knows the outgoing
|
|
|
|
|
/// `action record` of this `transaction` has a margin account, so it first will
|
|
|
|
|
/// choose that `action record`, and then it'll immediately choose the margin `account` in question.
|
|
|
|
|
/// Since the margin pair never changes (the same base `account` and same quote `account` interact exclusively
|
|
|
|
|
/// with eachother), it can immediately get the first `lot` in the `account` (to ensure it exists).
|
|
|
|
|
/// Then it gets a list of `movement`s in that first `lot`. Then, to ensure it's getting a `transaction`
|
|
|
|
|
/// where both `action record`s have a margin account, it chooses the first `movement`. And then it
|
|
|
|
|
/// takes that `transaction` to analyze for base `account` and quote `account`.
|
|
|
|
|
/// The rationale for this was to be able to determine whether this should be classifiable
|
|
|
|
|
/// as a Long or a Short. As it turns out, the two are basically indistinguishable and/or
|
|
|
|
|
/// interchangeable, so this `fn` should be simplified.
|
|
|
|
|
///
|
|
|
|
|
/// Just kidding. You weren't paying attention, were you? This is a dual `ActionRecord`
|
|
|
|
|
/// `Flow` `Transaction`, meaning this is most likely recording a margin profit or a
|
|
|
|
|
/// margin loss. The `fn` was designed this was to definitely work for a margin profit,
|
|
|
|
|
/// but it might could use reworking to be generalized for margin profit or loss.
|
|
|
|
|
fn get_base_and_quote_acct_for_dual_actionrecord_flow_tx(
|
|
|
|
|
txn_num: u32,
|
|
|
|
|
ar_map: &HashMap<u32, ActionRecord>,
|
|
|
|
@ -833,32 +614,35 @@ fn get_base_and_quote_acct_for_dual_actionrecord_flow_tx(
|
|
|
|
|
let txn = txns_map.get(&txn_num).expect("Couldn't get txn. Tx num invalid?");
|
|
|
|
|
|
|
|
|
|
let og_flow_ar = ar_map.get(txn.action_record_idx_vec.first().unwrap()).unwrap();
|
|
|
|
|
let og_flow_ar_acct = acct_map.get(&og_flow_ar.account_key).unwrap();
|
|
|
|
|
let og_flow_ar_acct_first_lot = &og_flow_ar_acct.list_of_lots.borrow()[0];
|
|
|
|
|
let first_lot_mvmts = og_flow_ar_acct_first_lot.movements.borrow();
|
|
|
|
|
let first_lot_mvmts_first_mvmt = &first_lot_mvmts.first().unwrap();
|
|
|
|
|
let txn_of_first_lot_mvmts_first_mvmt = txns_map.get(&first_lot_mvmts_first_mvmt.transaction_key).unwrap();
|
|
|
|
|
|
|
|
|
|
let (base_key,quote_key) = txn_of_first_lot_mvmts_first_mvmt.get_base_and_quote_raw_acct_keys(
|
|
|
|
|
let og_ar_mvmts_list = &og_flow_ar.get_mvmts_in_ar_in_lot_date_order(acct_map, txns_map);
|
|
|
|
|
let og_ar_list_first_mvmt = &og_ar_mvmts_list.first().unwrap(); // TODO: then this takes the one mvmt
|
|
|
|
|
let og_ar_list_first_mvmt_ar = ar_map.get(&og_ar_list_first_mvmt.action_record_key).unwrap();
|
|
|
|
|
let og_ar_list_first_mvmt_ar_acct = acct_map.get(&og_ar_list_first_mvmt_ar.account_key).unwrap();
|
|
|
|
|
let og_mvmt_lot = &og_ar_list_first_mvmt_ar_acct.list_of_lots.borrow()[(og_ar_list_first_mvmt.lot_num - 1) as usize];
|
|
|
|
|
|
|
|
|
|
let og_mvmt_lot_mvmts = og_mvmt_lot.movements.borrow();
|
|
|
|
|
let og_mvmt_lot_first_mvmt = &og_mvmt_lot_mvmts.first().unwrap();
|
|
|
|
|
let txn_of_og_mvmt_lot_first_mvmt = txns_map.get(&og_mvmt_lot_first_mvmt.transaction_key).unwrap();
|
|
|
|
|
let (base_key,quote_key) = txn_of_og_mvmt_lot_first_mvmt.get_base_and_quote_raw_acct_keys(
|
|
|
|
|
ar_map,
|
|
|
|
|
&raw_acct_map,
|
|
|
|
|
&acct_map)?;
|
|
|
|
|
&acct_map)?; // TODO: should this panic on margin loss? As of 2019-10-02, no. Should test for margin shorting too, though.
|
|
|
|
|
Ok((base_key, quote_key))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the index in the `action records` Hashmap of the base and quote `action record`s.
|
|
|
|
|
fn get_base_and_quote_ar_idxs(
|
|
|
|
|
base_and_quote_keys: (u16,u16),
|
|
|
|
|
pair_keys: (u16,u16),
|
|
|
|
|
txn: &Transaction,
|
|
|
|
|
ars: &HashMap<u32, ActionRecord>,
|
|
|
|
|
raw_acct_map: &HashMap<u16, RawAccount>,
|
|
|
|
|
acct_map: &HashMap<u16, Account>
|
|
|
|
|
) -> (u32, u32) {
|
|
|
|
|
) -> (u32, u32){
|
|
|
|
|
|
|
|
|
|
let incoming_ar = ars.get(&txn.action_record_idx_vec[0]).unwrap();
|
|
|
|
|
let incoming_acct = acct_map.get(&incoming_ar.account_key).unwrap();
|
|
|
|
|
let raw_ic_acct = raw_acct_map.get(&incoming_acct.raw_key).unwrap();
|
|
|
|
|
let compare = raw_acct_map.get(&base_and_quote_keys.0).unwrap(); // key.0 is base, and key.1 is quote
|
|
|
|
|
let compare = raw_acct_map.get(&pair_keys.0).unwrap(); // key.0 is base, and key.1 is quote
|
|
|
|
|
|
|
|
|
|
if raw_ic_acct == compare {
|
|
|
|
|
(txn.action_record_idx_vec[0], txn.action_record_idx_vec[1])
|
|
|
|
@ -867,8 +651,6 @@ fn get_base_and_quote_ar_idxs(
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Every time a new `movement` is created, it must be wrapped in an `Rc` because it is owned both by the
|
|
|
|
|
/// `lot` (which itself is owned by an `account`) and by the `action record` from which it was derived.
|
|
|
|
|
fn wrap_mvmt_and_push(
|
|
|
|
|
this_mvmt: Movement,
|
|
|
|
|
ar: &ActionRecord,
|
|
|
|
@ -881,19 +663,11 @@ fn wrap_mvmt_and_push(
|
|
|
|
|
let acct = acct_map.get(&ar.account_key).unwrap();
|
|
|
|
|
let raw_acct = raw_acct_map.get(&acct.raw_key).unwrap();
|
|
|
|
|
|
|
|
|
|
// For outgoing `action record`s, this is an optimal spot for setting this struct field. Interestingly,
|
|
|
|
|
// at the time of writing this note, this ratio isn't actually used. The `ratio_of_amt_to_incoming_mvmts_in_a_r`
|
|
|
|
|
// field, by contrast, is extremely important when deterministically setting basis and proceeds.
|
|
|
|
|
// TODO: Consider commenting or deleting this code block (and the two vars above).
|
|
|
|
|
if ar.direction() == Polarity::Outgoing && !raw_acct.is_home_currency(chosen_home_currency) {
|
|
|
|
|
let ratio = this_mvmt.amount / ar.amount;
|
|
|
|
|
this_mvmt.ratio_of_amt_to_outgoing_mvmts_in_a_r.set(round_d128_1e8(&ratio));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// This is the type of thing that should probably be changed into a test or an operation that is only
|
|
|
|
|
// run under certain circumstances, since it is double-checking something that prior code should have
|
|
|
|
|
// already taken care of, which is that the value has been rounded to 8 digits of precision already.
|
|
|
|
|
// TODO: Consider deleting this assertion.
|
|
|
|
|
let amt = this_mvmt.amount;
|
|
|
|
|
let amt2 = round_d128_1e8(&amt);
|
|
|
|
|
assert_eq!(amt, amt2);
|
|
|
|
@ -903,11 +677,6 @@ fn wrap_mvmt_and_push(
|
|
|
|
|
ar.movements.borrow_mut().push(mvmt);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Recursively check the balance in a `lot`, and if not zero then create a `movement` that is the lesser of
|
|
|
|
|
/// the `mvmt_to_fit` or the balance of the `lot`; and if the `lot` balance is smaller than the amount of
|
|
|
|
|
/// the `mvmt_to_fit`, then create a `movement` that will fit into that `lot` and push it to that `lot`; and
|
|
|
|
|
/// then select the next `lot` and replace the `mvmt_to_fit` with a new `mvmt_to_fit` (reduced by the one
|
|
|
|
|
/// that was pushed to the previous `lot`), and recursively check...
|
|
|
|
|
fn fit_into_lots(
|
|
|
|
|
txn_num: u32,
|
|
|
|
|
spawning_ar_key: u32,
|
|
|
|
@ -921,16 +690,16 @@ fn fit_into_lots(
|
|
|
|
|
acct_map: &HashMap<u16, Account>,
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
let spawning_ar = ar_map.get(&spawning_ar_key).unwrap();
|
|
|
|
|
|
|
|
|
|
let acct = acct_map.get(&spawning_ar.account_key).unwrap();
|
|
|
|
|
let ar = ar_map.get(&spawning_ar_key).unwrap();
|
|
|
|
|
let acct = acct_map.get(&ar.account_key).unwrap();
|
|
|
|
|
let raw_acct = raw_acct_map.get(&acct.raw_key).unwrap();
|
|
|
|
|
assert_eq!(raw_acct.is_home_currency(&chosen_home_currency), false);
|
|
|
|
|
|
|
|
|
|
let spawning_ar = ar_map.get(&spawning_ar_key).unwrap();
|
|
|
|
|
let mut current_index_position = index_position;
|
|
|
|
|
|
|
|
|
|
// Get the `lot`, and then get its balance to see how much room there is
|
|
|
|
|
let lot = mvmt_to_fit.get_lot(acct_map, ar_map);
|
|
|
|
|
|
|
|
|
|
let mut mut_sum_of_mvmts_in_lot: d128 = d128!(0.0);
|
|
|
|
|
for movement in lot.movements.borrow().iter() {
|
|
|
|
|
mut_sum_of_mvmts_in_lot += movement.amount;
|
|
|
|
@ -938,8 +707,7 @@ fn fit_into_lots(
|
|
|
|
|
let sum_of_mvmts_in_lot = mut_sum_of_mvmts_in_lot;
|
|
|
|
|
assert!(sum_of_mvmts_in_lot >= d128!(0.0));
|
|
|
|
|
|
|
|
|
|
// If the `lot` is "full", try the next
|
|
|
|
|
if sum_of_mvmts_in_lot == d128!(0.0) {
|
|
|
|
|
if sum_of_mvmts_in_lot == d128!(0.0) { // If the lot is "full", go to the next
|
|
|
|
|
current_index_position += 1;
|
|
|
|
|
assert!(current_index_position < vec_of_ordered_index_values.len());
|
|
|
|
|
let lot_index = vec_of_ordered_index_values[current_index_position];
|
|
|
|
@ -972,13 +740,11 @@ fn fit_into_lots(
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// There is a balance in the `lot`, so check if the tentative amount of the `movement` will fit
|
|
|
|
|
assert!(sum_of_mvmts_in_lot > d128!(0.0));
|
|
|
|
|
let remainder_amt = mvmt_to_fit.amount;
|
|
|
|
|
|
|
|
|
|
let does_remainder_fit: bool = (sum_of_mvmts_in_lot + remainder_amt) >= d128!(0.0);
|
|
|
|
|
|
|
|
|
|
// If the remainder fits, the `movement` is wrapped/pushed, and the recursion is complete
|
|
|
|
|
if does_remainder_fit {
|
|
|
|
|
let remainder_that_fits = Movement {
|
|
|
|
|
amount: mvmt_to_fit.amount,
|
|
|
|
@ -995,11 +761,9 @@ fn fit_into_lots(
|
|
|
|
|
cost_basis_lk: Cell::new(d128!(0.0)),
|
|
|
|
|
};
|
|
|
|
|
wrap_mvmt_and_push(remainder_that_fits, &spawning_ar, &lot, &chosen_home_currency, &raw_acct_map, &acct_map);
|
|
|
|
|
return
|
|
|
|
|
return // And we're done
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The `movement` doesn't fit in the present single `lot`, but some does. Create a `movement` that will fit,
|
|
|
|
|
// wrap/push it, and then continue to handle the remainder.
|
|
|
|
|
// Note: at this point, we know the movement doesn't fit in a single lot & sum_of_mvmts_in_lot > 0
|
|
|
|
|
let mvmt = RefCell::new(mvmt_to_fit);
|
|
|
|
|
let mvmt_rc = Rc::from(mvmt);
|
|
|
|
|
|
|
|
|
@ -1018,13 +782,13 @@ fn fit_into_lots(
|
|
|
|
|
cost_basis_lk: Cell::new(d128!(0.0)),
|
|
|
|
|
};
|
|
|
|
|
wrap_mvmt_and_push(mvmt_that_fits_in_lot, &spawning_ar, &lot, &chosen_home_currency, &raw_acct_map, &acct_map);
|
|
|
|
|
let remainder_amt_to_recurse = remainder_amt + sum_of_mvmts_in_lot;
|
|
|
|
|
// println!("Remainder amount to recurse: {}", remainder_amt_to_recurse);
|
|
|
|
|
|
|
|
|
|
if vec_of_ordered_index_values.len() == current_index_position + 1 {
|
|
|
|
|
println!("FATAL: Txn {} on {} spending {} {} has run out of lots to spend from.",
|
|
|
|
|
txn_num, lot.date_as_string, spawning_ar.amount, raw_acct.ticker);
|
|
|
|
|
let bal = if acct.get_sum_of_amts_in_lots() == d128!(0) { "0.00000000".to_string() }
|
|
|
|
|
else { acct.get_sum_of_amts_in_lots().to_string() };
|
|
|
|
|
println!("Account balance is only: {}", bal);
|
|
|
|
|
txn_num, lot.date_as_string, ar.amount, raw_acct.ticker);
|
|
|
|
|
println!("Account balance is only: {}", acct.get_sum_of_amts_in_lots());
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -1032,10 +796,6 @@ fn fit_into_lots(
|
|
|
|
|
let lot_index = vec_of_ordered_index_values[current_index_position];
|
|
|
|
|
let newly_chosen_lot = list_of_lots_to_use.borrow()[lot_index].clone();
|
|
|
|
|
|
|
|
|
|
// Take tentative `movement` amount (negative) and sum with the amount just used from the
|
|
|
|
|
// `mvmt_that_fits_in_lot` to come up with the unused portion of this `action record` amount.
|
|
|
|
|
let remainder_amt_to_recurse = remainder_amt + sum_of_mvmts_in_lot;
|
|
|
|
|
// println!("Remainder amount to recurse: {}", remainder_amt_to_recurse);
|
|
|
|
|
let remainder_mvmt_to_recurse = Movement {
|
|
|
|
|
amount: remainder_amt_to_recurse.reduce(),
|
|
|
|
|
date_as_string: mvmt_rc.borrow().date_as_string.clone(),
|
|
|
|
@ -1050,8 +810,7 @@ fn fit_into_lots(
|
|
|
|
|
proceeds_lk: Cell::new(d128!(0.0)),
|
|
|
|
|
cost_basis_lk: Cell::new(d128!(0.0)),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// After applying some of the `action record`'s amount to another `lot`, take the remainder and recurse
|
|
|
|
|
assert!(current_index_position < vec_of_ordered_index_values.len());
|
|
|
|
|
fit_into_lots(
|
|
|
|
|
txn_num,
|
|
|
|
|
spawning_ar_key,
|
|
|
|
@ -1066,9 +825,6 @@ fn fit_into_lots(
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// This is for the surprisingly common occasion (not surprising once you think about it) when an
|
|
|
|
|
/// incoming `action record` must be split into multiple `movement`s and therefore multiple `lot`s.
|
|
|
|
|
/// This happens every time a user transfers from one account of theirs to another.
|
|
|
|
|
fn process_multiple_incoming_lots_and_mvmts(
|
|
|
|
|
txn_num: u32,
|
|
|
|
|
outgoing_ar: &ActionRecord,
|
|
|
|
@ -1090,10 +846,11 @@ fn process_multiple_incoming_lots_and_mvmts(
|
|
|
|
|
let mut all_but_last_incoming_mvmt_ratio = d128!(0.0);
|
|
|
|
|
// println!("Txn date: {}. Outgoing mvmts: {}, Outgoing amount: {}", txn.date, outgoing_ar.movements.borrow().len(), outgoing_ar.amount);
|
|
|
|
|
let list_of_mvmts_of_outgoing_ar = outgoing_ar.get_mvmts_in_ar_in_lot_date_order(acct_map, txns_map);
|
|
|
|
|
let list_of_mvmts_of_outgoing_ar_len = list_of_mvmts_of_outgoing_ar.len();
|
|
|
|
|
let final_og_mvmt = list_of_mvmts_of_outgoing_ar.last().unwrap();
|
|
|
|
|
let final_mvmt = list_of_mvmts_of_outgoing_ar.last().unwrap();
|
|
|
|
|
// First iteration, for all but final movement
|
|
|
|
|
for outgoing_mvmt in list_of_mvmts_of_outgoing_ar.iter().take(list_of_mvmts_of_outgoing_ar_len - 1) {
|
|
|
|
|
for outgoing_mvmt in list_of_mvmts_of_outgoing_ar
|
|
|
|
|
.iter()
|
|
|
|
|
.take(outgoing_ar.get_mvmts_in_ar_in_lot_date_order(acct_map, txns_map).len() - 1) {
|
|
|
|
|
let ratio_of_outgoing_mvmt_to_total_ar = outgoing_mvmt.amount / outgoing_ar.amount; // Negative divided by negative is positive
|
|
|
|
|
// println!("Ratio of outgoing amt to total actionrecord amt: {:.8}", ratio_of_outgoing_to_total_ar);
|
|
|
|
|
let tentative_incoming_amt = ratio_of_outgoing_mvmt_to_total_ar * incoming_ar.amount;
|
|
|
|
@ -1105,7 +862,7 @@ fn process_multiple_incoming_lots_and_mvmts(
|
|
|
|
|
let this_acct = acct_of_incoming_ar;
|
|
|
|
|
let length_of_list_of_lots: usize = this_acct.list_of_lots.borrow().len();
|
|
|
|
|
let inherited_date = outgoing_mvmt.get_lot(acct_map, ar_map).date_of_first_mvmt_in_lot;
|
|
|
|
|
let inner_lot =
|
|
|
|
|
let lot =
|
|
|
|
|
Rc::new(
|
|
|
|
|
Lot {
|
|
|
|
|
date_as_string: txn.date_as_string.clone(),
|
|
|
|
@ -1126,7 +883,7 @@ fn process_multiple_incoming_lots_and_mvmts(
|
|
|
|
|
cost_basis: Cell::new(d128!(0.0)),
|
|
|
|
|
ratio_of_amt_to_incoming_mvmts_in_a_r: round_d128_1e8(&ratio_of_outgoing_mvmt_to_total_ar),
|
|
|
|
|
ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)),
|
|
|
|
|
lot_num: inner_lot.lot_number,
|
|
|
|
|
lot_num: lot.lot_number,
|
|
|
|
|
proceeds: Cell::new(d128!(0.0)),
|
|
|
|
|
proceeds_lk: Cell::new(d128!(0.0)),
|
|
|
|
|
cost_basis_lk: Cell::new(d128!(0.0)),
|
|
|
|
@ -1135,15 +892,15 @@ fn process_multiple_incoming_lots_and_mvmts(
|
|
|
|
|
// incoming_mvmt.amount, acct_incoming_ar.ticker, acct_incoming_ar.account_num);
|
|
|
|
|
all_but_last_incoming_mvmt_ratio += round_d128_1e8(&ratio_of_outgoing_mvmt_to_total_ar);
|
|
|
|
|
all_but_last_incoming_mvmt_amt += incoming_mvmt.amount;
|
|
|
|
|
wrap_mvmt_and_push(incoming_mvmt, &incoming_ar, &inner_lot, &chosen_home_currency, &raw_acct_map, &acct_map);
|
|
|
|
|
this_acct.list_of_lots.borrow_mut().push(inner_lot);
|
|
|
|
|
wrap_mvmt_and_push(incoming_mvmt, &incoming_ar, &lot, &chosen_home_currency, &raw_acct_map, &acct_map);
|
|
|
|
|
this_acct.list_of_lots.borrow_mut().push(lot);
|
|
|
|
|
}
|
|
|
|
|
// Second iteration, for final movement
|
|
|
|
|
let corresponding_incoming_amt = incoming_ar.amount - all_but_last_incoming_mvmt_amt;
|
|
|
|
|
assert!(corresponding_incoming_amt > d128!(0.0));
|
|
|
|
|
let this_acct = acct_of_incoming_ar;
|
|
|
|
|
let length_of_list_of_lots = this_acct.list_of_lots.borrow().len();
|
|
|
|
|
let inherited_date = final_og_mvmt.get_lot(acct_map, ar_map).date_of_first_mvmt_in_lot;
|
|
|
|
|
let inherited_date = final_mvmt.get_lot(acct_map, ar_map).date_of_first_mvmt_in_lot;
|
|
|
|
|
let lot =
|
|
|
|
|
Rc::new(
|
|
|
|
|
Lot {
|
|
|
|
|