mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-22 22:59:39 +02:00
* 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
94 lines
3.3 KiB
Ruby
94 lines
3.3 KiB
Ruby
# The current system calculates a single, end-of-day balance every day for each account for simplicity.
|
|
# In most cases, this is sufficient. However, for the "Activity View", we need to show intraday balances
|
|
# to show users how each entry affects their balances. This class calculates intraday balances by
|
|
# interpolating between end-of-day balances.
|
|
class Account::BalanceTrendCalculator
|
|
BalanceTrend = Struct.new(:trend, :cash, keyword_init: true)
|
|
|
|
class << self
|
|
def for(entries)
|
|
return nil if entries.blank?
|
|
|
|
account = entries.first.account
|
|
|
|
date_range = entries.minmax_by(&:date)
|
|
min_entry_date, max_entry_date = date_range.map(&:date)
|
|
|
|
# In case view is filtered and there are entry gaps, refetch all entries in range
|
|
all_entries = account.entries.where(date: min_entry_date..max_entry_date).chronological.to_a
|
|
balances = account.balances.where(date: (min_entry_date - 1.day)..max_entry_date).chronological.to_a
|
|
holdings = account.holdings.where(date: (min_entry_date - 1.day)..max_entry_date).to_a
|
|
|
|
new(all_entries, balances, holdings)
|
|
end
|
|
end
|
|
|
|
def initialize(entries, balances, holdings)
|
|
@entries = entries
|
|
@balances = balances
|
|
@holdings = holdings
|
|
end
|
|
|
|
def trend_for(entry)
|
|
intraday_balance = nil
|
|
intraday_cash_balance = nil
|
|
|
|
start_of_day_balance = balances.find { |b| b.date == entry.date - 1.day && b.currency == entry.currency }
|
|
end_of_day_balance = balances.find { |b| b.date == entry.date && b.currency == entry.currency }
|
|
|
|
return BalanceTrend.new(trend: nil) if start_of_day_balance.blank? || end_of_day_balance.blank?
|
|
|
|
todays_holdings_value = holdings.select { |h| h.date == entry.date }.sum(&:amount)
|
|
|
|
prior_balance = start_of_day_balance.balance
|
|
prior_cash_balance = start_of_day_balance.cash_balance
|
|
current_balance = nil
|
|
current_cash_balance = nil
|
|
|
|
todays_entries = entries.select { |e| e.date == entry.date }
|
|
|
|
todays_entries.each_with_index do |e, idx|
|
|
if e.account_valuation?
|
|
current_balance = e.amount
|
|
current_cash_balance = e.amount
|
|
else
|
|
multiplier = e.account.liability? ? 1 : -1
|
|
balance_change = e.account_trade? ? 0 : multiplier * e.amount
|
|
cash_change = multiplier * e.amount
|
|
|
|
current_balance = prior_balance + balance_change
|
|
current_cash_balance = prior_cash_balance + cash_change
|
|
end
|
|
|
|
if e.id == entry.id
|
|
# Final entry should always match the end-of-day balances
|
|
if idx == todays_entries.size - 1
|
|
intraday_balance = end_of_day_balance.balance
|
|
intraday_cash_balance = end_of_day_balance.cash_balance
|
|
else
|
|
intraday_balance = current_balance
|
|
intraday_cash_balance = current_cash_balance
|
|
end
|
|
|
|
break
|
|
else
|
|
prior_balance = current_balance
|
|
prior_cash_balance = current_cash_balance
|
|
end
|
|
end
|
|
|
|
return BalanceTrend.new(trend: nil) unless intraday_balance.present?
|
|
|
|
BalanceTrend.new(
|
|
trend: TimeSeries::Trend.new(
|
|
current: Money.new(intraday_balance, entry.currency),
|
|
previous: Money.new(prior_balance, entry.currency),
|
|
favorable_direction: entry.account.favorable_direction
|
|
),
|
|
cash: Money.new(intraday_cash_balance, entry.currency),
|
|
)
|
|
end
|
|
|
|
private
|
|
attr_reader :entries, :balances, :holdings
|
|
end
|