Investment Trade Entry#
Manual buy/sell trade entry is available for any account where Account#supports_trades? returns true — all Investment subtypes and Crypto exchange accounts. Trades are stored as Entry records whose entryable is a Trade model.
Entry Form#
The UI form (app/views/trades/_form.html.erb) presents a type selector with six options :
| Type | Key Fields |
|---|---|
| buy | Security (ticker combobox), date, qty, price, fee |
| sell | Security (ticker combobox), date, qty, price, fee |
| dividend | Security (dropdown from account's holdings), date, amount |
| interest | Security (optional), date, amount |
| deposit | Date, amount, (optional) transfer account |
| withdrawal | Date, amount, (optional) transfer account |
For buy/sell, the security field is a combobox backed by SecuritiesController#index, which delegates to Security.search_provider — a concurrent multi-provider search with an 8-second per-provider timeout and 5-minute result cache . If no price providers are configured, a plain manual_ticker text field is shown instead .
Qty must be positive; the sign is applied by the form object (+qty for buy, -qty for sell) . The entry amount is computed as signed_qty × price + fee .
Create Flow#
Both the web UI (TradesController) and the REST API (Api::V1::TradesController) delegate to Trade::CreateForm:
- Security resolution —
Trade::CreateFormcallsSecurity::Resolverto find or create the security from the parsed combobox ID (SYMBOL|EXCHANGE|PROVIDERformat) or the raw manual ticker . - Record creation — saves an
Entrywith a nestedTrade, settinginvestment_activity_labelto"Buy"or"Sell"automatically . - Post-save hooks —
Entry#lock_saved_attributes!andmark_user_modified!protect the record from being overwritten by a subsequent provider sync;account.sync_laterqueues an async balance/holding recalculation .
The API controller additionally validates that the target account supports_trades? and allows optional investment_activity_label and category_id overrides post-creation .
Permitted create params (API): account_id, date, qty, price, currency, security_id, ticker, manual_ticker, investment_activity_label, category_id, fee, type, amount, transfer_account_id .
Trade Model Validations#
Trade validates:
qtyandprice+currencypresenceinvestment_activity_labelmust be one ofACTIVITY_LABELS(shared withTransaction) ornilexchange_ratemust be a finite positive number when present
buy? / sell? check the sign of qty. Trades are always excluded from expense budgets via excluded_from_budget?.
Cost Basis & Gain/Loss Tracking#
Holding cost basis is materialized on sync, not computed on-demand. After a trade is saved, account.sync_later triggers Holding::Materializer, which calls either:
Holding::ForwardCalculator(manual accounts) — iterates chronologically, accumulating weighted-average cost from buy trades .Holding::ReverseCalculator(linked/synced accounts) — works backward; pre-computes cost basis snapshots and uses binary search for lookups .
The resulting avg_cost on each Holding record is the weighted average purchase price in account currency . Cost basis has a source priority: manual (user-locked) > calculated (from trades) > provider ; the Holding::CostBasisReconciler never overwrites a higher-priority source .
Unrealized gain/loss is computed in Trade#unrealized_gain_loss: (current_price × qty) - (entry_price × qty).
Realized gain/loss is computed in Trade#realized_gain_loss for sell trades: sale_proceeds - (holding.avg_cost × qty.abs). It uses a preloaded holdings array when set by the reports controller to avoid N+1 queries .
Key Files#
| File | Purpose |
|---|---|
app/models/trade.rb | Trade model, validations, gain/loss methods |
app/models/trade/create_form.rb | Creation logic for all trade types |
app/controllers/trades_controller.rb | Web UI controller |
app/controllers/api/v1/trades_controller.rb | REST API controller |
app/views/trades/_form.html.erb | Entry form partial |
app/models/holding.rb | Holding model with avg_cost |
app/models/holding/forward_calculator.rb | Cost basis calc for manual accounts |
app/models/holding/reverse_calculator.rb | Cost basis calc for linked accounts |