Investment Trade Conversion#
Investment trade conversion bridges the gap between raw synced investment transactions and properly structured trade records that drive portfolio tracking (holdings, cost basis, total return). Synced data from providers (SnapTrade, IBKR, Trading212, IndexaCapital) arrives as Transaction entries; conversion ensures security-level activity is stored as Trade entries with qty, price, and security_id so the portfolio engine can calculate accurate holdings.
Sure's data model uses Rails delegated types: an Entry (date, amount, account) points to one of Transaction, Trade, or Valuation. Both Transaction and Trade share the investment_activity_label field drawn from the canonical set in Transaction::ACTIVITY_LABELS .
Activity Label Assignment#
Each provider processor maps its native type strings to Sure's canonical labels via a hash constant before persisting entries through Account::ProviderImportAdapter.
| Provider | Label-mapping constant | Source |
|---|---|---|
| SnapTrade | SNAPTRADE_TYPE_TO_LABEL (25+ types) | |
| IBKR | buy_sell field + classify_cash_transaction | |
| Trading212 | classify_transaction | |
| IndexaCapital | ACTIVITY_TYPE_TO_LABEL |
Each processor also routes activity to either import_trade (security-level, e.g. BUY/SELL/REI) or import_transaction (cash flows, e.g. DIVIDEND/CONTRIBUTION/FEE) using TRADE_TYPES and CASH_TYPES constants.
The adapter auto-detects obvious labels from the transaction name when none is passed, and defaults Trade entries to "Buy" / "Sell" based on qty sign when no explicit label is provided.
In the transaction list UI, the category column is replaced by a color-coded quick-edit activity label badge for all accounts where supports_trades? returns true.
Dividend and Interest Handling#
Dividends and interest recorded manually are always stored as Trade records (not Transaction), with qty: 0 and price: 0 so they don't affect share counts — only the cash balance.
- Dividend trades require a security (the holding the income is attributed to).
- Interest trades allow an optional security; if omitted, the
Security.cash_for(account)synthetic cash security is used. - SnapTrade-synced dividends and interest arrive as
Transactionentries (inCASH_TYPES), notTradeentries — a key distinction from manually entered income.
Critical bug fixed in PR #2673: Income trades (qty: 0) were feeding price: 0 into PortfolioCache#load_prices, overriding market prices and wiping holdings to $0. The fix filters qty == 0 trades out of trade price sources and separates them from regular trades in Balance::BaseCalculator#flows_for_date.
Manual Conversion (Transaction → Trade)#
A user-facing Convert to Trade flow lets users upgrade synced Transaction entries into proper Trade records when the transaction represents a security buy or sell.
Entry points:
- The quick-edit badge carries a
convert_urlpointing toconvert_to_trade_transaction_path. GET /transactions/:id/convert_to_traderenders a modal with ticker/qty/price fields.POST /transactions/:id/create_trade_from_transactionprocesses the conversion: resolves the security viaSecurity::Resolver, calculates missing qty or price from the transaction amount, creates a newEntrywith aTradeentryable, marks it user-modified and import-locked, then marks the originalTransactionasexcluded: true.
The trade type (Buy/Sell) is inferred from the transaction amount sign: negative amount = money in = sell.
Provider Sign Normalization#
Sure's internal convention: inflows to an asset account are stored as negative amounts; outflows as positive. This is asserted in Entry#classification (amount.negative? ? "income" : "expense"), Transfer::Creator#inflow_transaction, and Balance::ForwardCalculator#signed_entry_flows.
Each provider processor normalizes raw amounts before calling the adapter. Two known sign bugs affect the SnapTrade and IndexaCapital processors:
SnapTrade — bare TRANSFER type (#2756)#
SnaptradeAccount::ActivitiesProcessor#normalize_cash_amount handles TRANSFER_IN and TRANSFER_OUT explicitly but has no branch for bare TRANSFER. Fidelity 401k contributions sent as type: "TRANSFER" with a positive amount fall through to the else branch, storing the provider's raw amount unchanged (positive) — which Sure's convention interprets as an outflow, producing a declining balance chart and negative display.
The suggested fix: add a when "TRANSFER" branch that inverts the provider's sign (-amount) since SnapTrade uses positive = money-in for this type. A PR is in progress.
User-modified entries will not self-heal — entries edited by the user have user_modified: true set and are skipped on re-sync. The transaction detail page's unlock control (POST /transactions/:id/unlock) clears this flag.
IndexaCapital — inverted convention (#2793)#
IndexaCapitalAccount::ActivitiesProcessor#normalize_cash_amount applies the opposite sign convention: money-in types (CONTRIBUTION, DIVIDEND, INTEREST) get amount.abs (positive) and money-out types get -amount.abs (negative) — the reverse of SnapTrade and of Sure's stated convention.
This is currently latent (not user-facing): Provider::IndexaCapital#get_activities returns nothing, so raw_activities_payload is always blank for Indexa accounts and the processor never runs on real data. The scaffolding is also missing an activities_processor_test.rb.
Key Source Files#
| File | Purpose |
|---|---|
app/controllers/transactions_controller.rb | convert_to_trade / create_trade_from_transaction actions |
app/models/account/provider_import_adapter.rb | Shared import_trade / import_transaction with deduplication and skip logic |
app/models/trade/create_form.rb | Manual dividend/interest income trade creation |
app/models/snaptrade_account/activities_processor.rb | SnapTrade type→label mapping, trade/cash split, sign normalization |
app/models/holding/portfolio_cache.rb | Filters income trades from price sources |
app/models/balance/base_calculator.rb | Separates income trades in flows_for_date |
app/views/investment_activity/_quick_edit_badge.html.erb | Activity label badge with convert-to-trade entry point |
Related KB articles: Broker Activity Import · Provider Import Adapter · Investment Activity Labels · Dividend and DRIP Modeling