Skip to content

Market Workflows

These examples show the intended runtime boundary.

  1. Use typed msm.api row APIs for market records.
  2. Initialize the required MetaTable runtime explicitly during process startup.
  3. Treat row operations as business operations only; they do not register or attach MetaTables on first use.
  4. Use DataNode helpers for historical tables such as holdings and target positions.

Runtime Setup

Production code normally initializes the required market MetaTables during application startup. A row call such as Asset.upsert(...) uses the active runtime and then runs the operation. If the runtime is missing, or if the runtime was initialized without the required table set, the error tells the caller to run explicit preflight.

Explicit preflight remains available for applications that want startup-time attachment or verification:

import msm

runtime = msm.start_engine(models=["Asset"])

Runtime attachment does not take labels. Use runtime.meta_tables and runtime.meta_table_models after bootstrap when a specific returned MetaTable needs follow-up labeling or handling. Import DataNode classes from their owning package modules. The preflight uses the Main Sequence logger at info level to report each MetaTable model being attached, what context was created, and when a cached runtime is reused.

For externally managed tables, create/migrate the tables through the admin migration flow and use the explicit startup/bootstrap path with management_mode="external_registered". Do not call model .register() methods or local registration helpers from application code.

Examples that use the platform namespace mainsequence.examples set MSM_AUTO_REGISTER_NAMESPACE before importing msm.api, then call msm.start_engine(...) before row operations:

import os

from examples.platform.bootstrap import (
    EXAMPLE_NAMESPACE_ENV,
    EXAMPLE_METATABLE_NAMESPACE,
)

os.environ.setdefault(EXAMPLE_NAMESPACE_ENV, EXAMPLE_METATABLE_NAMESPACE)

import msm

msm.start_engine(models=["Asset"])

The returned runtime from explicit preflight still exposes table handles and context for lower-level repository/service internals, but examples should not pass those handles around for normal row CRUD.

Assets And Categories

Start with assets before building reusable universes. Asset identity is the stable contract consumed by holdings, target positions, pricing details, and portfolio workflows.

from msm.api.assets import Asset
from msm.constants import ASSET_TYPE_CRYPTO

btc = Asset.upsert(
    unique_identifier="example-asset-btc",
    asset_type=ASSET_TYPE_CRYPTO,
)

btc_by_identifier = Asset.get_by_unique_identifier(
    unique_identifier="example-asset-btc",
)
btc_by_uid = Asset.get_by_uid(btc.uid)
crypto_assets = Asset.filter(
    unique_identifier_contains="example-asset-",
    asset_type=ASSET_TYPE_CRYPTO,
    limit=20,
)
# Optional cleanup for temporary custom assets only:
# Asset.delete(btc.uid)

Do not delete assets during normal setup. Use cleanup only for temporary or organization-owned custom assets. Shared public/mastered assets should remain stable so downstream workflows do not lose their canonical identity.

See examples/msm/assets/asset_crud_workflow.py for the focused workflow. It uses explicit example-namespace bootstrap for the required AssetType, Asset, and OpenFigiDetails MetaTables, creates temporary custom assets, resolves BBG00FNFPQH4 through OpenFIGI, writes an AssetSnapshot frame from the returned provider details, and lists the created assets. Set the Main Sequence secret OPEN_FIGI_API_KEY in www.main-sequence.app/app/main_sequence_workbench/secrets before running the workflow. Cleanup is disabled by default.

The local FastAPI surface under apps/v1 now exposes the migrated asset and asset-category registry routes. Keep the route layer thin and put reusable catalog/category logic under src/msm/services. The implemented category detail flow uses GET /api/v1/asset-category/{uid}/ plus the nested asset list route GET /api/v1/asset/?categories__uid=<uid>. The same local API surface now also exposes the simple index registry routes:

  • GET /api/v1/index/
  • GET /api/v1/index/{uid}/
  • DELETE /api/v1/index/{uid}/

When MSM_AUTO_REGISTER_NAMESPACE is set for this local API, startup now pre-registers the full apps/v1 table set against the real project/session data source already configured for the Main Sequence client. If the session cannot resolve a valid DynamicTable data source, startup should fail and that platform/data-source issue should be fixed directly.

See FastAPI v1 for the current route inventory and contract notes.

Currency Assets

Single currencies and currency spot pairs are separate assets. Create or resolve the component currency assets first, then let CurrencySpot.upsert(...) own the spot-pair asset and CurrencySpotAssetDetailsTable write:

from msm.api.assets import Asset, CurrencySpot
from msm.constants import ASSET_TYPE_CURRENCY

USD = {"code": "USD", "currency_name": "US Dollar"}
EUR = {"code": "EUR", "currency_name": "Euro"}

usd = Asset.upsert(unique_identifier=USD["code"], asset_type=ASSET_TYPE_CURRENCY)
eur = Asset.upsert(unique_identifier=EUR["code"], asset_type=ASSET_TYPE_CURRENCY)

