Skip to content

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.

Runtime Sources

  • Portfolio identity uses msm.api.portfolios.Portfolio.
  • Optional descriptive metadata uses msm_portfolios.api.portfolios.PortfolioMetadata.
  • Latest weights use msm_portfolios.data_nodes.portfolios.storage.PortfolioWeightsStorage.
  • Weight row asset labels use AssetSnapshotsStorage; OpenFIGI is not used for portfolio weight labels.

Latest weights resolve through:

Portfolio.uid
  -> Portfolio.portfolio_index_uid
  -> Index.uid
  -> Index.unique_identifier
  -> PortfolioWeightsStorage.portfolio_index_identifier

If a portfolio has no portfolio_index_uid, the weights endpoint returns 200 with an empty weights list and a resolution_warning.

List Portfolios

GET /api/v1/portfolio/?response_format=frontend_list&search=&calendar_uid=&calendar_name=&limit=50&offset=0

Returns PaginatedResponse[Portfolio]:

{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "uid": "portfolio-uid",
      "unique_identifier": "example-sleeve",
      "calendar_name": "CRYPTO_24_7",
      "calendar_uid": null,
      "portfolio_index_uid": "index-uid",
      "portfolio_weights_data_node_uid": null,
      "signal_weights_data_node_uid": null,
      "portfolio_data_node_uid": null,
      "backtest_table_price_column_name": "close"
    }
  ]
}

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_name": "CRYPTO_24_7",
    "calendar_uid": null,
    "portfolio_index_uid": "index-uid",
    "portfolio_weights_data_node_uid": null,
    "signal_weights_data_node_uid": null,
    "portfolio_data_node_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.

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",
  "portfolio_index_uid": "index-uid",
  "portfolio_index_identifier": "example-sleeve-index",
  "weights_date": "2026-06-07T10:30:00Z",
  "resolution_warning": null,
  "weights": [
    {
      "time_index": "2026-06-07T10:30:00Z",
      "portfolio_index_identifier": "example-sleeve-index",
      "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",
  "portfolio_index_uid": "index-uid",
  "portfolio_index_identifier": "example-sleeve-index",
  "weights_date": null,
  "resolution_warning": null,
  "weights": []
}

Delete Portfolio

DELETE /api/v1/portfolio/{uid}/

Returns:

{
  "detail": "Portfolio deleted.",
  "deleted_count": 1
}

If protected rows, such as account target-position history, reference the portfolio, the route returns 409 and does not delete the portfolio.

The delete route does not delete historical PortfolioWeightsStorage, PortfoliosStorage, or metadata rows.

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,
  "failed": [
    {
      "uid": "protected-portfolio-uid",
      "reason": "Portfolio is referenced by target positions."
    }
  ]
}