1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-09 15:35:22 +02:00

Fix Plaid cash balance double counting

This commit is contained in:
Zach Gollwitzer 2025-05-07 21:35:05 -04:00
parent 8b857e9c8a
commit 89cfac4d95
3 changed files with 44 additions and 1 deletions

View file

@ -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

View file

@ -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)

View file

@ -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