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

Base holding calculator

This commit is contained in:
Zach Gollwitzer 2025-03-07 09:42:18 -05:00
parent b147ce0036
commit 9426259999
5 changed files with 88 additions and 155 deletions

View file

@ -0,0 +1,59 @@
class Account::Holding::BaseCalculator
attr_reader :account
def initialize(account)
@account = account
end
def calculate
Rails.logger.tagged(self.class.name) do
calculate_holdings
Account::Holding.gapfill(@holdings)
end
end
private
def portfolio_cache
@portfolio_cache ||= Account::Holding::PortfolioCache.new(account)
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)
Rails.logger.info "Generating holdings for #{portfolio.size} securities on #{date}"
portfolio.map do |security_id, qty|
price = portfolio_cache.get_price(security_id, date)
account.holdings.build(
security_id: security_id,
date: date,
qty: qty,
price: price,
currency: account.currency,
amount: qty * price
)
end.compact
end
end

View file

@ -1,88 +1,15 @@
class Account::Holding::ForwardCalculator
attr_reader :account
def initialize(account)
@account = account
end
def calculate
Rails.logger.tagged("Account::HoldingCalculator") do
Rails.logger.info("Calculating holdings with strategy: forward sync")
calculated_holdings = calculate_holdings
Account::Holding.gapfill(calculated_holdings)
end
end
class Account::Holding::ForwardCalculator < Account::Holding::BaseCalculator
private
def portfolio_cache
@portfolio_cache ||= Account::Holding::PortfolioCache.new(account)
end
def calculate_holdings
prior_holding_quantities = load_empty_holding_quantities
current_holding_quantities = {}
todays_portfolio = generate_starting_portfolio
tomorrows_portfolio = {}
@holdings = []
holdings = []
account.start_date.upto(Date.current).map do |date|
today_trades = portfolio_cache.get_trades(date: date)
current_holding_quantities = calculate_portfolio(prior_holding_quantities, today_trades, inverse: true)
holdings += generate_holding_records(current_holding_quantities, date)
prior_holding_quantities = current_holding_quantities
account.start_date.upto(Date.current).each do |date|
trades = portfolio_cache.get_trades(date: date)
tomorrows_portfolio = transform_portfolio(todays_portfolio, trades, direction: :forward)
@holdings += build_holdings(tomorrows_portfolio, date)
todays_portfolio = tomorrows_portfolio
end
holdings
end
def generate_holding_records(portfolio, date)
Rails.logger.info "Generating holdings for #{portfolio.size} securities on #{date}"
portfolio.map do |security_id, qty|
price = portfolio_cache.get_price(security_id, date)
account.holdings.build(
security_id: security_id,
date: date,
qty: qty,
price: price,
currency: account.currency,
amount: qty * price
)
end.compact
end
def calculate_portfolio(holding_quantities, today_trades, inverse: false)
new_quantities = holding_quantities.dup
today_trades.each do |trade|
security_id = trade.entryable.security_id
qty_change = inverse ? trade.entryable.qty : -trade.entryable.qty
new_quantities[security_id] = (new_quantities[security_id] || 0) + qty_change
end
new_quantities
end
def load_empty_holding_quantities
holding_quantities = {}
trades = portfolio_cache.get_trades
trades.map { |t| t.entryable.security_id }.uniq.each do |security_id|
holding_quantities[security_id] = 0
end
holding_quantities
end
def load_current_holding_quantities
holding_quantities = load_empty_holding_quantities
account.holdings.where(date: Date.current, currency: account.currency).map do |holding|
holding_quantities[holding.security_id] = holding.qty
end
holding_quantities
end
end

View file

@ -40,6 +40,10 @@ class Account::Holding::PortfolioCache
converted_price
end
def get_securities
@security_cache.map { |_, v| v[:security] }
end
private
def trades
@trades ||= account.entries.includes(entryable: :security).account_trades.chronological.to_a
@ -59,7 +63,9 @@ class Account::Holding::PortfolioCache
def load_prices
@security_cache = {}
# Get securities from trades and current holdings
# Get securities from trades and current holdings. We need them from holdings
# because for linked accounts, our provider gives us holding data that may not
# exist solely in the trades history.
securities = trades.map(&:entryable).map(&:security).uniq
securities += account.holdings.where(date: Date.current).map(&:security)
securities.uniq!

View file

@ -1,85 +1,26 @@
class Account::Holding::ReverseCalculator
attr_reader :account
def initialize(account)
@account = account
end
def calculate
Rails.logger.tagged("Account::HoldingCalculator") do
Rails.logger.info("Calculating holdings with strategy: reverse sync")
calculated_holdings = calculate_holdings
Account::Holding.gapfill(calculated_holdings)
end
end
class Account::Holding::ReverseCalculator < Account::Holding::BaseCalculator
private
def portfolio_cache
@portfolio_cache ||= Account::Holding::PortfolioCache.new(account)
end
def calculate_holdings
current_holding_quantities = load_current_holding_quantities
prior_holding_quantities = {}
todays_portfolio = generate_starting_portfolio
yesterdays_portfolio = {}
holdings = []
@holdings = []
Date.current.downto(account.start_date).map do |date|
today_trades = portfolio_cache.get_trades(date: date)
prior_holding_quantities = calculate_portfolio(current_holding_quantities, today_trades)
holdings += generate_holding_records(current_holding_quantities, date)
current_holding_quantities = prior_holding_quantities
yesterdays_portfolio = transform_portfolio(todays_portfolio, today_trades, direction: :reverse)
@holdings += build_holdings(todays_portfolio, date)
todays_portfolio = yesterdays_portfolio
end
holdings
end
def generate_holding_records(portfolio, date)
Rails.logger.info "Generating holdings for #{portfolio.size} securities on #{date}"
# Since this is a reverse sync, we start with today's holdings
def generate_starting_portfolio
holding_quantities = empty_portfolio
portfolio.map do |security_id, qty|
price = portfolio_cache.get_price(security_id, date)
todays_holdings = account.holdings.where(date: Date.current)
account.holdings.build(
security_id: security_id,
date: date,
qty: qty,
price: price,
currency: account.currency,
amount: qty * price
)
end.compact
end
def calculate_portfolio(holding_quantities, today_trades, inverse: false)
new_quantities = holding_quantities.dup
today_trades.each do |trade|
security_id = trade.entryable.security_id
qty_change = inverse ? trade.entryable.qty : -trade.entryable.qty
new_quantities[security_id] = (new_quantities[security_id] || 0) + qty_change
end
new_quantities
end
def load_empty_holding_quantities
holding_quantities = {}
trades = portfolio_cache.get_trades
trades.map { |t| t.entryable.security_id }.uniq.each do |security_id|
holding_quantities[security_id] = 0
end
holding_quantities
end
def load_current_holding_quantities
holding_quantities = load_empty_holding_quantities
account.holdings.where(date: Date.current, currency: account.currency).map do |holding|
todays_holdings.each do |holding|
holding_quantities[holding.security_id] = holding.qty
end

View file

@ -32,7 +32,7 @@ class Account::Balance::SyncerTest < ActiveSupport::TestCase
test "purges stale balances and holdings" do
# Balance before start date is stale
@account.expects(:start_date).returns(2.days.ago.to_date)
@account.expects(:start_date).returns(2.days.ago.to_date).twice
stale_balance = Account::Balance.new(date: 3.days.ago.to_date, balance: 10000, cash_balance: 10000, currency: "USD")
Account::Balance::ForwardCalculator.any_instance.expects(:calculate).returns(