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:
parent
9426259999
commit
3a2263ea99
4 changed files with 103 additions and 76 deletions
31
app/models/account/balance/base_calculator.rb
Normal file
31
app/models/account/balance/base_calculator.rb
Normal 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
|
|
@ -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
|
||||
|
|
46
app/models/account/balance/sync_cache.rb
Normal file
46
app/models/account/balance/sync_cache.rb
Normal 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
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue