Broker Activity Import#
When a brokerage account syncs, raw activities from the provider API are processed by a per-provider ActivitiesProcessor class that classifies each activity and routes it to one of two record types:
Trade(entry withTradeentryable) — for security-level activity with a quantity: buys, sells, reinvestments, option events.Transaction(entry withTransactionentryable) — for pure cash flows: dividends, contributions, withdrawals, fees, transfers, interest.
Both record types share the same investment_activity_label field drawn from a canonical set defined in Transaction::ACTIVITY_LABELS . See Investment Activity Labels for label semantics, analytics impact, and UI badge behavior.
Supported Providers and Processors#
Three providers each have their own ActivitiesProcessor:
| Provider | Processor | Source |
|---|---|---|
| SnapTrade (Fidelity, Schwab, Robinhood, etc.) | SnaptradeAccount::ActivitiesProcessor | SnapTrade activity API |
| IBKR | IbkrAccount::ActivitiesProcessor | IBKR raw_activities_payload (split into trades + cash_transactions) |
| Trading212 | Trading212Account::ActivitiesProcessor | Three separate payloads: raw_orders_payload, raw_dividends_payload, raw_transactions_payload |
Each processor is called as part of the account's sync pipeline. For SnapTrade, activities are often fetched asynchronously via SnaptradeActivitiesFetchJob due to brokerage sync delays of 30–60+ seconds on fresh connections .
Trade vs. Transaction Split#
Each processor separates incoming activity into Trade and Transaction records at a defined boundary.
SnapTrade uses two explicit constants :
TRADE_TYPES—BUY,SELL,REI,REINVEST,OPTION_BUY,OPTION_SELL,EXERCISED,ASSIGNED→import_tradeCASH_TYPES—DIVIDEND,DIV,CONTRIBUTION,WITHDRAWAL,TRANSFER_*,INTEREST,FEE,TAX,CASH→import_transaction- Unknown types default to
"Other"label and go throughprocess_cash_activity.
IBKR receives data pre-split: the payload has a trades key (buy/sell equity trades → import_trade) and a cash_transactions key (dividends, deposits/withdrawals → import_transaction) . Only DEPOSITS/WITHDRAWALS and DIVIDENDS cash types are supported; others are skipped . Commission/fee for each trade is imported as a separate Transaction with label "Fee" .
Trading212 separates at the payload level :
raw_orders_payload(filled order records) →import_traderaw_dividends_payload(dividend records) →import_transactionwith label"Dividend"raw_transactions_payload(cash events:DEPOSIT,WITHDRAW,INTEREST,FEE) →import_transaction
Label Mapping Per Provider#
Each processor maps provider-native type strings to Sure's canonical investment_activity_label values.
SnapTrade uses SNAPTRADE_TYPE_TO_LABEL, a hash covering 25+ types. Notable non-obvious mappings: TAX → "Fee", STOCK_DIVIDEND → "Dividend", CASH → "Contribution", SPLIT/MERGER/SPIN_OFF → "Other" . Cash amount sign is normalized in normalize_cash_amount: income flows are stored as negative amounts; outflows as positive.
IBKR classifies cash at classify_cash_transaction:
DEPOSITS/WITHDRAWALSwith positive amount →"Contribution", negative →"Withdrawal"DIVIDENDS→"Dividend"(stored as-amount.abs)- Trades are always
"Buy"or"Sell"based on thebuy_sellfield
Trading212 classifies via classify_transaction:
DEPOSIT→"Contribution",WITHDRAW→"Withdrawal",INTEREST→"Interest",FEE→"Fee"- Orders use
order[:side](BUY/SELL) to set"Buy"or"Sell" - Dividends are always
"Dividend"
Shared Import Interface: Account::ProviderImportAdapter#
All processors delegate to Account::ProviderImportAdapter, which provides two methods used across all providers:
import_trade— creates aTrade-typeEntryfor security activityimport_transaction— creates aTransaction-typeEntryfor cash flows
Both methods use find_or_initialize_by(external_id:, source:) for idempotent deduplication : re-importing the same activity updates the existing record rather than creating a duplicate. The external_id is provider-namespaced (e.g., ibkr_trade_<trade_id>, trading212_order_<order_id>) to avoid cross-provider collisions .
If a matching external_id exists but with a different entryable type (e.g., a Trade where a Transaction is expected), the adapter raises an ArgumentError to prevent type collisions . Records flagged as user-modified or import-locked are not overwritten.
Key Source Files#
| File | Purpose |
|---|---|
app/models/snaptrade_account/activities_processor.rb | SnapTrade type→label mapping, TRADE_TYPES/CASH_TYPES split |
app/models/ibkr_account/activities_processor.rb | IBKR trade + cash transaction processing, commission import |
app/models/trading212_account/activities_processor.rb | Trading212 orders, dividends, and cash transaction processing |
app/models/account/provider_import_adapter.rb | Shared import_trade / import_transaction with deduplication |
| Investment Activity Labels | Full label set, budget/analytics impact, UI badge behavior |
| SnapTrade Integration | SnapTrade sync pipeline and async activities fetch |
| Dividend and DRIP Modeling | Trade vs. Transaction distinction for dividends; DRIP reinvestment |