Files
sure/app/models/assistant/function/get_holdings.rb
LPW fa78e1d292 Improve handling of cost_basis during holding materialization and display (#619)
- Refactored `persist_holdings` to separate and conditionally upsert holdings with and without cost_basis.
- Updated `avg_cost` logic to treat 0 cost_basis as unknown and return nil when cost_basis cannot be determined.
- Modified trend and investment calculation to exclude holdings with unknown cost_basis.
- Adjusted `average_cost` formatting to handle nil values in API responses and views.
- Added comprehensive tests to ensure cost_basis preservation and fallback behavior.
- Localized `unknown` label for display when cost_basis is unavailable.

Co-authored-by: Josh Waldrep <joshua.waldrep5+github@gmail.com>
2026-01-11 23:58:51 +01:00

168 lines
4.5 KiB
Ruby

class Assistant::Function::GetHoldings < Assistant::Function
include Pagy::Backend
SUPPORTED_ACCOUNT_TYPES = %w[Investment Crypto].freeze
class << self
def default_page_size
50
end
def name
"get_holdings"
end
def description
<<~INSTRUCTIONS
Use this to search user's investment holdings by using various optional filters.
This function is great for things like:
- Finding specific holdings or securities
- Getting portfolio composition and allocation
- Viewing investment performance and cost basis
Note: This function only returns holdings from Investment and Crypto accounts.
Note on pagination:
This function can be paginated. You can expect the following properties in the response:
- `total_pages`: The total number of pages of results
- `page`: The current page of results
- `page_size`: The number of results per page (this will always be #{default_page_size})
- `total_results`: The total number of results for the given filters
- `total_value`: The total value of all holdings for the given filters
Simple example (all current holdings):
```
get_holdings({
page: 1
})
```
More complex example (various filters):
```
get_holdings({
page: 1,
accounts: ["Brokerage Account"],
securities: ["AAPL", "GOOGL"]
})
```
INSTRUCTIONS
end
end
def strict_mode?
false
end
def params_schema
build_schema(
required: [ "page" ],
properties: {
page: {
type: "integer",
description: "Page number"
},
accounts: {
type: "array",
description: "Filter holdings by account name (only Investment and Crypto accounts are supported)",
items: { enum: investment_account_names },
minItems: 1,
uniqueItems: true
},
securities: {
type: "array",
description: "Filter holdings by security ticker symbol",
items: { enum: family_security_tickers },
minItems: 1,
uniqueItems: true
}
}
)
end
def call(params = {})
holdings_query = build_holdings_query(params)
pagy, paginated_holdings = pagy(
holdings_query.includes(:security, :account).order(amount: :desc),
page: params["page"] || 1,
limit: default_page_size
)
total_value = holdings_query.sum(:amount)
normalized_holdings = paginated_holdings.map do |holding|
{
ticker: holding.ticker,
name: holding.name,
quantity: holding.qty.to_f,
price: holding.price.to_f,
currency: holding.currency,
amount: holding.amount.to_f,
formatted_amount: holding.amount_money.format,
weight: holding.weight&.round(2),
average_cost: holding.avg_cost&.to_f,
formatted_average_cost: holding.avg_cost&.format,
account: holding.account.name,
date: holding.date
}
end
{
holdings: normalized_holdings,
total_results: pagy.count,
page: pagy.page,
page_size: default_page_size,
total_pages: pagy.pages,
total_value: Money.new(total_value, family.currency).format
}
end
private
def default_page_size
self.class.default_page_size
end
def build_holdings_query(params)
accounts = investment_accounts
if params["accounts"].present?
accounts = accounts.where(name: params["accounts"])
end
holdings = Holding.where(account: accounts)
.where(
id: Holding.where(account: accounts)
.select("DISTINCT ON (account_id, security_id) id")
.where.not(qty: 0)
.order(:account_id, :security_id, date: :desc)
)
if params["securities"].present?
security_ids = family.securities.where(ticker: params["securities"]).pluck(:id)
holdings = holdings.where(security_id: security_ids)
end
holdings
end
def investment_accounts
family.accounts.visible.where(accountable_type: SUPPORTED_ACCOUNT_TYPES)
end
def investment_account_names
@investment_account_names ||= investment_accounts.pluck(:name)
end
def family_security_tickers
@family_security_tickers ||= Security
.where(id: Holding.where(account_id: investment_accounts.select(:id)).select(:security_id))
.distinct
.pluck(:ticker)
end
end