mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-18 20:59:39 +02:00
* PlaidConnectable concern * Remove bad abstraction * Put sync implementations in own concerns * Sync strategies * Move sync orchestration to Sync class * Clean up sync class, add state machine * Basic market data sync cron * Fix price sync * Improve sync window column names, add timestamps * 30 day syncs by default * Clean up market data methods * Report high duplicate sync counts to Sentry * Add sync states throughout app * account tab session * Persistent account tab selections * Remove manual sleep * Add migration to clear stale syncs on self hosted apps * Tweak sync states * Sync completion event broadcasts * Fix timezones in tests * Cleanup * More cleanup * Plaid item UI broadcasts for sync * Fix account ID namespace conflict * Sync broadcasters * Smoother account sync refreshes * Remove test sync delay
135 lines
3.9 KiB
Ruby
135 lines
3.9 KiB
Ruby
class Holding::PortfolioCache
|
|
attr_reader :account, :use_holdings
|
|
|
|
class SecurityNotFound < StandardError
|
|
def initialize(security_id, account_id)
|
|
super("Security id=#{security_id} not found in portfolio cache for account #{account_id}. This should not happen unless securities were preloaded incorrectly.")
|
|
end
|
|
end
|
|
|
|
def initialize(account, use_holdings: false)
|
|
@account = account
|
|
@use_holdings = use_holdings
|
|
load_prices
|
|
end
|
|
|
|
def get_trades(date: nil)
|
|
if date.blank?
|
|
trades
|
|
else
|
|
trades.select { |t| t.date == date }
|
|
end
|
|
end
|
|
|
|
def get_price(security_id, date, source: nil)
|
|
security = @security_cache[security_id]
|
|
raise SecurityNotFound.new(security_id, account.id) unless security
|
|
|
|
if source.present?
|
|
price = security[:prices].select { |p| p.price.date == date && p.source == source }.min_by(&:priority)&.price
|
|
else
|
|
price = security[:prices].select { |p| p.price.date == date }.min_by(&:priority)&.price
|
|
end
|
|
|
|
return nil unless price
|
|
|
|
price_money = Money.new(price.price, price.currency)
|
|
|
|
converted_amount = price_money.exchange_to(account.currency, fallback_rate: 1).amount
|
|
|
|
Security::Price.new(
|
|
security_id: security_id,
|
|
date: price.date,
|
|
price: converted_amount,
|
|
currency: account.currency
|
|
)
|
|
end
|
|
|
|
def get_securities
|
|
@security_cache.map { |_, v| v[:security] }
|
|
end
|
|
|
|
private
|
|
PriceWithPriority = Data.define(:price, :priority, :source)
|
|
|
|
def trades
|
|
@trades ||= account.entries.includes(entryable: :security).trades.chronological.to_a
|
|
end
|
|
|
|
def holdings
|
|
@holdings ||= account.holdings.chronological.to_a
|
|
end
|
|
|
|
def collect_unique_securities
|
|
unique_securities_from_trades = trades.map(&:entryable).map(&:security).uniq
|
|
|
|
return unique_securities_from_trades unless use_holdings
|
|
|
|
unique_securities_from_holdings = holdings.map(&:security).uniq
|
|
|
|
(unique_securities_from_trades + unique_securities_from_holdings).uniq
|
|
end
|
|
|
|
# Loads all known prices for all securities in the account with priority based on source:
|
|
# 1 - DB or provider prices
|
|
# 2 - Trade prices
|
|
# 3 - Holding prices
|
|
def load_prices
|
|
@security_cache = {}
|
|
securities = collect_unique_securities
|
|
|
|
Rails.logger.info "Preloading #{securities.size} securities for account #{account.id}"
|
|
|
|
securities.each do |security|
|
|
Rails.logger.info "Loading security: ID=#{security.id} Ticker=#{security.ticker}"
|
|
|
|
# High priority prices from DB (synced from provider)
|
|
db_prices = security.prices.where(date: account.start_date..Date.current).map do |price|
|
|
PriceWithPriority.new(
|
|
price: price,
|
|
priority: 1,
|
|
source: "db"
|
|
)
|
|
end
|
|
|
|
# Medium priority prices from trades
|
|
trade_prices = trades
|
|
.select { |t| t.entryable.security_id == security.id }
|
|
.map do |trade|
|
|
PriceWithPriority.new(
|
|
price: Security::Price.new(
|
|
security: security,
|
|
price: trade.entryable.price,
|
|
currency: trade.entryable.currency,
|
|
date: trade.date
|
|
),
|
|
priority: 2,
|
|
source: "trade"
|
|
)
|
|
end
|
|
|
|
# Low priority prices from holdings (if applicable)
|
|
holding_prices = if use_holdings
|
|
holdings.select { |h| h.security_id == security.id }.map do |holding|
|
|
PriceWithPriority.new(
|
|
price: Security::Price.new(
|
|
security: security,
|
|
price: holding.price,
|
|
currency: holding.currency,
|
|
date: holding.date
|
|
),
|
|
priority: 3,
|
|
source: "holding"
|
|
)
|
|
end
|
|
else
|
|
[]
|
|
end
|
|
|
|
@security_cache[security.id] = {
|
|
security: security,
|
|
prices: db_prices + trade_prices + holding_prices
|
|
}
|
|
end
|
|
end
|
|
end
|