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