1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-06 05:55:21 +02:00
Maybe/app/models/account/holding/syncer.rb
Zach Gollwitzer 453a54e5e6
Add security prices provider (Synth integration) (#1039)
* User tickers as primary lookup symbol instead of isin

* Add security price provider

* Fetch security prices in bulk to improve sync performance

* Fetch prices in bulk, better mocking for tests
2024-08-01 19:43:23 -04:00

128 lines
3.9 KiB
Ruby

class Account::Holding::Syncer
attr_reader :warnings
def initialize(account, start_date: nil)
@account = account
@warnings = []
@sync_date_range = calculate_sync_start_date(start_date)..Date.current
@portfolio = {}
load_prior_portfolio if start_date
end
def run
holdings = []
sync_date_range.each do |date|
holdings += build_holdings_for_date(date)
end
upsert_holdings holdings
end
private
attr_reader :account, :sync_date_range
def sync_entries
@sync_entries ||= account.entries
.account_trades
.includes(entryable: :security)
.where("date >= ?", sync_date_range.begin)
.order(:date)
end
def get_cached_price(ticker, date)
return nil unless security_prices.key?(ticker)
price = security_prices[ticker].find { |p| p.date == date }
price ? price[:price] : nil
end
def security_prices
@security_prices ||= begin
prices = {}
ticker_start_dates = {}
sync_entries.each do |entry|
unless ticker_start_dates[entry.account_trade.security.ticker]
ticker_start_dates[entry.account_trade.security.ticker] = entry.date
end
end
ticker_start_dates.each do |ticker, date|
prices[ticker] = Security::Price.find_prices(ticker: ticker, start_date: date, end_date: Date.current)
end
prices
end
end
def build_holdings_for_date(date)
trades = sync_entries.select { |trade| trade.date == date }
@portfolio = generate_next_portfolio(@portfolio, trades)
@portfolio.map do |ticker, holding|
trade = trades.find { |trade| trade.account_trade.security_id == holding[:security_id] }
trade_price = trade&.account_trade&.price
price = get_cached_price(ticker, date) || trade_price
account.holdings.build \
date: date,
security_id: holding[:security_id],
qty: holding[:qty],
price: price,
amount: price ? (price * holding[:qty]) : nil,
currency: holding[:currency]
end
end
def generate_next_portfolio(prior_portfolio, trade_entries)
trade_entries.each_with_object(prior_portfolio) do |entry, new_portfolio|
trade = entry.account_trade
price = trade.price
prior_qty = prior_portfolio.dig(trade.security.ticker, :qty) || 0
new_qty = prior_qty + trade.qty
new_portfolio[trade.security.ticker] = {
qty: new_qty,
price: price,
amount: new_qty * price,
currency: entry.currency,
security_id: trade.security_id
}
end
end
def upsert_holdings(holdings)
current_time = Time.now
holdings_to_upsert = holdings.map do |holding|
holding.attributes
.slice("date", "currency", "qty", "price", "amount", "security_id")
.merge("updated_at" => current_time)
end
account.holdings.upsert_all(holdings_to_upsert, unique_by: %i[account_id security_id date currency])
end
def load_prior_portfolio
prior_day_holdings = account.holdings.where(date: sync_date_range.begin - 1.day)
prior_day_holdings.each do |holding|
@portfolio[holding.security.ticker] = {
qty: holding.qty,
price: holding.price,
amount: holding.amount,
currency: holding.currency,
security_id: holding.security_id
}
end
end
def calculate_sync_start_date(start_date)
start_date || account.entries.account_trades.order(:date).first.try(:date) || Date.current
end
end