Investment Account Holdings and Securities Tracking Architecture in Maybe
The Maybe finance platform tracks investment holdings and securities through a tightly-coupled ActiveRecord architecture involving Account, Holding, and Security models, with automated ingestion from Plaid and calculated fields for portfolio analytics.
The maybe-finance/maybe repository implements a robust domain-driven design for investment account holdings and securities tracking that powers personal finance management. This Ruby on Rails application uses specialized ActiveRecord models to represent brokerage accounts, individual security positions, and the underlying financial instruments, creating a complete picture of an investor's portfolio.
Core Domain Models for Investment Tracking
The architecture centers on three primary models that establish ownership, snapshot positions, and instrument definitions.
Account Model
Located in app/models/account.rb, the Account model represents financial accounts such as brokerage or crypto wallets. It defines has_many :holdings and has_many :securities, through: :holdings associations, establishing ownership of investment positions. The model provides the current_holdings scope that returns the latest non-zero holdings per security using a DISTINCT ON (security_id) subquery.
Holding Model
The Holding model in app/models/holding.rb stores time-stamped snapshots of security positions in the account_holdings table. Each record captures quantity, price, and market value for a specific date, belonging to both an Account and a Security. The model implements calculated methods including weight for portfolio allocation percentage and avg_cost for cost-basis approximation based on trade history.
Security Model
Found in app/models/security.rb, the Security model represents underlying financial instruments such as stocks, ETFs, and cryptocurrencies. It maintains has_many :trades and has_many :prices associations for historical transaction and market data. The model exposes current_price to retrieve the latest market price and provides combobox representations for UI components.
Data Ingestion from External Providers
When users link brokerage accounts via Plaid, the HoldingsProcessor automates the transformation of external data into local Holding records.
The ingestion pipeline in app/models/plaid_account/investments/holdings_processor.rb follows four distinct steps:
- Raw Payload Processing: Extracts holdings data from
plaid_account.raw_investments_payload["holdings"] - Security Resolution: Uses
SecurityResolverinapp/models/plaid_account/investments/security_resolver.rbto map Plaid identifiers to local Security records - Record Creation: Invokes
find_or_initialize_byfor account, security, date, and currency combinations, then assignsqty,price, andamountbefore saving within a transaction - Deduplication: Deletes older duplicate holdings for the same security, ensuring only the most recent snapshot persists while maintaining historical accuracy for different dates
Calculated Portfolio Metrics
The architecture derives key analytics directly from the data model without requiring additional database columns.
Weight Calculation
The Holding#weight method calculates the position's percentage of the total account balance. When the account balance equals zero, the method returns 1 to prevent division errors. This metric powers portfolio allocation visualizations throughout the UI.
Average Cost Basis
The Holding#avg_cost method computes cost basis by analyzing the parent account's trade history. It joins exchange-rate data to normalize currencies, filters trades to include only those relevant to the holding's security and date range, and computes the average trade price. This approach provides an approximate cost basis without requiring explicit lot-level tracking.
Querying and Managing Holdings
The system provides optimized scopes and safe deletion methods for portfolio management.
Current Holdings Scope
The Account#current_holdings scope in app/models/account.rb constructs a subquery using DISTINCT ON (security_id) to retrieve the latest holding per security. It filters out zero-quantity positions and orders results by market value, providing an efficient way to display current portfolio snapshots without loading historical records.
Deletion with Transaction Cleanup
The Holding#destroy_holding_and_entries! method supports manual removal of positions. When invoked, it destroys the holding record and all associated entry records (trade entries) within a single database transaction. This ensures referential integrity is maintained while allowing users to remove erroneous or closed positions from their portfolio history.
Practical Implementation Examples
# 1️⃣ Create a new security (if it does not already exist)
security = Security.find_or_create_by!(ticker: "AAPL") do |s|
s.name = "Apple Inc."
s.exchange_operating_mic = "XNAS"
end
# 2️⃣ Add a holding to a brokerage account
account = Account.find(42) # an Investment type account
holding = account.holdings.create!(
security: security,
date: Date.current,
currency: "USD",
qty: 10,
price: 170.25, # price per share
amount: 10 * 170.25
)
# 3️⃣ Retrieve the holding’s weight in the portfolio
puts "Holding weight: #{holding.weight.round(2)}%" # => e.g. "Holding weight: 12.34%"
# 4️⃣ Compute average cost based on prior trades
puts "Avg cost: #{holding.avg_cost.format}" # => Money formatted string
# 5️⃣ Get the current market price of the underlying security
price = security.current_price
puts "Current price: #{price.format}" # => Money formatted string
Summary
- The architecture centers on three core ActiveRecord models: Account, Holding, and Security, defined in
app/models/account.rb,app/models/holding.rb, andapp/models/security.rb. - Holdings represent time-stamped snapshots of security positions, storing quantity, price, and market value for specific dates in the
account_holdingstable. - The HoldingsProcessor in
app/models/plaid_account/investments/holdings_processor.rbautomates ingestion from Plaid, resolving securities via SecurityResolver and deduplicating historical records. - Calculated metrics such as
weightandavg_costprovide portfolio allocation and cost-basis analysis without requiring additional database columns. - The
current_holdingsscope on Account efficiently retrieves the latest non-zero positions usingDISTINCT ONsubqueries.
Frequently Asked Questions
How does Maybe handle duplicate holdings from Plaid syncs?
When processing Plaid data, the HoldingsProcessor creates new holding records using find_or_initialize_by keyed to account, security, date, and currency. After saving the latest snapshot, it explicitly deletes older duplicate holdings for the same security, ensuring only the most recent data persists while maintaining historical accuracy for different dates.
What is the difference between a Holding and a Security in Maybe?
A Security represents the underlying financial instrument—such as a stock, ETF, or cryptocurrency—stored in the securities table with identifiers like ticker symbols and exchange codes. A Holding represents a specific position in that security within a particular account, capturing the quantity, price, and market value as of a specific date in the account_holdings table.
How is the average cost basis calculated for a holding?
The Holding#avg_cost method calculates cost basis by analyzing the parent account's trade history. It joins exchange-rate data to normalize currencies, filters trades to include only those relevant to the holding's security and date range, and computes the average trade price. This approach provides an approximate cost basis without requiring explicit lot-level tracking.
Can holdings be manually deleted in Maybe?
Yes, the Holding#destroy_holding_and_entries! method supports manual removal of positions. When invoked, it destroys the holding record and all associated entry records (trade entries) within a single database transaction. This ensures referential integrity is maintained while allowing users to remove erroneous or closed positions from their portfolio history.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →