2025-04-14 11:40:34 -04:00
|
|
|
class Holding::BaseCalculator
|
2025-03-07 17:35:55 -05:00
|
|
|
attr_reader :account
|
|
|
|
|
|
|
|
def initialize(account)
|
|
|
|
@account = account
|
|
|
|
end
|
|
|
|
|
|
|
|
def calculate
|
|
|
|
Rails.logger.tagged(self.class.name) do
|
|
|
|
holdings = calculate_holdings
|
2025-04-14 11:40:34 -04:00
|
|
|
Holding.gapfill(holdings)
|
2025-03-07 17:35:55 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
def portfolio_cache
|
2025-04-14 11:40:34 -04:00
|
|
|
@portfolio_cache ||= Holding::PortfolioCache.new(account)
|
2025-03-07 17:35:55 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def empty_portfolio
|
|
|
|
securities = portfolio_cache.get_securities
|
|
|
|
securities.each_with_object({}) { |security, hash| hash[security.id] = 0 }
|
|
|
|
end
|
|
|
|
|
|
|
|
def generate_starting_portfolio
|
|
|
|
empty_portfolio
|
|
|
|
end
|
|
|
|
|
|
|
|
def transform_portfolio(previous_portfolio, trade_entries, direction: :forward)
|
|
|
|
new_quantities = previous_portfolio.dup
|
|
|
|
|
|
|
|
trade_entries.each do |trade_entry|
|
|
|
|
trade = trade_entry.entryable
|
|
|
|
security_id = trade.security_id
|
|
|
|
qty_change = trade.qty
|
|
|
|
qty_change = qty_change * -1 if direction == :reverse
|
|
|
|
new_quantities[security_id] = (new_quantities[security_id] || 0) + qty_change
|
|
|
|
end
|
|
|
|
|
|
|
|
new_quantities
|
|
|
|
end
|
|
|
|
|
|
|
|
def build_holdings(portfolio, date)
|
|
|
|
portfolio.map do |security_id, qty|
|
|
|
|
price = portfolio_cache.get_price(security_id, date)
|
|
|
|
|
|
|
|
if price.nil?
|
|
|
|
Rails.logger.warn "No price found for security #{security_id} on #{date}"
|
|
|
|
next
|
|
|
|
end
|
|
|
|
|
2025-04-14 11:40:34 -04:00
|
|
|
Holding.new(
|
2025-03-07 17:35:55 -05:00
|
|
|
account_id: account.id,
|
|
|
|
security_id: security_id,
|
|
|
|
date: date,
|
|
|
|
qty: qty,
|
|
|
|
price: price.price,
|
|
|
|
currency: price.currency,
|
|
|
|
amount: qty * price.price
|
|
|
|
)
|
|
|
|
end.compact
|
|
|
|
end
|
|
|
|
end
|