diff --git a/app/models/balance/reverse_calculator.rb b/app/models/balance/reverse_calculator.rb index 36106d2f..35a77445 100644 --- a/app/models/balance/reverse_calculator.rb +++ b/app/models/balance/reverse_calculator.rb @@ -11,6 +11,8 @@ class Balance::ReverseCalculator < Balance::BaseCalculator # Calculates in reverse-chronological order (End of day -> Start of day) account.current_anchor_date.downto(account.opening_anchor_date).map do |date| + flows = flows_for_date(date) + if use_opening_anchor_for_date?(date) end_cash_balance = derive_cash_balance_on_date_from_total( total_balance: account.opening_anchor_balance, @@ -20,33 +22,30 @@ class Balance::ReverseCalculator < Balance::BaseCalculator start_cash_balance = end_cash_balance start_non_cash_balance = end_non_cash_balance - - build_balance( - date: date, - cash_balance: end_cash_balance, - non_cash_balance: end_non_cash_balance, - start_cash_balance: start_cash_balance, - start_non_cash_balance: start_non_cash_balance - ) + market_value_change = 0 else start_cash_balance = derive_start_cash_balance(end_cash_balance: end_cash_balance, date: date) start_non_cash_balance = derive_start_non_cash_balance(end_non_cash_balance: end_non_cash_balance, date: date) - - # Even though we've just calculated "start" balances, we set today equal to end of day, then use those - # in our next iteration (slightly confusing, but just the nature of a "reverse" sync) - output_balance = build_balance( - date: date, - cash_balance: end_cash_balance, - non_cash_balance: end_non_cash_balance, - start_cash_balance: start_cash_balance, - start_non_cash_balance: start_non_cash_balance - ) - - end_cash_balance = start_cash_balance - end_non_cash_balance = start_non_cash_balance - - output_balance + market_value_change = market_value_change_on_date(date, flows) end + + output_balance = build_balance( + date: date, + balance: end_cash_balance + end_non_cash_balance, + cash_balance: end_cash_balance, + start_cash_balance: start_cash_balance, + start_non_cash_balance: start_non_cash_balance, + cash_inflows: flows[:cash_inflows], + cash_outflows: flows[:cash_outflows], + non_cash_inflows: flows[:non_cash_inflows], + non_cash_outflows: flows[:non_cash_outflows], + net_market_flows: market_value_change + ) + + end_cash_balance = start_cash_balance + end_non_cash_balance = start_non_cash_balance + + output_balance end end end @@ -62,13 +61,6 @@ class Balance::ReverseCalculator < Balance::BaseCalculator account.asset? ? entry_flows : -entry_flows end - # Reverse syncs are a bit different than forward syncs because we do not allow "reconciliation" valuations - # to be used at all. This is primarily to keep the code and the UI easy to understand. For a more detailed - # explanation, see the test suite. - def use_opening_anchor_for_date?(date) - account.has_opening_anchor? && date == account.opening_anchor_date - end - # Alias method, for algorithmic clarity # Derives cash balance, starting from the end-of-day, applying entries in reverse to get the start-of-day balance def derive_start_cash_balance(end_cash_balance:, date:) @@ -80,4 +72,11 @@ class Balance::ReverseCalculator < Balance::BaseCalculator def derive_start_non_cash_balance(end_non_cash_balance:, date:) derive_non_cash_balance(end_non_cash_balance, date, direction: :reverse) end + + # Reverse syncs are a bit different than forward syncs because we do not allow "reconciliation" valuations + # to be used at all. This is primarily to keep the code and the UI easy to understand. For a more detailed + # explanation, see the test suite. + def use_opening_anchor_for_date?(date) + account.has_opening_anchor? && date == account.opening_anchor_date + end end diff --git a/test/models/balance/reverse_calculator_test.rb b/test/models/balance/reverse_calculator_test.rb index df792ed9..c3ba12ba 100644 --- a/test/models/balance/reverse_calculator_test.rb +++ b/test/models/balance/reverse_calculator_test.rb @@ -20,9 +20,9 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase { date: Date.current, legacy_balances: { balance: 20000, cash_balance: 20000 }, - balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 20000, end_non_cash: 0, end: 20000 }, + balances: { start: 20000, start_cash: 20000, start_non_cash: 0, end_cash: 20000, end_non_cash: 0, end: 20000 }, flows: 0, - adjustments: { cash_adjustments: 20000, non_cash_adjustments: 0 } + adjustments: { cash_adjustments: 0, non_cash_adjustments: 0 } } ] ) @@ -78,9 +78,9 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase { date: 3.days.ago, legacy_balances: { balance: 20000, cash_balance: 20000 }, - balances: { start: 15000, start_cash: 15000, start_non_cash: 0, end_cash: 20000, end_non_cash: 0, end: 20000 }, + balances: { start: 20000, start_cash: 20000, start_non_cash: 0, end_cash: 20000, end_non_cash: 0, end: 20000 }, flows: 0, - adjustments: { cash_adjustments: 5000, non_cash_adjustments: 0 } + adjustments: { cash_adjustments: 0, non_cash_adjustments: 0 } }, { date: 4.days.ago, @@ -135,8 +135,8 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase account: { type: Depository, balance: 20000, cash_balance: 20000, currency: "USD" }, entries: [ { type: "current_anchor", date: Date.current, balance: 20000 }, - { type: "transaction", date: 4.days.ago, amount: -500 }, # income - { type: "transaction", date: 2.days.ago, amount: 100 } # expense + { type: "transaction", date: 2.days.ago, amount: 100 }, # expense + { type: "transaction", date: 4.days.ago, amount: -500 } # income ] ) @@ -185,7 +185,7 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase legacy_balances: { balance: 19600, cash_balance: 19600 }, balances: { start: 19600, start_cash: 19600, start_non_cash: 0, end_cash: 19600, end_non_cash: 0, end: 19600 }, flows: 0, - adjustments: { cash_adjustments: 19600, non_cash_adjustments: 0 } + adjustments: { cash_adjustments: 0, non_cash_adjustments: 0 } } # After income (-500) ] ) @@ -239,7 +239,7 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase date: 4.days.ago, legacy_balances: { balance: 1900, cash_balance: 1900 }, balances: { start: 2400, start_cash: 2400, start_non_cash: 0, end_cash: 1900, end_non_cash: 0, end: 1900 }, - flows: { cash_inflows: 0, cash_outflows: 500 }, + flows: { cash_inflows: 500, cash_outflows: 0 }, adjustments: 0 }, # After CC payment (-500) { @@ -247,7 +247,7 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase legacy_balances: { balance: 2400, cash_balance: 2400 }, balances: { start: 2400, start_cash: 2400, start_non_cash: 0, end_cash: 2400, end_non_cash: 0, end: 2400 }, flows: 0, - adjustments: { cash_adjustments: 2400, non_cash_adjustments: 0 } + adjustments: { cash_adjustments: 0, non_cash_adjustments: 0 } } ] ) @@ -290,7 +290,7 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase legacy_balances: { balance: 200000, cash_balance: 0 }, balances: { start: 200000, start_cash: 0, start_non_cash: 200000, end_cash: 0, end_non_cash: 200000, end: 200000 }, flows: 0, - adjustments: { cash_adjustments: 0, non_cash_adjustments: 200000 } + adjustments: { cash_adjustments: 0, non_cash_adjustments: 0 } } ] ) @@ -332,7 +332,7 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase legacy_balances: { balance: 1000, cash_balance: 0 }, balances: { start: 1000, start_cash: 0, start_non_cash: 1000, end_cash: 0, end_non_cash: 1000, end: 1000 }, flows: 0, - adjustments: { cash_adjustments: 0, non_cash_adjustments: 1000 } + adjustments: { cash_adjustments: 0, non_cash_adjustments: 0 } } ] ) @@ -382,7 +382,7 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase legacy_balances: { balance: 20000, cash_balance: 20000 }, balances: { start: 20000, start_cash: 20000, start_non_cash: 0, end_cash: 20000, end_non_cash: 0, end: 20000 }, flows: { market_flows: 0 }, - adjustments: { cash_adjustments: 20000, non_cash_adjustments: 0 } + adjustments: { cash_adjustments: 0, non_cash_adjustments: 0 } } # At first, account is 100% cash, no holdings (no trades) ] ) @@ -426,15 +426,15 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase date: 1.day.ago.to_date, legacy_balances: { balance: 20000, cash_balance: 19000 }, balances: { start: 20000, start_cash: 19500, start_non_cash: 500, end_cash: 19000, end_non_cash: 1000, end: 20000 }, - flows: { cash_inflows: 0, cash_outflows: 500, non_cash_inflows: 500, non_cash_outflows: 0, net_market_flows: 0 }, + flows: { cash_inflows: 0, cash_outflows: 500, non_cash_inflows: 500, non_cash_outflows: 0, market_flows: 0 }, adjustments: 0 }, # After AAPL trade: $19k cash + $1k holdings { date: 2.days.ago.to_date, legacy_balances: { balance: 20000, cash_balance: 19500 }, - balances: { start: 20000, start_cash: 19500, start_non_cash: 500, end_cash: 19500, end_non_cash: 500, end: 20000 }, - flows: { market_flows: 0 }, - adjustments: { cash_adjustments: 19500, non_cash_adjustments: 500 } + balances: { start: 19500, start_cash: 19500, start_non_cash: 0, end_cash: 19500, end_non_cash: 500, end: 20000 }, + flows: { market_flows: -500 }, + adjustments: 0 } # Before AAPL trade: $19.5k cash + $500 MSFT ] ) @@ -450,8 +450,9 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase ], holdings: [ # Create holdings that differ in value from provider ($2,000 vs. the $1,000 reported by provider) - { date: Date.current, ticker: "AAPL", qty: 10, price: 100, amount: 2000 }, - { date: 1.day.ago, ticker: "AAPL", qty: 10, price: 100, amount: 2000 } + { date: Date.current, ticker: "AAPL", qty: 10, price: 100, amount: 1000 }, + { date: 1.day.ago, ticker: "AAPL", qty: 10, price: 100, amount: 1000 }, + { date: 2.days.ago, ticker: "AAPL", qty: 10, price: 100, amount: 1000 } ] ) @@ -464,22 +465,22 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase # This ensures the user sees the same top-line number reported by the provider (even if it creates a discrepancy in the cash balance) { date: Date.current, - legacy_balances: { balance: 20000, cash_balance: 18000 }, - balances: { start: 20000, start_cash: 18000, start_non_cash: 2000, end_cash: 18000, end_non_cash: 2000, end: 20000 }, + legacy_balances: { balance: 20000, cash_balance: 19000 }, + balances: { start: 20000, start_cash: 19000, start_non_cash: 1000, end_cash: 19000, end_non_cash: 1000, end: 20000 }, flows: { market_flows: 0 }, adjustments: 0 }, { date: 1.day.ago, - legacy_balances: { balance: 20000, cash_balance: 18000 }, - balances: { start: 20000, start_cash: 18000, start_non_cash: 2000, end_cash: 18000, end_non_cash: 2000, end: 20000 }, + legacy_balances: { balance: 20000, cash_balance: 19000 }, + balances: { start: 20000, start_cash: 19000, start_non_cash: 1000, end_cash: 19000, end_non_cash: 1000, end: 20000 }, flows: { market_flows: 0 }, - adjustments: { cash_adjustments: 3000, non_cash_adjustments: 2000 } + adjustments: 0 }, { date: 2.days.ago, - legacy_balances: { balance: 15000, cash_balance: 15000 }, - balances: { start: 15000, start_cash: 15000, start_non_cash: 0, end_cash: 15000, end_non_cash: 0, end: 15000 }, + legacy_balances: { balance: 15000, cash_balance: 14000 }, + balances: { start: 15000, start_cash: 14000, start_non_cash: 1000, end_cash: 14000, end_non_cash: 1000, end: 15000 }, flows: { market_flows: 0 }, adjustments: 0 } # Opening anchor sets absolute balance diff --git a/test/support/ledger_testing_helper.rb b/test/support/ledger_testing_helper.rb index dc18a039..d1345c76 100644 --- a/test/support/ledger_testing_helper.rb +++ b/test/support/ledger_testing_helper.rb @@ -12,8 +12,8 @@ module LedgerTestingHelper created_account = families(:empty).accounts.create!( name: "Test Account", accountable: account_type.new, - balance: 0, # Doesn't matter, ledger derives this - cash_balance: 0, # Doesn't matter, ledger derives this + balance: account[:balance] || 0, # Doesn't matter, ledger derives this + cash_balance: account[:cash_balance] || 0, # Doesn't matter, ledger derives this **account_attrs )