From 3c7e01a42c36d29e870d63bc4afb0fa21fc2e553 Mon Sep 17 00:00:00 2001 From: scoobybejesus Date: Thu, 26 Nov 2020 10:10:33 -0500 Subject: [PATCH] Documented how create_lots_and_movements() works. Completed documenting create_lots_and_movements() --- Cargo.toml | 2 +- src/crptls_lib/create_lots_mvmts.rs | 649 +++++++++++++++++++--------- 2 files changed, 447 insertions(+), 204 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6774110..41951f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cryptools" -version = "0.9.2" +version = "0.9.3" authors = ["scoobybejesus "] edition = "2018" description = "Command-line utility for processing cryptocurrency transactions into 'lots' and 'movements'." diff --git a/src/crptls_lib/create_lots_mvmts.rs b/src/crptls_lib/create_lots_mvmts.rs index 9f074d9..d9d6a68 100644 --- a/src/crptls_lib/create_lots_mvmts.rs +++ b/src/crptls_lib/create_lots_mvmts.rs @@ -14,6 +14,37 @@ 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, @@ -29,14 +60,25 @@ 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; - let multiple_incoming_mvmts_per_ar = 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; - // 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); @@ -45,6 +87,8 @@ 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, @@ -53,19 +97,27 @@ 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 { @@ -73,6 +125,9 @@ 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."); @@ -81,12 +136,14 @@ 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; let quote_lot: Rc; + // 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, @@ -95,10 +152,8 @@ 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, @@ -107,13 +162,18 @@ 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(), @@ -146,12 +206,19 @@ 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(); @@ -159,10 +226,16 @@ 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 { - let lot = - Rc::new( + lot = Rc::new( Lot { date_as_string: txn.date_as_string.clone(), date_of_first_mvmt_in_lot: txn.date, @@ -171,56 +244,60 @@ pub(crate) fn create_lots_and_movements( account_key: acct.raw_key, movements: RefCell::new([].to_vec()), } - ) - ; - 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); - acct.list_of_lots.borrow_mut().push(lot); - continue + ); + new_lot_created = true; } 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 + 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(), + 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); + + // 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`. + continue } - // Note: a_r is not in home currency if here or below + // 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`. 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() @@ -241,6 +318,9 @@ 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 { @@ -250,7 +330,7 @@ pub(crate) fn create_lots_and_movements( let list_of_lots_to_use = acct.list_of_lots.clone(); - // the following returns vec to be iterated from beginning to end, which provides the index for the correct lot + // The following returns a Vec to be iterated from beginning to end. It provides the index for the desired `lot`. let vec_of_ordered_index_values = match chosen_costing_method { InventoryCostingMethod::LIFObyLotCreationDate => { get_lifo_by_creation_date(&list_of_lots_to_use.borrow())} @@ -267,8 +347,7 @@ pub(crate) fn create_lots_and_movements( for (idx, _lot) in list_of_lots.iter().enumerate() { vec_of_indexes.insert(0, idx) } - let vec = vec_of_indexes; - vec + vec_of_indexes } fn get_lifo_by_lot_basis_date(list_of_lots: &Ref>>) -> Vec { @@ -285,8 +364,7 @@ pub(crate) fn create_lots_and_movements( for (idx, _lot) in reordered_vec.iter().enumerate() { vec_of_indexes.insert(0, idx) } - let vec = vec_of_indexes; - vec + vec_of_indexes } fn get_fifo_by_creation_date(list_of_lots: &Ref>>) -> Vec { @@ -294,8 +372,7 @@ pub(crate) fn create_lots_and_movements( for (idx, _lot) in list_of_lots.iter().enumerate() { vec_of_indexes.push(idx) } - let vec = vec_of_indexes; - vec + vec_of_indexes } fn get_fifo_by_lot_basis_date(list_of_lots: &Ref>>) -> Vec { @@ -312,13 +389,21 @@ pub(crate) fn create_lots_and_movements( for (idx, _lot) in reordered_vec.iter().enumerate() { vec_of_indexes.push(idx) } - let vec = vec_of_indexes; - vec + vec_of_indexes } + // 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, @@ -335,6 +420,7 @@ 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, @@ -347,21 +433,43 @@ 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; + 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(); - let mvmt = Movement { + mvmt = Movement { amount: ar.amount, date_as_string: txn.date_as_string.clone(), date: txn.date, @@ -376,12 +484,21 @@ 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 { - let mvmt: Movement; + + // 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. 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, @@ -391,8 +508,7 @@ 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(), @@ -407,91 +523,217 @@ 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); - // create list of incoming/positive amounts(mvmts) in margin lot, add them - let mut positive_mvmt_list: Vec> = [].to_vec(); - let mut total_positive_amounts = d128!(0); - let (base_acct_key, quote_acct_key) = get_base_and_quote_acct_for_dual_actionrecord_flow_tx( - txn_num, - &ar_map, - &raw_acct_map, - &acct_map, - &txns_map, - )?; + // 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. + let mut positive_mvmt_list: Vec> = [].to_vec(); + let mut total_positive_amounts = d128!(0); - let base_acct = acct_map.get(&base_acct_key).unwrap(); - let base_acct_lot = base_acct.list_of_lots.borrow().last().unwrap().clone(); - // 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 += mvmt.amount; - positive_mvmt_list.push(mvmt.clone()) + // 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, + &raw_acct_map, + &acct_map, + &txns_map, + )?; + + 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) { + // 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()) + } } - } - let mut amounts_used = d128!(0); - let mut percentages_used = d128!(0); - for pos_mvmt in positive_mvmt_list.iter().take(positive_mvmt_list.len()-1) { - let inner_lot = - Rc::new( + // 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( + Lot { + date_as_string: txn.date_as_string.clone(), + date_of_first_mvmt_in_lot: txn.date, + date_for_basis_purposes: pos_mvmt.date, + lot_number: acct.list_of_lots.borrow().len() as u32 + 1, + account_key: acct.raw_key, + movements: RefCell::new([].to_vec()), + } + ); + let percentage_used = round_d128_1e8(&(pos_mvmt.amount/&total_positive_amounts)); + let amount_used = round_d128_1e8(&(ar.amount*percentage_used)); + let inner_mvmt = Movement { + amount: amount_used, + 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: percentage_used, + ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)), + lot_num: inner_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(inner_mvmt, &ar, &inner_lot, &chosen_home_currency, &raw_acct_map, &acct_map); + acct.list_of_lots.borrow_mut().push(inner_lot); + amounts_used += amount_used; + 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( Lot { date_as_string: txn.date_as_string.clone(), date_of_first_mvmt_in_lot: txn.date, - date_for_basis_purposes: pos_mvmt.date, + date_for_basis_purposes: final_pos_mvmt.date, lot_number: acct.list_of_lots.borrow().len() as u32 + 1, account_key: acct.raw_key, movements: RefCell::new([].to_vec()), } ); - let percentage_used = round_d128_1e8(&(pos_mvmt.amount/&total_positive_amounts)); - let amount_used = round_d128_1e8(&(ar.amount*percentage_used)); - let inner_mvmt = Movement { - amount: amount_used, + mvmt = Movement { + amount: round_d128_1e8(&(ar.amount - amounts_used)), 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: percentage_used, + ratio_of_amt_to_incoming_mvmts_in_a_r: round_d128_1e8(&(d128!(1.0) - percentages_used)), 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)), }; - wrap_mvmt_and_push(inner_mvmt, &ar, &inner_lot, &chosen_home_currency, &raw_acct_map, &acct_map); - acct.list_of_lots.borrow_mut().push(inner_lot); - amounts_used += amount_used; - percentages_used += percentage_used; - } - let final_mvmt = positive_mvmt_list.last().unwrap(); - lot = - Rc::new( + // 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(); + let og_raw_acct = raw_acct_map.get(&og_acct.raw_key).unwrap(); + let ic_ar = ar; + let ic_raw_acct = raw_acct; + 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 { + process_multiple_incoming_lots_and_mvmts( + txn_num, + &og_ar, + &ic_ar, + &chosen_home_currency, + *ar_num, + &raw_acct_map, + &acct_map, + &txns_map, + &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: final_mvmt.date, - lot_number: acct.list_of_lots.borrow().len() as u32 + 1, + 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: round_d128_1e8(&(ar.amount - amounts_used)), + ); + 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: round_d128_1e8(&(d128!(1.0) - percentages_used)), + 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)), @@ -499,39 +741,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); - acct.list_of_lots.borrow_mut().push(lot); - continue - } - } - TxType::Exchange => { - 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(); - let og_raw_acct = raw_acct_map.get(&og_acct.raw_key).unwrap(); - let ic_ar = ar; - let ic_raw_acct = raw_acct; - 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 && multiple_incoming_mvmts_per_ar && (txn.date <= like_kind_cutoff_date) { - process_multiple_incoming_lots_and_mvmts( - txn_num, - &og_ar, - &ic_ar, - &chosen_home_currency, - *ar_num, - &raw_acct_map, - &acct_map, - &txns_map, - &ar_map, - ); - continue } + // 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 { - let lot = - Rc::new( + lot = Rc::new( Lot { date_as_string: txn.date_as_string.clone(), date_of_first_mvmt_in_lot: txn.date, @@ -540,9 +755,8 @@ pub(crate) fn create_lots_and_movements( account_key: acct.raw_key, movements: RefCell::new([].to_vec()), } - ) - ; - let whole_mvmt = Movement { + ); + whole_mvmt = Movement { amount: ar.amount, date_as_string: txn.date_as_string.clone(), date: txn.date, @@ -556,15 +770,22 @@ pub(crate) fn create_lots_and_movements( 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); - acct.list_of_lots.borrow_mut().push(lot); - continue } + // 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, @@ -580,29 +801,27 @@ pub(crate) fn create_lots_and_movements( } continue } - } - } - } - } // end for ar in txn.actionrecords + } // 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 of tx does not have marginness of TwoARs - } // end for txn in transactions + } // end for txn in transactions (txn_num in txn_map.len()) Ok(txns_map) } -/// 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! +/// 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). /// -/// 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. +/// 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`. fn get_base_and_quote_acct_for_dual_actionrecord_flow_tx( txn_num: u32, ar_map: &HashMap, @@ -614,35 +833,32 @@ 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 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( + let (base_key,quote_key) = txn_of_first_lot_mvmts_first_mvmt.get_base_and_quote_raw_acct_keys( ar_map, &raw_acct_map, - &acct_map)?; // TODO: should this panic on margin loss? As of 2019-10-02, no. Should test for margin shorting too, though. + &acct_map)?; 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( - pair_keys: (u16,u16), + base_and_quote_keys: (u16,u16), txn: &Transaction, ars: &HashMap, raw_acct_map: &HashMap, acct_map: &HashMap - ) -> (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(&pair_keys.0).unwrap(); // key.0 is base, and key.1 is quote + let compare = raw_acct_map.get(&base_and_quote_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]) @@ -651,6 +867,8 @@ 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, @@ -663,11 +881,19 @@ 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); @@ -677,6 +903,11 @@ 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, @@ -690,16 +921,16 @@ fn fit_into_lots( acct_map: &HashMap, ) { - let ar = ar_map.get(&spawning_ar_key).unwrap(); - let acct = acct_map.get(&ar.account_key).unwrap(); + let spawning_ar = ar_map.get(&spawning_ar_key).unwrap(); + + let acct = acct_map.get(&spawning_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; @@ -707,7 +938,8 @@ 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 sum_of_mvmts_in_lot == d128!(0.0) { // If the lot is "full", go to the next + // If the `lot` is "full", try the next + if sum_of_mvmts_in_lot == d128!(0.0) { 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]; @@ -740,11 +972,13 @@ 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, @@ -761,9 +995,11 @@ 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 // And we're done + return } - // Note: at this point, we know the movement doesn't fit in a single lot & sum_of_mvmts_in_lot > 0 + + // 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. let mvmt = RefCell::new(mvmt_to_fit); let mvmt_rc = Rc::from(mvmt); @@ -782,13 +1018,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, ar.amount, raw_acct.ticker); - println!("Account balance is only: {}", acct.get_sum_of_amts_in_lots()); + 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); std::process::exit(1); } @@ -796,6 +1032,10 @@ 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(), @@ -810,7 +1050,8 @@ fn fit_into_lots( proceeds_lk: Cell::new(d128!(0.0)), cost_basis_lk: Cell::new(d128!(0.0)), }; - assert!(current_index_position < vec_of_ordered_index_values.len()); + + // After applying some of the `action record`'s amount to another `lot`, take the remainder and recurse fit_into_lots( txn_num, spawning_ar_key, @@ -825,6 +1066,9 @@ 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, @@ -846,11 +1090,10 @@ 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 final_mvmt = list_of_mvmts_of_outgoing_ar.last().unwrap(); + 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(); // First iteration, for all but final movement - 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) { + for outgoing_mvmt in list_of_mvmts_of_outgoing_ar.iter().take(list_of_mvmts_of_outgoing_ar_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; @@ -862,7 +1105,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 lot = + let inner_lot = Rc::new( Lot { date_as_string: txn.date_as_string.clone(), @@ -883,7 +1126,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: lot.lot_number, + lot_num: inner_lot.lot_number, proceeds: Cell::new(d128!(0.0)), proceeds_lk: Cell::new(d128!(0.0)), cost_basis_lk: Cell::new(d128!(0.0)), @@ -892,15 +1135,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, &lot, &chosen_home_currency, &raw_acct_map, &acct_map); - this_acct.list_of_lots.borrow_mut().push(lot); + 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); } // 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_mvmt.get_lot(acct_map, ar_map).date_of_first_mvmt_in_lot; + let inherited_date = final_og_mvmt.get_lot(acct_map, ar_map).date_of_first_mvmt_in_lot; let lot = Rc::new( Lot {