diff --git a/app/models/balance/reverse_calculator.rb b/app/models/balance/reverse_calculator.rb index 4c124ced..78a372cf 100644 --- a/app/models/balance/reverse_calculator.rb +++ b/app/models/balance/reverse_calculator.rb @@ -21,7 +21,13 @@ class Balance::ReverseCalculator < Balance::BaseCalculator if valuation.present? @balances << build_balance(date, previous_cash_balance, holdings_value) else - @balances << build_balance(date, current_cash_balance, holdings_value) + # If date is today, we don't distinguish cash vs. total since provider's are inconsistent with treatment + # of the cash component. Instead, just set the balance equal to the "total value" reported by the provider + if date == Date.current + @balances << build_balance(date, account.balance, 0) + else + @balances << build_balance(date, current_cash_balance, holdings_value) + end end current_cash_balance = previous_cash_balance diff --git a/app/models/plaid_investment_sync.rb b/app/models/plaid_investment_sync.rb index 489b0ca1..8a95d1e2 100644 --- a/app/models/plaid_investment_sync.rb +++ b/app/models/plaid_investment_sync.rb @@ -11,6 +11,7 @@ class PlaidInvestmentSync @securities = securities PlaidAccount.transaction do + normalize_cash_balance! sync_transactions! sync_holdings! end @@ -19,6 +20,23 @@ class PlaidInvestmentSync private attr_reader :transactions, :holdings, :securities + # Plaid considers "brokerage cash" and "cash equivalent holdings" to all be part of "cash balance" + # Internally, we DO NOT. + # Maybe clearly distinguishes between "brokerage cash" vs. "holdings (i.e. invested cash)" + # For this reason, we must back out cash + cash equivalent holdings from the reported cash balance to avoid double counting + def normalize_cash_balance! + non_cash_holdings = holdings.reject do |h| + _internal_security, plaid_security = get_security(h.security_id, securities) + plaid_security&.type == "cash" + end + + non_cash_holdings_value = non_cash_holdings.sum { |h| h.quantity * h.institution_price } + + plaid_account.account.update!( + cash_balance: plaid_account.account.cash_balance - non_cash_holdings_value + ) + end + def sync_transactions! transactions.each do |transaction| security, plaid_security = get_security(transaction.security_id, securities) diff --git a/test/models/balance/reverse_calculator_test.rb b/test/models/balance/reverse_calculator_test.rb index 29711702..38ede057 100644 --- a/test/models/balance/reverse_calculator_test.rb +++ b/test/models/balance/reverse_calculator_test.rb @@ -120,4 +120,23 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase assert_equal expected, calculated end + + test "uses provider reported holdings and cash value on current day" do + aapl = securities(:aapl) + + # Implied holdings value of $1,000 from provider + @account.update!(cash_balance: 19000, balance: 20000) + + # Create a holding that differs in value from provider ($2,000 vs. the $1,000 reported by provider) + Holding.create!(date: Date.current, account: @account, security: aapl, qty: 10, price: 100, amount: 2000, currency: "USD") + Holding.create!(date: 1.day.ago.to_date, account: @account, security: aapl, qty: 10, price: 100, amount: 2000, currency: "USD") + + # Today reports the provider value. Yesterday, provider won't give us any data, so we MUST look at the generated holdings value + # to calculate the end balance ($19,000 cash + $2,000 holdings = $21,000 total value) + expected = [ 21000, 20000 ] + + calculated = Balance::ReverseCalculator.new(@account).calculate.sort_by(&:date).map(&:balance) + + assert_equal expected, calculated + end end