Skip to content

Client

msm.client lets an application use Markets registries through the Markets API instead of MetaTables (ADR 0047, decision 2). The application keeps the same msm.api row classes and calls; only the backend changes:

Backend Who uses it What a row call does
runtime (default) the Markets deployment, its Jobs, and code that owns Markets tables compiled SQL through the Markets runtime
remote every other application one call to the Markets API, which runs the same method on its runtime

Portfolio.get_by_uid(uid), Asset.upsert(...) or CurrencySpot.upsert(...) are the same calls under both.

Selecting The Remote Backend

from msm.api.remote import use_remote_backend
from msm.client import MarketsClient

use_remote_backend(MarketsClient.from_environment().backend)

or use_remote_backend(LazyRemoteBackend()), which finds the Markets API on the first row call so a process that never touches a Markets registry never needs to see it, or set MSM_API_BACKEND=remote (lazy as well) before the first row call. The choice is process-wide and applies only to row classes whose table lives in the Markets namespace. A provider's own row classes, over tables in the provider's namespace, keep the runtime backend, so one provider process writes its own tables with MetaTables and Markets registries through the API.

Under remote, start_engine() on a Markets row class attaches nothing. Remove msm.start_engine(models=[...]) calls for Markets tables from such an application; it holds no grant on mainsequence.markets. When it attaches its own tables, a Markets table that is only a foreign-key target of one of them is referenced by physical name and not attached; a provider's own row classes should list only its own tables in __required_tables__.

Finding The Markets API

MarketsClient.from_environment() finds the deployment the way metatables.endpoint finds MetaTables: the FastAPI release named api (from source_path: api/main.py) of the MainSequenceMarkets CodeRepository in the caller's Environment, authenticated with the release token that ResourceRelease.resolve_runtime_access returns. A 401 refreshes the token once. The calling workload sees the release when its workflow grants it reader on that repository's branch:

access:
  branches:
    - repository: MainSequenceMarkets
      policy: reader
Variable Effect
MARKETS_API_URL Call this URL instead of discovering one (local development).
MARKETS_API_TOKEN Bearer token for MARKETS_API_URL; without it the caller's platform session is sent.
MARKETS_API_RELEASE_UID Use this release instead of searching.
MARKETS_API_RELEASE_NAME, MARKETS_API_REPOSITORY_NAME Search for another release or repository name.

Writes

Every create, upsert, update and delete runs through the Markets API's registry_write, which records the acting identity on the API's audit log. Any workload the platform admits to the Markets API may write; a refused operation (403) raises msm.api.remote.MarketsAccessError, a PermissionError.

A table row class's remote delete runs the resource's service delete, with its preflight checks, and exists only for asset, asset-category, calendar, index, portfolio and portfolio-group; other table row classes raise MarketsOperationUnsupported. Composites that implement delete themselves, such as Curve and the pricing market-data bindings, delete remotely as they do under the runtime backend.

The remote backend covers the msm.api rows, the msm_portfolios.api metadata rows (SignalMetadata, PortfolioMetadata, RebalanceStrategyMetadata) and the msm_pricing.api composites (curves, curve building details, index conventions, market-data sets and bindings, asset pricing details).

Errors

Markets API status Raised by the row call
400, 422 ValueError (or TypeError for wrong arguments)
401, 403 MarketsAccessError (PermissionError)
404 LookupError; gets return None as under the runtime backend
409 MarketsConflictError (ValueError)
501 MarketsOperationUnsupported (NotImplementedError)

Code that catches ValueError or LookupError behaves the same under both backends.

Set-Based Operations

Every table row class offers the same set-based calls under both backends, so a provider never loops over rows through the API:

Asset.upsert_many([{"unique_identifier": "ACME", "asset_type": "equity"}, ...])
AccountHoldingsSet.filter(account_uid=uid, order_by="-time_index", limit=50, offset=0)
AccountHoldingsSet.count(account_uid=uid)
Asset.filter(unique_identifier__in=["A", "B"])
CalendarSession.filter(calendar_uid=uid, local_date__gte=start, local_date__lte=end)

