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

Base calculator for balances

This commit is contained in:
Zach Gollwitzer 2025-03-07 11:30:49 -05:00
parent 9426259999
commit 3a2263ea99
4 changed files with 103 additions and 76 deletions

View file

@ -0,0 +1,31 @@
class Account::Balance::BaseCalculator
attr_reader :account
def initialize(account)
@account = account
end
private
CashBalance = Data.define(:date, :balance)
def sync_cache
@sync_cache ||= Account::Balance::SyncCache.new(account)
end
def build_balance(amount, date)
Account::Balance.new(
account: account,
date: date,
balance: amount,
cash_balance: amount,
currency: account.currency
)
end
def calculate_next_balance(prior_balance, transactions, direction: :forward)
flows = transactions.sum(&:amount)
negated = direction == :forward ? account.asset? : account.liability?
flows *= -1 if negated
prior_balance + flows
end
end

View file

@ -1,92 +1,42 @@
class Account::Balance::ForwardCalculator
attr_reader :account, :holdings
def initialize(account, holdings: nil)
@account = account
@holdings = holdings || []
end
class Account::Balance::ForwardCalculator < Account::Balance::BaseCalculator
def calculate
Rails.logger.tagged("Account::BalanceCalculator") do
Rails.logger.info("Calculating cash balances with strategy: forward 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
calculate_cash_balances
calculate_balances
end
end
private
def calculate_balances
prior_balance = 0
current_balance = nil
@balances = []
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 }
@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
valuation = entries_for_date.find { |e| e.account_valuation? }
def calculate_cash_balances
prior_cash_balance = 0
current_cash_balance = nil
current_balance = if valuation
@cash_balances = []
account.start_date.upto(Date.current).each do |date|
entries = sync_cache.get_entries(date)
holdings = sync_cache.get_holdings(date)
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_for_date.sum(&:amount)
valuation.amount - holdings.sum(&:amount)
else
transactions = entries_for_date.select { |e| e.account_transaction? || e.account_trade? }
calculate_balance(prior_balance, transactions, inverse: true)
calculate_next_balance(prior_cash_balance, entries, direction: :forward)
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
@cash_balances << CashBalance.new(date, current_cash_balance)
prior_cash_balance = current_cash_balance
end
end
def oldest_date
converted_entries.first ? converted_entries.first.date - 1.day : Date.current
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

View file

@ -0,0 +1,46 @@
class Account::Balance::SyncCache
def initialize(account)
@account = account
end
def get_valuation(date)
converted_entries.find { |e| e.date == date && e.account_valuation? }
end
def get_holdings(date)
converted_holdings.select { |h| h.date == date }
end
def get_entries(date)
converted_entries.select { |e| e.date == date && (e.account_transaction? || e.account_trade?) }
end
private
attr_reader :account
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 ||= account.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
end

View file

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