03.Trades & executions
Executions are the atom. Trades are FIFO-derived. The roll-up is deterministic, idempotent, and safe to re-run at any time. This page covers the data model, the views, and the editing endpoints (split / merge / risk / bulk).
The data model in one sentence
Executions are the source of truth; trades are a materialized view. Every broker fill is one immutable row in executions; traderview-core::rollup walks those rows in (account_id, symbol) partitions and FIFO-matches buy/sell pairs into the trades table. Re-running the roll-up at any time produces the same trade IDs and P&L for the same execution input.
| Table | Columns (abridged) |
|---|---|
executions | id, account_id, symbol, side, qty, price, multiplier, fees, executed_at, asset_class, broker_order_id, fx_rate, imported_at, import_id |
trades | id, account_id, symbol, side (long/short), entry_avg, exit_avg, qty, gross_pnl, net_pnl, status (open/closed), opened_at, closed_at, asset_class, r_multiple, planned_risk, tags, … |
Every trades row also links to its constituent execution IDs via trade_executions so you can drill down to the exact fills that built it.
FIFO roll-up — what the algorithm actually does
In crates/traderview-core/src/rollup.rs:
- Group all executions by
(account_id, symbol). - Sort each group by
executed_at ASC, id ASC(the tie-break onidmakes the order deterministic if two fills land in the same millisecond). - Walk the sorted list with a position pointer and an open-lot queue:
- Same-side fill → push a new lot onto the queue.
- Opposite-side fill → pop FIFO from the queue, match qty, accumulate realized P&L. If the opposite-side qty exceeds the queue, flip to short (or long) and push the residual as a new lot.
- Emit one
tradesrow per fully-closed position; one row per still-open position withstatus='open'andexit_avg = NULL. - Compute statistics per-trade —
gross_pnl,net_pnl(gross minus fees),r_multiple(net_pnl / planned_riskifplanned_riskis set on the trade), hold duration, max favorable / adverse excursion.
The roll-up is single-pass per (account_id, symbol) group, so a 100 k-execution book finishes in seconds. Re-running on an unchanged input is a no-op for the trade IDs: stable IDs are derived from the first opening execution.
The Trades view
frontend/js/views/trades.js. Server-side pagination (GET /api/trades?account=&status=&limit=&offset=&sort=), filter chips for asset class / status / R-bucket / tags, and the following per-row context menu (right-click anywhere on a trade row):
- Open journal — jumps to the per-trade journal entry (step 04).
- Open chart with markers — Charts view with entry / exit markers stamped on the bar.
- Set planned risk —
POST /api/trades/{id}/risk. Re-derivesr_multiplefor the row without touching executions. - Split —
POST /api/trades/{id}/split. For trades that should be modeled as two separate ideas (e.g. a position you held overnight and added to the next day). - Merge —
POST /api/trades/merge. Combine adjacent trades on the same symbol with a custom join logic. - Tag — multi-select per-account tag picker. Tags are queryable in reports (step 05).
- Symbol nav — six per-symbol jumps (Charts / Options / Research / Earnings / News / Copy). 222 distinct symbol-nav paths are wired across the app — every grid that emits a symbol surfaces the same menu.
The Executions view
One row per fill. Same paginated grid pattern as Trades but with a sharper filter set: account / symbol / side / asset class / date range / import batch / has-error. Useful for forensics — "did that 9:31 fill at $185.04 really come through?" The Executions view answers without re-deriving anything.
You can also hand-add executions from this view (POST /api/executions) — handy for cash dividends, broker corrections, or backfilling a missing fill the CSV didn't capture. Hand-added rows are flagged with import_id = NULL and survive re-imports.
Bulk operations
Selecting multiple trades unlocks bulk endpoints. The grid uses range-select (Shift+click) and toggle-select (Cmd+click); the bulk action bar shows once anything is selected.
- Bulk tag — adds / removes tags across the selection.
- Bulk set planned risk — same R for all (useful when you sized every trade the same week).
- Bulk delete — only deletes the trade row; executions are preserved, the rollup re-fires, and a fresh trade row appears. Net effect: trade-level metadata (tags, notes, planned-risk) is wiped without touching fills.
- Bulk export — CSV / JSON of the selected trades (column set matches the visible grid).
- Bulk POST —
POST /api/trades/bulktakes a list of partial updates; useful when scripting against the API.
Re-running the roll-up safely
POST /api/trades/rollup with no body re-runs every (account_id, symbol) partition. Optional body narrows the scope:
{
"account_id": 17,
"symbols": ["AAPL", "TSLA"],
"force": false
}
force: true drops the trade rows for the targeted partitions and re-derives from scratch (also wipes per-trade metadata — tags, notes, planned-risk). The default force: false preserves trade IDs and metadata wherever the rolled-up rows match the existing ones; only trades whose execution membership changed are rewritten.
Open positions
Open trades (status='open') have exit_avg = NULL and unrealized P&L computed against the latest quote in quote_snapshots. The Live Positions tile (step 06) shows them in real time; the Trades grid shows them with a different row tint and the equity-curve view marks them with a dashed continuation.