# Investment Account Holdings and Securities Tracking Architecture in Maybe

> Discover the Investment Account Holdings and Securities Tracking architecture in Maybe. Learn how Maybe finance uses ActiveRecord models and Plaid integration for seamless portfolio analytics.

- Repository: [Maybe/maybe](https://github.com/maybe-finance/maybe)
- Tags: architecture
- Published: 2026-03-07

---

**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`](https://github.com/maybe-finance/maybe/blob/main/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`](https://github.com/maybe-finance/maybe/blob/main/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`](https://github.com/maybe-finance/maybe/blob/main/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`](https://github.com/maybe-finance/maybe/blob/main/app/models/plaid_account/investments/holdings_processor.rb) follows four distinct steps:

1. **Raw Payload Processing**: Extracts holdings data from `plaid_account.raw_investments_payload["holdings"]`
2. **Security Resolution**: Uses `SecurityResolver` in [`app/models/plaid_account/investments/security_resolver.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/plaid_account/investments/security_resolver.rb) to map Plaid identifiers to local Security records
3. **Record Creation**: Invokes `find_or_initialize_by` for account, security, date, and currency combinations, then assigns `qty`, `price`, and `amount` before saving within a transaction
4. **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`](https://github.com/maybe-finance/maybe/blob/main/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

```ruby

# 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`](https://github.com/maybe-finance/maybe/blob/main/app/models/account.rb), [`app/models/holding.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/holding.rb), and [`app/models/security.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/security.rb).
- **Holdings** represent time-stamped snapshots of security positions, storing quantity, price, and market value for specific dates in the `account_holdings` table.
- The **HoldingsProcessor** in [`app/models/plaid_account/investments/holdings_processor.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/plaid_account/investments/holdings_processor.rb) automates ingestion from Plaid, resolving securities via **SecurityResolver** and deduplicating historical records.
- Calculated metrics such as `weight` and `avg_cost` provide portfolio allocation and cost-basis analysis without requiring additional database columns.
- The `current_holdings` scope on **Account** efficiently retrieves the latest non-zero positions using `DISTINCT ON` subqueries.

## 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.