Provider Import Adapter#
Account::ProviderImportAdapter is the shared import layer that all per-provider investment activity processors delegate to when persisting trades and cash transactions . It provides two public write methods — import_trade and import_transaction — and handles idempotent deduplication, type-collision guards, skip protection, and investment activity label detection. Provider-specific processors (SnapTrade, IndexaCapital, IBKR, Trading212) normalize raw provider data — including sign convention — before calling into the adapter.
Architecture: Two-Layer Design#
Each processor instantiates the adapter via @import_adapter ||= Account::ProviderImportAdapter.new(account) (SnapTrade at , IndexaCapital at ).
import_trade and import_transaction#
import_trade#
Creates or updates an Entry with a Trade entryable for security-level activity (buys, sells, reinvestments, options) . Deduplicates by find_or_initialize_by(external_id:, source:) . When no explicit activity label is supplied, defaults investment_activity_label to "Buy" for positive quantity or "Sell" for negative .
import_transaction#
Creates or updates an Entry with a Transaction entryable for pure cash flows (dividends, contributions, withdrawals, fees, transfers, interest) . Uses the same deduplication strategy: find_or_initialize_by(external_id:, source:) . For investment accounts, auto-detects obvious labels (dividend, interest, fee, contribution) from the transaction name when none is passed .
Shared Behaviors#
- Idempotent deduplication: Re-importing the same activity updates the existing record rather than creating a duplicate .
- Type-collision guard: Raises
ArgumentErrorif an existing entry with the sameexternal_idhas the wrong entryable type — e.g., aTradewhere aTransactionis expected . - Skip protection: Entries flagged as
excluded,user_modified, orimport_lockedare never overwritten . The adapter records skipped entries for statistics collection .
Provider Activity Processors#
SnapTrade#
Maps 25+ SnapTrade activity types to Sure labels via SNAPTRADE_TYPE_TO_LABEL. Routes activity by type using two 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
IndexaCapital#
Maps types via ACTIVITY_TYPE_TO_LABEL. Same structure: TRADE_TYPES include BUY, SELL, REINVEST; CASH_TYPES include the remaining cash flow types.
IBKR and Trading212#
Both providers have their own ActivitiesProcessor implementations following the same pattern . IBKR receives data pre-split into trades and cash_transactions keys; Trading212 separates at the payload level with raw_orders_payload, raw_dividends_payload, and raw_transactions_payload.
Sign Convention and normalize_cash_amount#
Sure's convention for Transaction entries: amount < 0 = cash inflow (money in); amount >= 0 = cash outflow (money out) .
Each processor normalizes the raw provider amount before calling the adapter. The two processors use opposite normalization for inflows and outflows:
| Activity class | SnapTrade sign | IndexaCapital sign |
|---|---|---|
| Inflow (Dividend, Contribution, Interest, Transfer In) | −amount.abs (negative) | +amount.abs (positive) |
| Outflow (Withdrawal, Fee, Tax, Transfer Out) | +amount.abs (positive) | −amount.abs (negative) |
SnapTrade normalize_cash_amount: inflows → -amount.abs, outflows → +amount.abs — conforming to Sure's sign convention where cash in is negative.
IndexaCapital normalize_cash_amount: the opposite — inflows → +amount.abs, outflows → -amount.abs. This is inverted relative to the SnapTrade/Sure convention and relative to the Transaction sign semantics.
⚠️ Sign inconsistency: The two processors apply mirrored sign conventions. When debugging import amounts or adding a new provider, confirm which convention the adapter expects and align accordingly.
Trade Quantity Signs#
Both processors force sell-side types to negative quantity and buy-side to positive, using SELL_SIDE_TYPES constants:
- SnapTrade SELL_SIDE_TYPES:
SELL,OPTION_SELL,ASSIGNED - IndexaCapital SELL_SIDE_TYPES:
SELL
The adapter then sets investment_activity_label based on quantity > 0 ? "Buy" : "Sell" when no explicit label is passed .
Key Source Files#
| File | Purpose |
|---|---|
app/models/account/provider_import_adapter.rb | Shared import_trade / import_transaction with deduplication and skip logic |
app/models/snaptrade_account/activities_processor.rb | SnapTrade type→label mapping, TRADE/CASH split, sign normalization |
app/models/indexa_capital_account/activities_processor.rb | IndexaCapital type→label mapping, TRADE/CASH split, sign normalization |
| Broker Activity Import | Overview of all providers (IBKR, Trading212) and shared import interface |
| Investment Account Flow Semantics | Explains the amount < 0 = inflow sign convention and how trades interact with balance calculator |
| Dividend and DRIP Modeling | Trade vs. Transaction distinction for dividends; DRIP reinvestment |