eur_usd = CurrencySpot.upsert(
    unique_identifier="BBG0013HGRV5",
    base_currency_uid=eur.uid,
    quote_currency_uid=usd.uid,
)

Typed asset APIs normalize asset type keys before writing them, so "Currency" is stored as currency, "Currency Spot" is stored as currency_spot, and "Future" is stored as future.

See examples/msm/assets/currency_spot_workflow.py and Currency Assets for the detailed schema and workflow.

Bond Assets

Bond setup uses reference issuers plus canonical asset rows. Create the issuer and denomination currency first, then let Bond.upsert(...) own the bond asset and BondAssetDetailsTable write:

import datetime as dt

from msm.api.assets import Asset, AssetType, Bond
from msm.api.issuers import Issuer
from msm.constants import (
    ASSET_TYPE_BOND_DEFINITION,
    ASSET_TYPE_CURRENCY,
    ASSET_TYPE_CURRENCY_DEFINITION,
)

AssetType.upsert(**ASSET_TYPE_CURRENCY_DEFINITION.as_payload())
AssetType.upsert(**ASSET_TYPE_BOND_DEFINITION.as_payload())

issuer = Issuer.upsert(
    unique_identifier="example-issuer",
    display_name="Example Issuer",
)
usd = Asset.upsert(unique_identifier="USD", asset_type=ASSET_TYPE_CURRENCY)

bond = Bond.upsert(
    unique_identifier="example-usd-bond-2031",
    issuer_uid=issuer.uid,
    currency_asset_uid=usd.uid,
    issue_date=dt.date(2026, 5, 27),
    maturity_date=dt.date(2031, 5, 27),
    status="ACTIVE",
)

See examples/msm/assets/bond_workflow.py and Bond Assets for the detailed schema and workflow. examples/msm/assets/us_treasury_bond_workflow.py shows the same API on a US Treasury note where CUSIP maps to canonical asset identity, FIGI maps to provider details, and coupon/tenor fields stay outside the minimal bond detail table.

Asset Snapshots

AssetSnapshot is a DataNode, not a MetaTable. Build validated snapshot frames or a configured node through AssetSnapshot methods. Construct the node first, then bind rows whose payloads each carry their own time_index:

from datetime import datetime, UTC

from msm.data_nodes.assets import AssetSnapshot

snapshot_node = AssetSnapshot().set_snapshots(
    {
        "time_index": datetime.now(UTC),
        "asset_identifier": "example-asset-btc",
        "ticker": "BTC",
    },
)
snapshot_frame = snapshot_node.run(debug_mode=True, force_update=True)

Markets DataNodes use the same identifier rule as MetaTables. With the default markets namespace, logical identifiers stay bare, such as Asset and AssetSnapshotsTS. With MSM_AUTO_REGISTER_NAMESPACE=mainsequence.examples, the published identifiers become mainsequence.examples.Asset and mainsequence.examples.AssetSnapshotsTS; the default DataNode hash_namespace is also mainsequence.examples. Pass explicit identifier or hash_namespace only when a test or experiment needs isolation.

Asset snapshot source tables have a canonical foreign key from asset_identifier to AssetTable.unique_identifier, so the Asset MetaTable must exist before a snapshot DataNode initializes its source table. In examples that perform source-table initialization, run msm.start_engine(models=["Asset"]) with the intended namespace before creating the DataNode.

Before the write path persists rows, AssetSnapshot checks the backend for the incoming (time_index, asset_identifier) tuples and fails if any tuple already exists. Publish corrections as a new timestamped snapshot instead of overwriting the existing row.

See examples/msm/assets/asset_crud_workflow.py for the workflow that also writes an AssetSnapshot frame. See Asset-Indexed DataNodes for the detailed AssetIndexedDataNode contract and how AssetSnapshot implements it.

When the universe itself should be a reusable platform object, create an asset category and manage memberships separately:

from msm.api.assets import Asset, AssetCategory, AssetType
from msm.constants import ASSET_TYPE_CRYPTO, ASSET_TYPE_CRYPTO_DEFINITION

AssetType.upsert(**ASSET_TYPE_CRYPTO_DEFINITION.as_payload())

btc = Asset.upsert(unique_identifier="BTC", asset_type=ASSET_TYPE_CRYPTO)
eth = Asset.upsert(unique_identifier="ETH", asset_type=ASSET_TYPE_CRYPTO)
category = AssetCategory.upsert(
    unique_identifier="crypto-majors",
    display_name="Crypto Majors",
)
memberships = AssetCategory.replace_memberships(
    category_uid=category.uid,
    asset_uids=[btc.uid, eth.uid],
)

For a step-by-step membership lifecycle, run examples/msm/assets/asset_category_workflow.py. It creates a category, adds assets, removes assets, and prints the category contents after each change. The normal run leaves assets in the category unless the cleanup flag is used. Asset examples reuse shared identifiers and FIGI constants from examples/msm/assets/utils/reference_data.py.

