mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 05:09:38 +02:00
Handle holding quantity generation for reverse syncs correctly when not all holdings are generated for current day (#2417)
* Handle reverse calculator starting portfolio generation correctly * Fix current_holdings to handle different dates and hide zero quantities - Use DISTINCT ON to get most recent holding per security instead of assuming same date - Filter out zero quantity holdings from UI display - Maintain cash display regardless of zero balance - Use single efficient query with proper Rails syntax * Continue to process holdings even if one is not resolvable * Lint fixes
This commit is contained in:
parent
e60b5df442
commit
8db95623cf
8 changed files with 281 additions and 39 deletions
|
@ -82,7 +82,14 @@ class Account < ApplicationRecord
|
|||
end
|
||||
|
||||
def current_holdings
|
||||
holdings.where(currency: currency, date: holdings.maximum(:date)).order(amount: :desc)
|
||||
holdings.where(currency: currency)
|
||||
.where.not(qty: 0)
|
||||
.where(
|
||||
id: holdings.select("DISTINCT ON (security_id) id")
|
||||
.where(currency: currency)
|
||||
.order(:security_id, date: :desc)
|
||||
)
|
||||
.order(amount: :desc)
|
||||
end
|
||||
|
||||
def update_with_sync!(attributes)
|
||||
|
|
|
@ -52,7 +52,8 @@ class Holding::Materializer
|
|||
|
||||
def calculator
|
||||
if strategy == :reverse
|
||||
Holding::ReverseCalculator.new(account)
|
||||
portfolio_snapshot = Holding::PortfolioSnapshot.new(account)
|
||||
Holding::ReverseCalculator.new(account, portfolio_snapshot: portfolio_snapshot)
|
||||
else
|
||||
Holding::ForwardCalculator.new(account)
|
||||
end
|
||||
|
|
32
app/models/holding/portfolio_snapshot.rb
Normal file
32
app/models/holding/portfolio_snapshot.rb
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Captures the most recent holding quantities for each security in an account's portfolio.
|
||||
# Returns a portfolio hash compatible with the reverse calculator's format.
|
||||
class Holding::PortfolioSnapshot
|
||||
attr_reader :account
|
||||
|
||||
def initialize(account)
|
||||
@account = account
|
||||
end
|
||||
|
||||
# Returns a hash of {security_id => qty} representing today's starting portfolio.
|
||||
# Includes all securities from trades (with 0 qty if no holdings exist).
|
||||
def to_h
|
||||
@portfolio ||= build_portfolio
|
||||
end
|
||||
|
||||
private
|
||||
def build_portfolio
|
||||
# Start with all securities from trades initialized to 0
|
||||
portfolio = account.trades
|
||||
.pluck(:security_id)
|
||||
.uniq
|
||||
.each_with_object({}) { |security_id, hash| hash[security_id] = 0 }
|
||||
|
||||
# Get the most recent holding for each security and update quantities
|
||||
account.holdings
|
||||
.select("DISTINCT ON (security_id) security_id, qty")
|
||||
.order(:security_id, date: :desc)
|
||||
.each { |holding| portfolio[holding.security_id] = holding.qty }
|
||||
|
||||
portfolio
|
||||
end
|
||||
end
|
|
@ -1,8 +1,9 @@
|
|||
class Holding::ReverseCalculator
|
||||
attr_reader :account
|
||||
attr_reader :account, :portfolio_snapshot
|
||||
|
||||
def initialize(account)
|
||||
def initialize(account, portfolio_snapshot:)
|
||||
@account = account
|
||||
@portfolio_snapshot = portfolio_snapshot
|
||||
end
|
||||
|
||||
def calculate
|
||||
|
@ -21,7 +22,8 @@ class Holding::ReverseCalculator
|
|||
end
|
||||
|
||||
def calculate_holdings
|
||||
current_portfolio = generate_starting_portfolio
|
||||
# Start with the portfolio snapshot passed in from the materializer
|
||||
current_portfolio = portfolio_snapshot.to_h
|
||||
previous_portfolio = {}
|
||||
|
||||
holdings = []
|
||||
|
@ -38,24 +40,6 @@ class Holding::ReverseCalculator
|
|||
holdings
|
||||
end
|
||||
|
||||
def empty_portfolio
|
||||
securities = portfolio_cache.get_securities
|
||||
securities.each_with_object({}) { |security, hash| hash[security.id] = 0 }
|
||||
end
|
||||
|
||||
# Since this is a reverse sync, we start with today's holdings
|
||||
def generate_starting_portfolio
|
||||
holding_quantities = empty_portfolio
|
||||
|
||||
todays_holdings = account.holdings.where(date: Date.current)
|
||||
|
||||
todays_holdings.each do |holding|
|
||||
holding_quantities[holding.security_id] = holding.qty
|
||||
end
|
||||
|
||||
holding_quantities
|
||||
end
|
||||
|
||||
def transform_portfolio(previous_portfolio, trade_entries, direction: :forward)
|
||||
new_quantities = previous_portfolio.dup
|
||||
|
||||
|
|
|
@ -8,11 +8,14 @@ class PlaidAccount::Investments::HoldingsProcessor
|
|||
holdings.each do |plaid_holding|
|
||||
resolved_security_result = security_resolver.resolve(plaid_security_id: plaid_holding["security_id"])
|
||||
|
||||
return unless resolved_security_result.security.present?
|
||||
next unless resolved_security_result.security.present?
|
||||
|
||||
security = resolved_security_result.security
|
||||
holding_date = plaid_holding["institution_price_as_of"] || Date.current
|
||||
|
||||
holding = account.holdings.find_or_initialize_by(
|
||||
security: resolved_security_result.security,
|
||||
date: Date.current,
|
||||
security: security,
|
||||
date: holding_date,
|
||||
currency: plaid_holding["iso_currency_code"]
|
||||
)
|
||||
|
||||
|
@ -22,7 +25,15 @@ class PlaidAccount::Investments::HoldingsProcessor
|
|||
amount: plaid_holding["quantity"] * plaid_holding["institution_price"]
|
||||
)
|
||||
|
||||
holding.save!
|
||||
ActiveRecord::Base.transaction do
|
||||
holding.save!
|
||||
|
||||
# Delete all holdings for this security after the institution price date
|
||||
account.holdings
|
||||
.where(security: security)
|
||||
.where("date > ?", holding_date)
|
||||
.destroy_all
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue