1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-10 07:55:21 +02:00

Finish balance calculators

This commit is contained in:
Zach Gollwitzer 2025-03-07 11:50:19 -05:00
parent 3a2263ea99
commit 791683aca0
4 changed files with 38 additions and 103 deletions

View file

@ -5,6 +5,12 @@ class Account::Balance::BaseCalculator
@account = account
end
def calculate
Rails.logger.tagged(self.class.name) do
calculate_balances
end
end
private
CashBalance = Data.define(:date, :balance)
@ -12,12 +18,12 @@ class Account::Balance::BaseCalculator
@sync_cache ||= Account::Balance::SyncCache.new(account)
end
def build_balance(amount, date)
def build_balance(date, cash_balance, holdings_value)
Account::Balance.new(
account: account,
date: date,
balance: amount,
cash_balance: amount,
balance: holdings_value + cash_balance,
cash_balance: cash_balance,
currency: account.currency
)
end

View file

@ -1,42 +1,29 @@
class Account::Balance::ForwardCalculator < Account::Balance::BaseCalculator
def calculate
Rails.logger.tagged("Account::BalanceCalculator") do
calculate_cash_balances
calculate_balances
end
end
private
def calculate_balances
@balances = []
@balances = @cash_balances.map do |balance|
holdings = sync_cache.get_holdings(balance.date)
holdings_value = holdings.sum(&:amount)
build_balance(balance.balance + holdings_value, balance.date)
end.compact
end
def calculate_cash_balances
prior_cash_balance = 0
current_cash_balance = nil
@cash_balances = []
@balances = []
account.start_date.upto(Date.current).each do |date|
entries = sync_cache.get_entries(date)
holdings = sync_cache.get_holdings(date)
holdings_value = holdings.sum(&:amount)
valuation = sync_cache.get_valuation(date)
current_cash_balance = if valuation
# To get this to a cash valuation, we back out holdings value on day
valuation.amount - holdings.sum(&:amount)
# Since a valuation means "total balance" (which includes holdings), we back out holdings value to get cash balance
valuation.amount - holdings_value
else
calculate_next_balance(prior_cash_balance, entries, direction: :forward)
end
@cash_balances << CashBalance.new(date, current_cash_balance)
@balances << build_balance(date, current_cash_balance, holdings_value)
prior_cash_balance = current_cash_balance
end
@balances
end
end

View file

@ -1,91 +1,33 @@
class Account::Balance::ReverseCalculator
attr_reader :account, :holdings
def initialize(account, holdings: nil)
@account = account
@holdings = holdings || []
end
def calculate
Rails.logger.tagged("Account::BalanceCalculator") do
Rails.logger.info("Calculating cash balances with strategy: reverse sync")
cash_balances = calculate_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.compact
end
end
class Account::Balance::ReverseCalculator < Account::Balance::BaseCalculator
private
def oldest_date
converted_entries.first ? converted_entries.first.date - 1.day : Date.current
end
def calculate_balances
prior_balance = account.cash_balance
todays_cash_balance = account.cash_balance
yesterdays_cash_balance = nil
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 }
@balances = []
valuation = entries_for_date.find { |e| e.account_valuation? }
Date.current.downto(account.start_date).map do |date|
entries = sync_cache.get_entries(date)
holdings = sync_cache.get_holdings(date)
holdings_value = holdings.sum(&:amount)
valuation = sync_cache.get_valuation(date)
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)
yesterdays_cash_balance = if valuation
# Since a valuation means "total balance" (which includes holdings), we back out holdings value to get cash balance
valuation.amount - holdings_value
else
transactions = entries_for_date.select { |e| e.account_transaction? || e.account_trade? }
calculate_balance(prior_balance, transactions)
calculate_next_balance(todays_cash_balance, entries, direction: :reverse)
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
)
if valuation.present?
@balances << build_balance(date, yesterdays_cash_balance, holdings_value)
else
@balances << build_balance(date, todays_cash_balance, holdings_value)
end
prior_balance = current_balance
balance_record
todays_cash_balance = yesterdays_cash_balance
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
@balances
end
end

View file

@ -17,7 +17,7 @@ class Account::Balance::ReverseCalculatorTest < ActiveSupport::TestCase
test "no entries sync" do
assert_equal 0, @account.balances.count
expected = [ @account.balance ]
expected = [ @account.balance, @account.balance ]
calculated = Account::Balance::ReverseCalculator.new(@account).calculate
assert_equal expected, calculated.map(&:balance)