Accounts, Virtual Funds, And Portfolios

import msm

from msm.api.accounts import Account
from msm.api.calendars import Calendar
from msm.api.portfolios import Portfolio
from msm.api.virtual_funds import VirtualFund

msm.start_engine(
    models=["Account", "Calendar", "CalendarDate", "CalendarSession", "Portfolio", "VirtualFund"]
)

account = Account.upsert(
    unique_identifier="acct-main",
    account_name="Main Account",
)
calendar = Calendar.create_from_pandas_calendar(
    source_identifier="24/7",
    unique_identifier="CRYPTO_24_7",
    display_name="Crypto 24/7",
    valid_from="2026-05-25",
    valid_to="2026-05-25",
    timezone="UTC",
)
portfolio = Portfolio.upsert(
    unique_identifier="btc-eth-target",
    calendar_uid=calendar.uid,
    calendar_name=calendar.unique_identifier,
)
virtual_fund = VirtualFund.upsert(
    unique_identifier="vf-core",
    account_uid=account.uid,
    target_portfolio_uid=portfolio.uid,
)

Holdings And Target Positions

Run the portfolio workflow in two stages:

python examples/msm_portfolios/portfolio_equal_weights_prepare_schema.py
python examples/msm_portfolios/portfolio_equal_weights_run.py

The preparation script derives the configured interpolated price storage from the registered ExternalPricesStorage table and the example interpolation policy, finds or generates the real dynamic Alembic revision under the active migration namespace, and runs the dynamic provider upgrade before portfolio DataNodes write. If an older registered ExternalPricesStorage table is missing cadence metadata, the preparation step repairs that source metadata before deriving the dynamic interpolation table. The run script creates the optional portfolio Index, publishes example OHLCV source bars to ExternalPricesStorage, interpolates prices, runs SignalWeights, PortfolioWeights, and PortfoliosDataNode, creates or reuses the crypto CRYPTO_24_7 calendar, and stores the calendar, index, and DataNode UIDs on the Portfolio row. The price configuration stores the ExternalPricesStorage TimeIndexMetaTable UID on InterpolatedPricesConfig, so the explicit upstream interpolation node can recover the price source through the SDK APIDataNode lookup path. The portfolio configuration receives that InterpolatedPrices node as price_source_instance; PortfoliosDataNode does not create interpolation storage internally. Real portfolio extensions can pass any compatible price DataNode or APIDataNode and focus on portfolio logic. The source bar frequency is read from the registered source table's cadence metadata, then used with __metatable_extra_hash_components__ to select a configured output storage table, so different source cadence, upsample frequency, and interpolation rule combinations do not collide inside one price table. The script prints the workflow steps, created row UIDs, source price row counts, explicit price-source dependency details, and published DataNode storage UIDs.

from msm.api.accounts import (
    AccountAllocationModel,
    AccountHoldingsSet,
    AccountTargetAllocation,
    PositionSet,
)
from msm.api.assets import Asset
from msm.api.portfolios import Portfolio
from msm.services import build_account_holdings_frame
from msm.services import build_target_positions_frame

holdings_set = AccountHoldingsSet.upsert(
    account_uid=account.uid,
    time_index="2026-05-25T00:00:00Z",
)
holdings = build_account_holdings_frame(
    holdings_date="2026-05-25T00:00:00Z",
    account_uid=account.uid,
    holdings_set_uid=holdings_set.uid,
    positions=[
        {"asset_identifier": "BTC", "quantity": 1.0, "direction": 1},
        {"asset_identifier": "ETH", "quantity": 10.0, "direction": -1},
    ],
)

allocation_model = AccountAllocationModel.upsert(
    allocation_model_name="balanced-allocation-model"
)
account_target_allocation = AccountTargetAllocation.upsert(
    unique_identifier="account-main-balanced-target",
    account_uid=account.uid,
    account_allocation_model_uid=allocation_model.uid,
)
position_set = PositionSet.upsert(
    account_target_allocation_uid=account_target_allocation.uid,
    position_set_time="2026-05-25T00:00:00Z",
)
btc_asset = Asset.upsert(unique_identifier="BTC", asset_type="crypto")
portfolio_sleeve = Portfolio.upsert(unique_identifier="account-main-sleeve")

targets = build_target_positions_frame(
    target_positions_date="2026-05-25T00:00:00Z",
    position_set_uid=position_set.uid,
    positions=[
        {"asset_uid": btc_asset.uid, "weight_notional_exposure": 0.6},
        {"portfolio_uid": portfolio_sleeve.uid, "weight_notional_exposure": 0.4},
    ],
)

The DataNode frame helpers validate the dynamic-table contract locally. The actual table provisioning and writes remain generic TDAG/DataNode behavior.