Dosu LogoDosu Logo
Ask
Join our Discord
SurePublic
we-promise
DocumentsSure
Investment Account Flow Semantics
Investment Account Flow Semantics
Type
Topic
Status
Published
Created
Jul 12, 2026
Updated
Jul 12, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

Investment Account Flow Semantics#

Overview#

Investment accounts require inverted cash/non-cash flow semantics compared to regular transaction entries. The core insight: buying a security is simultaneously a cash outflow (money leaves the account) and a non-cash inflow (holdings value enters the account). The balance calculator in Balance::BaseCalculator models this dual-sided nature explicitly, keeping cash and holdings balances consistent without double-counting.

Entry Types and Sign Conventions#

All monetary events are stored as Entry rows. Investment accounts use two entryable types relevant here:

  • Transaction — income, expenses, deposits, withdrawals, transfers. Standard semantics: amount < 0 = inflow (cash in), amount >= 0 = outflow (cash out).
  • Trade — buy/sell events. Same signed-amount storage, but interpreted inversely by the flow calculator.

Trade::CreateForm sets amount = signed_qty × price + fee, where signed_qty is positive for buys and negative for sells . This means a buy produces a positive entry amount (outflow sign), and a sell produces a negative amount (inflow sign) — consistent with how cash moves, but inverted relative to what holdings are doing.

The Inversion Logic in flows_for_date#

BaseCalculator#flows_for_date separates Trade entries from Transaction entries and applies the inversion explicitly:

cash_inflows = txn_inflow_sum.abs + trade_cash_inflow_sum.abs # both add to cash
cash_outflows = txn_outflow_sum + trade_cash_outflow_sum # both drain cash

# Trades are inverse: cash outflow to buy = non-cash (holdings) inflow
non_cash_outflows = trade_cash_inflow_sum.abs # sell → holdings leave
non_cash_inflows = trade_cash_outflow_sum # buy → holdings enter

The comment in the source is explicit: "Trades are inverse (a 'buy' is outflow of cash, but 'inflow' of non-cash, aka 'holdings')" .

Transactions are never assigned to non-cash flows; only trades move the holdings dimension.

How Balances Are Derived#

For investment accounts, end_non_cash_balance is taken directly from holdings_value_for_date() rather than being accumulated from flows . Holdings values come from the Balance::SyncCache, which snapshots the materialized holdings table at sync time .

Cash balance is then: total_balance − holdings_value .

Market value change (price appreciation, not trades) is isolated in market_value_change_on_date:

net_market_flows = Δholdings_value − net_buy_sell_value

This strips out the effect of buys/sells so only genuine price-driven changes are attributed to net_market_flows.

Asset/Liability Sign Factor#

All flow values stored in Balance rows are multiplied by flows_factor: +1 for asset accounts, -1 for liabilities . Investment accounts are always assets, so flows_factor = +1 and no additional sign flip occurs there.

Quick Reference#

Entry typeamount < 0 (negative)amount >= 0 (positive)
TransactionCash inflowCash outflow
Trade (buy)—Cash outflow + non-cash inflow (holdings ↑)
Trade (sell)Cash inflow + non-cash outflow (holdings ↓)—

Key Files#

FilePurpose
app/models/balance/base_calculator.rbCore flow categorization and inversion logic
app/models/trade/create_form.rbSets signed amount and qty on trade entries
app/models/trade.rbbuy?/sell? helpers based on qty sign
Documents
Account Authorization and Permissions
Account Balance Calculation
Account Creation
Account Lifecycle Management
Account Provider Architecture
Account Reporting Controls
Account Statement Management
Account Statement Reconciliation
Account Type Architecture
AI Bank Statement Extraction
AI Chat Interface
API Authentication and Authorization
Authentication and Session Management
Balance History System
Banking Data Encryption
Banking Provider Integration
Broker Activity Import
Budget Management
Category Management
Cryptocurrency Account Management
CSV Import and Column Mapping
Currency Management
Dashboard Filtering and Drilldowns
Depository Yield Modeling
Dev Container Setup
Dividend and DRIP Modeling
Docker Self-Hosting
Enable Banking Consent Management
Enable Banking Error Handling
Enable Banking OAuth and PSD2 Authentication
Family Data Export
Family Settings Management
Financial Insights and Metrics
Financial Reporting
FIRE Planning and Retirement Calculations
Goals and Savings Tracking
Internationalization and Localization
Investment Account Data Pipeline
Investment Account Flow Semantics
Investment Account Reconciliation
Investment Activity Labels
Investment Holdings Management
Investment Tax Treatment Classification
Investment Trade Conversion
Investment Trade Entry
Invitation Lifecycle and State Management
Ledger Entry Accounting Model
LLM Provider Configuration
LLM Request Timeout and Watchdog System
LLM Tool Calling
Manual Account Entry and Import
Manual Valuation
MCP Tool Access
Multi-Currency Exchange Rates
NDJSON Import System
Net Worth Balance Sheet
OIDC Provider Configuration
Pending Transaction Reconciliation
Plaid Integration
Provider Import Adapter
Rails Development Environment Configuration
Rails PWA Integration
Recurring Transactions and Cash Flow Projection
REST API Architecture
Securities Lookup
Security Exchange Identification
Security Price Import Pipeline
SimpleFIN Holdings Import
SimpleFIN Integration
SimpleFIN Liability Balance Normalization
SnapTrade Integration
Split Transactions
SSO Audit Logging
SSO Authentication Flow
SSO Provider Management
Timezone-Aware Financial Data Handling
Transaction Categorization
Transaction Deduplication
Transaction Exclusion
Transaction Filtering and Search
Transaction Management
Transaction Rule Engine
Transaction Sync Windows and Lookback
Transfer Management
Transfer Matching and Pairing
Turbo Frame Navigation
Yahoo Finance Integration
How split transaction child exclusion was implemented
Is it possible to edit the date of a synced transaction (e.g., one synced by LunchFlow)?
Provider Architecture