mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-02 20:15:22 +02:00
Plaid portfolio sync algorithm and calculation improvements (#1526)
* Start tests rework * Cash balance on schema * Add reverse syncer * Reverse balance sync with holdings * Reverse holdings sync * Reverse holdings sync should work with only trade entries * Consolidate brokerage cash * Add forward sync option * Update new balance info after syncs * Intraday balance calculator and sync fixes * Show only balance for trade entries * Tests passing * Update Gemfile.lock * Cleanup, performance improvements * Remove account reloads for reliable sync outputs * Simplify valuation view logic * Special handling for Plaid cash holding
This commit is contained in:
parent
a59ca5b7c6
commit
49c353e10c
72 changed files with 1152 additions and 1046 deletions
121
app/models/account/balance_calculator.rb
Normal file
121
app/models/account/balance_calculator.rb
Normal file
|
@ -0,0 +1,121 @@
|
|||
class Account::BalanceCalculator
|
||||
def initialize(account, holdings: nil)
|
||||
@account = account
|
||||
@holdings = holdings || []
|
||||
end
|
||||
|
||||
def calculate(reverse: false, start_date: nil)
|
||||
cash_balances = reverse ? reverse_cash_balances : forward_cash_balances
|
||||
|
||||
cash_balances.map do |balance|
|
||||
holdings_value = converted_holdings.select { |h| h.date == balance.date }.sum(&:amount)
|
||||
balance.balance = balance.balance + holdings_value
|
||||
balance
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
attr_reader :account, :holdings
|
||||
|
||||
def oldest_date
|
||||
converted_entries.first ? converted_entries.first.date - 1.day : Date.current
|
||||
end
|
||||
|
||||
def reverse_cash_balances
|
||||
prior_balance = account.cash_balance
|
||||
|
||||
Date.current.downto(oldest_date).map do |date|
|
||||
entries_for_date = converted_entries.select { |e| e.date == date }
|
||||
holdings_for_date = converted_holdings.select { |h| h.date == date }
|
||||
|
||||
valuation = entries_for_date.find { |e| e.account_valuation? }
|
||||
|
||||
current_balance = if valuation
|
||||
# To get this to a cash valuation, we back out holdings value on day
|
||||
valuation.amount - holdings_for_date.sum(&:amount)
|
||||
else
|
||||
transactions = entries_for_date.select { |e| e.account_transaction? || e.account_trade? }
|
||||
|
||||
calculate_balance(prior_balance, transactions)
|
||||
end
|
||||
|
||||
balance_record = Account::Balance.new(
|
||||
account: account,
|
||||
date: date,
|
||||
balance: valuation ? current_balance : prior_balance,
|
||||
cash_balance: valuation ? current_balance : prior_balance,
|
||||
currency: account.currency
|
||||
)
|
||||
|
||||
prior_balance = current_balance
|
||||
|
||||
balance_record
|
||||
end
|
||||
end
|
||||
|
||||
def forward_cash_balances
|
||||
prior_balance = 0
|
||||
current_balance = nil
|
||||
|
||||
oldest_date.upto(Date.current).map do |date|
|
||||
entries_for_date = converted_entries.select { |e| e.date == date }
|
||||
holdings_for_date = converted_holdings.select { |h| h.date == date }
|
||||
|
||||
valuation = entries_for_date.find { |e| e.account_valuation? }
|
||||
|
||||
current_balance = if valuation
|
||||
# To get this to a cash valuation, we back out holdings value on day
|
||||
valuation.amount - holdings_for_date.sum(&:amount)
|
||||
else
|
||||
transactions = entries_for_date.select { |e| e.account_transaction? || e.account_trade? }
|
||||
|
||||
calculate_balance(prior_balance, transactions, inverse: true)
|
||||
end
|
||||
|
||||
balance_record = Account::Balance.new(
|
||||
account: account,
|
||||
date: date,
|
||||
balance: current_balance,
|
||||
cash_balance: current_balance,
|
||||
currency: account.currency
|
||||
)
|
||||
|
||||
prior_balance = current_balance
|
||||
|
||||
balance_record
|
||||
end
|
||||
end
|
||||
|
||||
def converted_entries
|
||||
@converted_entries ||= @account.entries.order(:date).to_a.map do |e|
|
||||
converted_entry = e.dup
|
||||
converted_entry.amount = converted_entry.amount_money.exchange_to(
|
||||
account.currency,
|
||||
date: e.date,
|
||||
fallback_rate: 1
|
||||
).amount
|
||||
converted_entry.currency = account.currency
|
||||
converted_entry
|
||||
end
|
||||
end
|
||||
|
||||
def converted_holdings
|
||||
@converted_holdings ||= holdings.map do |h|
|
||||
converted_holding = h.dup
|
||||
converted_holding.amount = converted_holding.amount_money.exchange_to(
|
||||
account.currency,
|
||||
date: h.date,
|
||||
fallback_rate: 1
|
||||
).amount
|
||||
converted_holding.currency = account.currency
|
||||
converted_holding
|
||||
end
|
||||
end
|
||||
|
||||
def calculate_balance(prior_balance, transactions, inverse: false)
|
||||
flows = transactions.sum(&:amount)
|
||||
negated = inverse ? account.asset? : account.liability?
|
||||
flows *= -1 if negated
|
||||
prior_balance + flows
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue