Portfolio Routes
The apps/v1 portfolio routes expose portfolio identity rows, detail-page
composition, latest portfolio weights, and delete operations.
The routes do not create or update portfolios. Portfolio construction remains a library workflow outside this API surface. ADR 0047 adds the multi-portfolio frames, the batch lookup and the dataset catalog routes described below.
Pre-3.0 field names
Through ms-markets 3.x every response that carries a *_table_update_uid portfolio pointer also
repeats it under its pre-3.0 *_data_node_uid name, which OpenAPI marks
deprecated, and requests accept either name. ms-markets 4.0 removes the
pre-3.0 names (ADR 0048).
Runtime Sources
- Portfolio identity uses
msm.api.portfolios.Portfolio. - Optional descriptive metadata uses
msm_portfolios.api.portfolios.PortfolioMetadata. - Latest weights use
msm_portfolios.time_index.portfolios.storage.PortfolioWeightsStorage. - Weight row asset labels use
AssetSnapshotsStorage; OpenFIGI is not used for portfolio weight labels. - Signal weights resolve from nullable
Portfolio.signal_uid, which points toSignalMetadataTable.signal_uid. The API does not recompute signal identity from the TimeIndexTableUpdater build configuration or by scanning shared signal storage. - Values, weights and signal-weight reads resolve each portfolio's table from
the Markets catalog (
PortfolioDatasetAvailabilityTable) and read it by uid throughTimeIndexTableRef; a portfolio without a catalog row reads the core tables (ADR 0047, see the portfolios knowledge page).
Latest weights resolve through:
Portfolio.uid
-> Portfolio.unique_identifier
-> PortfolioWeightsStorage.portfolio_identifier
The optional published_index_uid field is not used for latest-weight
resolution. It is publication metadata only.
List Portfolios
GET /api/v1/portfolio/?search=&calendar_uid=&limit=50&offset=0
Portfolio rows always include a non-null calendar_uid that references a
persisted CalendarTable row.
Returns command-center.resource_collection@v1 and exposes
GET /api/v1/portfolio/discovery/:
{
"items": [
{
"uid": "portfolio-uid",
"unique_identifier": "example-sleeve",
"calendar_uid": "00000000-0000-0000-0000-000000000001",
"published_index_uid": "index-uid",
"portfolio_weights_table_update_uid": null,
"signal_weights_table_update_uid": null,
"signal_uid": null,
"portfolio_table_update_uid": null,
"backtest_table_price_column_name": "close"
}
],
"pageInfo": {
"pageIndex": 0,
"pageSize": 50,
"totalItems": 1,
"hasNextPage": false,
"hasPreviousPage": false
}
}
Portfolio Detail
GET /api/v1/portfolio/{uid}/
Returns the core portfolio row, optional metadata, detail-page tabs, and route links:
{
"portfolio": {
"uid": "portfolio-uid",
"unique_identifier": "example-sleeve",
"calendar_uid": "00000000-0000-0000-0000-000000000001",
"published_index_uid": "index-uid",
"portfolio_weights_table_update_uid": null,
"signal_weights_table_update_uid": null,
"signal_uid": null,
"portfolio_table_update_uid": null,
"backtest_table_price_column_name": "close"
},
"metadata": {
"uid": "metadata-uid",
"unique_identifier": "example-sleeve",
"description": "Example sleeve portfolio."
},
"tabs": [
{
"key": "latest_weights",
"label": "Latest Weights",
"url": "/api/v1/portfolio/portfolio-uid/weights/?order=desc&limit=1&include_asset_detail=true"
}
],
"links": {
"summary": "/api/v1/portfolio/portfolio-uid/summary/",
"latest_weights": "/api/v1/portfolio/portfolio-uid/weights/",
"delete": "/api/v1/portfolio/portfolio-uid/"
}
}
Missing metadata does not make the detail route return 404. Only a missing portfolio row returns 404.
Portfolio Summary
GET /api/v1/portfolio/{uid}/summary/
Returns the shared FrontEndDetailSummary contract. The summary entity.id
is the portfolio uid string.
The summary includes PortfolioTable pointer fields as inline code fields.
Summary badges, inline fields, highlight fields, and stats can carry a
nullable link_url field. The response does not use a separate summary
links array; frontend navigation belongs on the actual row being rendered.
{
"key": "calendar_uid",
"label": "Calendar UID",
"value": "calendar-uid",
"kind": "code",
"icon": null,
"link_url": "/api/v1/calendar/calendar-uid/"
}
Portfolio summaries attach link_url to the calendar field, published-index
badge/field, portfolio-weights node field, signal-weights node field, and
portfolio-values node field when the backing UID exists. extensions keeps
page-specific resolved metadata for consumers that need more than a field URL.
{
"badges": [
{
"key": "published_index",
"label": "Published Index",
"tone": "info",
"link_url": "/api/v1/index/index-uid/"
}
],
"inline_fields": [
{
"key": "calendar_uid",
"label": "Calendar UID",
"value": "calendar-uid",
"kind": "code",
"icon": null,
"link_url": "/api/v1/calendar/calendar-uid/"
},
{
"key": "portfolio_weights_table_update_uid",
"label": "Portfolio Weights Node",
"value": "weights-node-uid",
"kind": "code",
"icon": null,
"link_url": "/api/v1/portfolio/portfolio-uid/weights/?order=desc&limit=1&include_asset_detail=true"
},
{
"key": "signal_weights_table_update_uid",
"label": "Signal Weights Node",
"value": "signal-node-uid",
"kind": "code",
"icon": null,
"link_url": "/api/v1/portfolio/portfolio-uid/signals_weights/?order=desc&limit=100"
},
{
"key": "portfolio_table_update_uid",
"label": "Portfolio Values Node",
"value": "values-node-uid",
"kind": "code",
"icon": null,
"link_url": "/api/v1/portfolio/portfolio-uid/portfolio_values/?order=desc&limit=100"
}
],
"highlight_fields": [
{
"key": "calendar",
"label": "Calendar",
"value": "Example Calendar",
"kind": "text",
"icon": "calendar",
"link_url": "/api/v1/calendar/calendar-uid/"
}
],
"extensions": {
"calendar": {
"uid": "calendar-uid",
"label": "Example Calendar",
"display_name": "Example Calendar",
"unique_identifier": "EXAMPLE_CALENDAR",
"detail_url": "/api/v1/calendar/calendar-uid/",
"dates_url": "/api/v1/calendar/calendar-uid/dates/",
"sessions_url": "/api/v1/calendar/calendar-uid/sessions/",
"events_url": "/api/v1/calendar/calendar-uid/events/"
},
"nodes": {
"portfolio_weights": {
"uid": "weights-node-uid",
"label": "Portfolio weights",
"url": "/api/v1/portfolio/portfolio-uid/weights/?order=desc&limit=1&include_asset_detail=true"
},
"signal_weights": {
"uid": "signal-node-uid",
"signal_uid": "canonical-signal-uid",
"label": "Signal weights",
"url": "/api/v1/portfolio/portfolio-uid/signals_weights/?order=desc&limit=100"
},
"portfolio_values": {
"uid": "values-node-uid",
"label": "Portfolio values",
"url": "/api/v1/portfolio/portfolio-uid/portfolio_values/?order=desc&limit=100"
}
},
"pointers": {
"portfolio_weights_table_update_uid": "weights-node-uid",
"signal_weights_table_update_uid": "signal-node-uid",
"signal_uid": "canonical-signal-uid",
"portfolio_table_update_uid": "values-node-uid"
}
}
}
signal_uid is nullable for portfolios created before a signal workflow has run.
When present, it is the canonical SignalMetadataTable.signal_uid used by
GET /api/v1/portfolio/{uid}/signals_weights/. The signal-weights endpoint
does not infer the signal by scanning shared signal-weight storage.
Batch Lookup By Uid
GET /api/v1/portfolio/by-uids/?uid__in=<uid>&uid__in=<uid>
Returns the matching Portfolio rows in request order inside a
ResourceCollection. uid__in is repeated or comma-separated, at most 500;
unknown uids are omitted. A bounded lookup, not a browsable collection: no
limit, offset, search or discovery route. include=<sidecar MetaTable uid>
attaches provider sidecar rows under includes[<uid>], as described for
accounts.
Values Frame For Several Portfolios
GET /api/v1/portfolio/values/?portfolio_uid__in=<uid>&portfolio_uid__in=<uid>&start_date=&end_date=&order=desc&limit=500&offset=0
Returns one core.tabular_frame@v1 with columns time_index,
portfolio_identifier, portfolio_uid, close, return, calculated_close
and close_time for every requested portfolio. The portfolios are grouped by
the PortfoliosTS instance the catalog records for them; each instance is read
once, so comparing portfolios computed by two different providers is one call
whose two sources live in two namespaces. order sorts by
time_index across all portfolios and limit (1 to 5000) bounds the merged
result; offset skips that many rows of the merged order, so a longer window
is read in pages (MarketsClient.portfolio_values pages for a larger
limit). Every uid must be a portfolio (404 otherwise); source.context
names the requested uids and the window.
Weights Frame For Several Portfolios
GET /api/v1/portfolio/weights/?portfolio_uid__in=<uid>&weights_date=&as_of=true
Returns one core.tabular_frame@v1 with one weight snapshot per portfolio:
the latest snapshot, the latest at or before weights_date with as_of=true,
or the exact weights_date snapshot with as_of=false. Columns are
time_index, portfolio_identifier, portfolio_uid, asset_identifier,
weight, weight_before, price_current, price_before, volume_current
and volume_before. Portfolios are grouped by their PortfolioWeightsTS
instance as for values.
Dataset Catalog
POST /api/v1/portfolio/{uid}/datasets/
GET /api/v1/portfolio/{uid}/datasets/
POST /api/v1/portfolio/datasets/reconcile/
A provider that computes a portfolio in its own namespace registers, per
portfolio, the PortfoliosTS, PortfolioWeightsTS and SignalWeightsTS
instances that hold its rows, and reconciles after each run (ADR 0047).
POST /api/v1/portfolio/{uid}/datasets/takesvalues_meta_table_uid(required),weights_meta_table_uid,signal_meta_table_uidandreconcile(defaulttrue). Each table must be aTimeIndexMetaTablevisible to the Markets workload that exposes the contract columns and index names; extra provider columns are allowed. The row is upserted, the signal'sSignalMetadata.meta_table_uidis bound when the portfolio has asignal_uid, and the values instance is reconciled unlessreconcileisfalse, in which case the row isunavailablewith error codenot_reconciled. Returns thePortfolioDatasetStatewith201;404for an unknown portfolio or a table that is not visible,400for a table that does not satisfy its contract.GET /api/v1/portfolio/{uid}/datasets/returns the catalog row, ornullwhen the portfolio exists and reads the core tables;404for an unknown portfolio.POST /api/v1/portfolio/datasets/reconcile/takesportfolio_uidsandportfolio_identifiers(at least one selector, at most 500 in all) and returns aPortfolioDatasetReconciliationResult: the reconciled states, one aggregate per instance however many portfolios it holds, plusskipped_portfolio_uidsfor portfolios without a catalog row.404names selectors that are not portfolios.
PortfolioDatasetState carries portfolio_uid, portfolio_identifier, the
three table uids, population_state (populated, compatible_empty or
unavailable), row_count, earliest_time_index, latest_time_index,
reconciled_at and error. Registration and reconciliation record the acting
identity.
Latest Portfolio Weights
GET /api/v1/portfolio/{uid}/weights/?order=desc&limit=1&include_asset_detail=true
Returns one PortfolioWeightsSnapshotResponse:
{
"portfolio_uid": "portfolio-uid",
"portfolio_unique_identifier": "example-sleeve",
"published_index_uid": "index-uid",
"portfolio_identifier": "example-sleeve",
"weights_date": "2026-06-07T10:30:00Z",
"resolution_warning": null,
"weights": [
{
"time_index": "2026-06-07T10:30:00Z",
"portfolio_identifier": "example-sleeve",
"asset_identifier": "example-asset-btc",
"weight": "0.600000000000000000",
"weight_before": "0.550000000000000000",
"price_current": "100.0",
"price_before": "95.0",
"volume_current": null,
"volume_before": null,
"asset": {
"uid": "asset-uid",
"unique_identifier": "example-asset-btc",
"current_snapshot": {
"name": "Bitcoin",
"ticker": "BTC"
}
}
}
]
}
Portfolio Weights By Date
GET /api/v1/portfolio/{uid}/weights/?weights_date=2026-06-07T10:30:00Z&include_asset_detail=true
weights_date selects the exact PortfolioWeightsStorage.time_index
snapshot and takes precedence over order.
If the portfolio exists but no rows exist for the requested timestamp, the
response is 200 with an empty weights list:
{
"portfolio_uid": "portfolio-uid",
"portfolio_unique_identifier": "example-sleeve",
"published_index_uid": "index-uid",
"portfolio_identifier": "example-sleeve",
"weights_date": null,
"resolution_warning": null,
"weights": []
}
Delete Portfolio
DELETE /api/v1/portfolio/{uid}/
Deletes the portfolio identity row plus historical PortfolioWeightsStorage
and PortfoliosStorage rows resolved through:
Portfolio.uid
-> Portfolio.unique_identifier
-> PortfolioWeightsStorage.portfolio_identifier
-> PortfoliosStorage.portfolio_identifier
Returns:
{
"detail": "Portfolio deleted.",
"deleted_count": 1,
"deleted_weights_count": 4,
"deleted_values_count": 4,
"retained_datasets": []
}
If protected rows, such as virtual funds or account target-position history, reference the portfolio, the route returns 409 and does not delete the portfolio.
When the Markets catalog (PortfolioDatasetAvailabilityTable, ADR 0047) places
the portfolio's values or weights in a contract instance another application
owns, Markets does not delete that table's rows: only the owning application
writes or deletes them. The route still deletes the portfolio row, its catalog
row and any rows left in the core tables, and lists each table it left in
place:
{
"detail": "Portfolio deleted. Rows in 1 table another application owns were left in place; see retained_datasets.",
"deleted_count": 1,
"deleted_weights_count": 0,
"deleted_values_count": 0,
"retained_datasets": [
{
"portfolio_uid": "portfolio-uid",
"portfolio_identifier": "example-sleeve",
"dataset": "values",
"meta_table_uid": "provider-values-table-uid"
}
]
}
The deleted counts cover the core tables only. A catalog row that names the
core table itself counts as core. POST /api/v1/portfolio/bulk-delete/preflight/
reports each retained table as a warning before the delete, and the bulk and
cascade deletes return the same retained_datasets list.
Storage cleanup uses each storage table's
TimeIndexMetaTable.delete_after_date(None, dimension_filters=...) API, scoped
by Portfolio.unique_identifier. The delete route does not delete
PortfolioMetadataTable rows.
Delete Portfolio Weights
DELETE /api/v1/portfolio/{uid}/weights/?weights_date=2026-06-07T10:30:00Z
Deletes PortfolioWeightsStorage rows for the portfolio identifier through the
storage table's TimeIndexMetaTable.delete_after_date(...) API. If
weights_date is omitted, all weight rows for that portfolio identifier are
deleted by calling delete_after_date(None, dimension_filters=...) with the
portfolio scope. If weights_date is provided, rows at or after that timestamp
are deleted.
Returns:
{
"detail": "Portfolio weights deleted.",
"portfolio_uid": "portfolio-uid",
"portfolio_identifier": "example-sleeve",
"weights_date": "2026-06-07T10:30:00Z",
"deleted_count": 4
}
Weight deletion itself is scoped by Portfolio.unique_identifier; the API never
calls delete_after_date(None) without that dimension scope. Protected
portfolio references only matter for deleting the portfolio identity row.
When the catalog places the portfolio's weights in a PortfolioWeightsTS
instance another application owns, the route returns 409, deletes nothing and
names the table. Delete those rows through the application that owns the table.
Bulk Delete Portfolios
POST /api/v1/portfolio/bulk-delete/
Request:
{
"uids": ["portfolio-uid-1", "portfolio-uid-2"]
}
Response:
{
"detail": "Deleted 1 portfolio; 1 portfolio could not be deleted.",
"deleted_count": 1,
"deleted_weights_count": 4,
"failed": [
{
"uid": "protected-portfolio-uid",
"reason": "Portfolio is referenced by target positions."
}
]
}