upsert_many validates each payload through the row module's <Row>Upsert model and writes them in one statement on __upsert_keys__. filter takes equality, <field>_contains, <field>__in and <field>__gte/__gt/__lte/ __lt filters, order_by (-column for descending), limit and offset; count takes the same filters. Remote values are typed by the row model's field, so a date bound arrives as a date.

The typed asset composites batch as well: CurrencySpot.upsert_many and Future.upsert_many write the assets and their details in one statement each, and CurrencySpot.filter(asset_uids=, base_currency_uids=, quote_currency_uids=) and Future.filter(asset_uids=, underlying_index_uids=, kinds=) read them with their details.

Holdings And Asset Snapshots

Account holdings and asset snapshots are Markets tables that applications publish into rather than write:

from msm.api.accounts import AccountHoldingsSnapshots
from msm.api.assets import AssetSnapshots

AccountHoldingsSnapshots.publish(
    account_uid=account_uid,
    holdings_date=as_of,
    positions=[{"asset_identifier": "ACME", "quantity": "10", "extra_details": {"source": "feed"}}],
    overwrite=True,
)
latest = AccountHoldingsSnapshots.get(account_uid=account_uid)
history = AccountHoldingsSnapshots.get_many(account_uid=account_uid, limit=10)
AssetSnapshots.publish([{"asset_identifier": "ACME", "time_index": as_of, "name": "Acme"}])
current = AssetSnapshots.latest(tickers=["ACME"])

AssetSnapshots.latest takes exactly one of asset_identifiers, asset_uids or tickers and returns each existing asset with its latest snapshot (AssetSnapshotReference: name, ticker, exchange code, snapshot time; None without a snapshot). tickers selects the assets that ever had one of those tickers; compare the latest ticker to keep the ones that still carry it.

Provider Time Series

Time series a provider produces live in its own namespace (ADR 0047). Besides the portfolio contracts, msm.storage_instances.configured_time_index_instance builds an instance of any Markets time-index storage (discount curves, index fixings, pricing details, index values, orders, trades ...) with the core storage's columns, foreign keys and grain index, under the provider's namespace, storage_app and Alembic base; the provider binds its updater with output_table=. Markets finds the instance through the catalog of the series: pricing market-data bindings carry its storage_table_uid, the Index dataset catalog its table uid, the portfolio catalog its values, weights and signal tables (PortfolioDatasets.register; register_signal for a signal that is not a portfolio's current one). Rows a series already has in a core table move with msm.storage_copies.copy_time_index_rows, which writes only the rows the instance lacks and verifies every key; TableSeriesMove, PortfolioSeriesMove and SignalSeriesMove add the catalog steps and the rollback (msm series move|rollback). PortfolioDatasets.unregister removes a portfolio's catalog row to roll a move back; its reads return to the core tables and the instance keeps its rows. unregister_signal undoes register_signal for one signal.

Portfolio Time Series

Time series are not row calls. Read portfolio values and weights from the catalogued instance tables through the Markets API:

client = MarketsClient.from_environment()
values = client.portfolio_values([portfolio_uid], start_date=start, as_pandas=True)
weights = client.portfolio_weights([portfolio_uid], as_pandas=True)

Both return the core.tabular_frame@v1 frame, or a pandas DataFrame with as_pandas=True. One values request returns at most 5,000 rows; a larger limit is read in pages with offset and returned as one frame.

Persisted calendar schedules (PersistedCalendarSchedule, used by calendar-relative rebalancing) read CalendarSession.filter, so they run under either backend.

How It Works

Each public class method of a Markets row class is wrapped when the class is defined (msm.api.remote.install_remote_dispatch). Under runtime the original method runs unchanged. Under remote the wrapper sends the bound arguments to POST /api/v1/rows/{resource}/{operation}/; the Markets API runs the same method on its runtime backend and returns the result, which the client validates into the method's return type. GET /api/v1/rows/ lists every resource and operation. Because the remote operations are the row classes' own methods, the two backends cannot drift apart.