Portfolio API
19 endpoints in this group.
GET /api/portfolio/archive-stats
Archive Stats
Row counts and date range for the snapshot and intraday tables.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id | query | — | optional | |
account_id | query | — | optional | |
account_ids | query | — | optional |
Responses
| Code | Description |
|---|---|
200 | Successful Response |
422 | Validation Error |
POST /api/portfolio/backfill/csv
Backfill From Csv
Deprecated stub — use /api/portfolio/backfill/upload instead.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id | query | — | optional |
Responses
| Code | Description |
|---|---|
200 | Successful Response |
422 | Validation Error |
POST /api/portfolio/backfill/save
Backfill Save
Request body: application/json
Responses
| Code | Description |
|---|---|
201 | Successful Response |
422 | Validation Error |
POST /api/portfolio/backfill/upload
Backfill Upload
Upload a CSV of historical position snapshots and upsert into daily_position_snapshots.
Expected CSV columns (flexible header matching): symbol, date, quantity, avg_cost, market_price, market_value Optional: unrealized_pnl, day_change_pct, source
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
year | query | integer | optional | |
source_type | query | string | optional | |
tenant_id | query | — | optional |
Request body: multipart/form-data
Responses
| Code | Description |
|---|---|
201 | Successful Response |
422 | Validation Error |
DELETE /api/portfolio/data
Delete Year Data
Soft-delete a year's realized_trades + daily_position_snapshots.
Both tables get deleted_at = now() on still-active rows. No row is
physically removed (see migration 0115_soft_delete_realized_dps).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id | query | — | optional | |
year | query | integer | required |
Responses
| Code | Description |
|---|---|
200 | Successful Response |
422 | Validation Error |
POST /api/portfolio/eod
Run Eod Ingestion
Trigger end-of-day broker position fetch + snapshot save.
A successful response can represent either saved positions or a verified empty
broker account. When the broker confirms that an account has no positions, the
response includes reconciled_empty_accounts and
reconciled_empty_positions; this is a successful reconciliation that retires
stale snapshot rows, not a broker failure. If one or more connected brokers
cannot be read or reconciled while the rest of the run continues, the response
includes degraded: true, reason: "portfolio_broker_partial", and
skipped_brokers with the affected broker names. These fields may accompany
the normal snapshot result or the no-positions message.
Storage semantics: successful empty reconciliation persists zero-quantity snapshot markers for the verified account and snapshot date. These markers establish the latest-date boundary; they are not open holdings. Consumers must select the latest boundary including markers, then exclude zero quantities from returned holdings or holding-derived subscription interest. Excluding markers before selecting the latest date can resurrect an older holding. Explicit watchlist interest remains valid even when a holding is closed.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id | query | — | optional |
Responses
| Code | Description |
|---|---|
200 | Successful Response |
422 | Validation Error |
GET /api/portfolio/eod/run-log
Get Eod Run Log
Return recent EOD snapshot runs grouped by snapshot_date.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id | query | — | optional | |
account_id | query | — | optional | |
account_ids | query | — | optional | |
limit | query | integer | optional |
Responses
| Code | Description |
|---|---|
200 | Successful Response |
422 | Validation Error |
POST /api/portfolio/eod/trigger
Run Eod Ingestion
Trigger end-of-day broker position fetch + snapshot save.
This alias has the same response contract as POST /api/portfolio/eod. A
verified empty account can return reconciled_empty_accounts and
reconciled_empty_positions. A partial broker failure returns
degraded: true, reason: "portfolio_broker_partial", and
skipped_brokers while preserving successful work from other brokers.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id | query | — | optional |
Responses
| Code | Description |
|---|---|
200 | Successful Response |
422 | Validation Error |
GET /api/portfolio/fees-summary
Fees Summary
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
days | query | integer | optional | |
account_hash | query | — | optional |
Responses
| Code | Description |
|---|---|
200 | Successful Response |
422 | Validation Error |
GET /api/portfolio/pnl
Get Pnl Series
Daily P&L time series — P0-8: limit capped at 100.
G10 (mobile#150): supports keyset cursor pagination for infinite-scroll.
The series is grouped by snapshot_date and ordered ASC (chronological).
Pass cursor from the previous page's next_cursor to fetch the
next chunk; the server emits next_cursor only when a full page was
returned (len(rows) == limit). Cursor is opaque — a base64url JSON
blob whose payload is {"d": "YYYY-MM-DD"} (date-only, since rows are
grouped by date and have no row-id).
The page dependency provides limit (1..100) + a generic
cursor query param for shape-parity with the blotter endpoint;
we decode it locally rather than via decode_cursor because pnl
keyset is one-column not two.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id | query | — | optional | |
account_id | query | — | optional | |
account_ids | query | — | optional | |
start_date | query | — | optional | |
end_date | query | — | optional | |
limit | query | integer | optional | Page size (max 100). Legacy — prefer ?page_size=. |
page_size | query | — | optional | Canonical page size. Must be one of 25, 50, 100. When supplied, overrides ?limit=. |
cursor | query | — | optional | Opaque pagination cursor from a previous page's next_cursor. |
Responses
| Code | Description |
|---|---|
200 | Successful Response |
422 | Validation Error |
GET /api/portfolio/positions/{symbol}
Get Position By Symbol
Latest daily snapshot row for one symbol.
Returns 404 when the tenant has no snapshot for that symbol so
the mobile client can render an empty-state without parsing. Returns
200 with the same shape as /snapshots (singular snapshot
key + snapshot_date echo) so PositionDetailScreen reuses the
existing dashboard summary-row parser.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
symbol | path | string | required | |
tenant_id | query | — | optional | |
account_id | query | — | optional | |
account_ids | query | — | optional |
Responses
| Code | Description |
|---|---|
200 | Successful Response |
422 | Validation Error |
GET /api/portfolio/realized-from-orders
Get Realized From Orders
Return FIFO realized lots derived from filled local orders.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id | query | — | optional | |
account_id | query | — | optional | |
account_ids | query | — | optional | |
year | query | — | optional | |
symbol | query | — | optional |
Responses
| Code | Description |
|---|---|
200 | Successful Response |
422 | Validation Error |
GET /api/portfolio/realized-kpis
Get Realized Kpis
Day/MTD/YTD realized gain/loss for the live Portfolio KPI strip.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id | query | — | optional | |
account_id | query | — | optional | |
account_ids | query | — | optional | |
source | query | string | optional | broker |
Responses
| Code | Description |
|---|---|
200 | Successful Response |
422 | Validation Error |
GET /api/portfolio/realized-trades
Get Realized Trades
Return realized trades from uploads or connected-broker lot projections.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id | query | — | optional | |
account_id | query | — | optional | |
account_ids | query | — | optional | |
year | query | — | optional | |
symbol | query | — | optional | |
source | query | — | optional | |
limit | query | — | optional | |
page | query | integer | optional | 1-based page index. |
page_size | query | integer | optional | Page size — must be one of 25, 50, 100. |
Responses
| Code | Description |
|---|---|
200 | Successful Response |
422 | Validation Error |
POST /api/portfolio/realized-trades/sync
Sync Realized Trades
Repair Schwab realized-trade rows from already-stored broker data.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
account_id | query | — | optional | |
account_ids | query | — | optional | |
force_rebuild_closed_lots | query | boolean | optional |
Responses
| Code | Description |
|---|---|
200 | Successful Response |
422 | Validation Error |
GET /api/portfolio/snapshots
Get Snapshots
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id | query | — | optional | |
account_id | query | — | optional | |
account_ids | query | — | optional | |
account_number | query | — | optional | |
start_date | query | — | optional | |
end_date | query | — | optional | |
as_of_date | query | — | optional | |
latest_only | query | boolean | optional | |
refresh_live | query | boolean | optional | When true with latest_only, foreground-refresh connected broker positions before reading. |
limit | query | integer | optional |
Responses
| Code | Description |
|---|---|
200 | Successful Response |
422 | Validation Error |
POST /api/portfolio/snapshots
Save Snapshots
Request body: application/json
Responses
| Code | Description |
|---|---|
201 | Successful Response |
422 | Validation Error |
POST /api/portfolio/snapshots/archive
Archive Snapshots
- Move daily_position_snapshots older than keep_snapshot_years to archive table.
- Rollup of old 5-min intraday bars into the shared market_data_daily is skipped (plan slice 1m, ).
- Intraday bars beyond the retention window are retained, not purged.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id | query | — | optional | |
keep_snapshot_years | query | integer | optional | |
keep_intraday_years | query | integer | optional |
Responses
| Code | Description |
|---|---|
200 | Successful Response |
422 | Validation Error |
DELETE /api/portfolio/trades
Delete Trades
Soft-delete realized_trades for the given tenant + year.
Sets deleted_at = now() on already-active rows (idempotent — rows
that are already soft-deleted are skipped). Returns the number of rows
that transitioned to soft-deleted in this call.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id | query | — | optional | |
year | query | integer | required |
Responses
| Code | Description |
|---|---|
200 | Successful Response |
422 | Validation Error |