Skip to content

Portfolios

Construct an equal-weights portfolio end to end. The workflow runs in two stages: a schema-preparation step that provisions the interpolated price storage, then a run step that publishes prices, computes weights, and stores the portfolio TimeIndexTableUpdater results. It reuses the calendar from Calendars as Portfolio.calendar_uid.

For the runtime model behind these row APIs, see Core Concepts.

Two-stage equal-weights workflow

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, PortfolioCalendarEvents, PortfolioRebalance, PortfolioWeights, and PortfoliosDataNode, creates or reuses the crypto CRYPTO_24_7 calendar, and stores the calendar, index, and TimeIndexTableUpdater 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 TimeIndexTableRef lookup path. The portfolio configuration receives that InterpolatedPrices node as valuation_source_instance and sets valuation_column="close"; PortfoliosDataNode does not create interpolation storage internally. Real portfolio extensions can pass any compatible asset-indexed valuation TimeIndexTableUpdater or TimeIndexTableRef and choose any numeric valuation column, such as fair_value or nav, without reshaping the source into OHLC bars. A focused configuration example is available at examples/msm_portfolios/portfolio_custom_valuation_column_example.py:

python examples/msm_portfolios/portfolio_custom_valuation_column_example.py \
  --source-time-index-meta-table-uid <fair-value-time-index-meta-table-uid>

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 valuation row counts, explicit valuation-source dependency details, and published TimeIndexTableUpdater storage UIDs.

Source-bar timestamp payloads are kept as timezone-aware datetimes and normalized to nanosecond UTC before InterpolatedPrices publishes them. When repairing rows written by an affected older runtime, upgrade ms-markets before applying any asset_identifier-scoped tail delete, then replay the interpolation updater and downstream portfolio graph immediately.

Asset-scope mappings use asset_identifier as their canonical identity key; the example supplies that key alongside calendar. Mapping payloads containing unique_identifier are rejected, even when asset_identifier is also present.

Understand the independent clocks

The example intentionally keeps execution, valuation, and reporting separate:

FixedWeights signal
  + PortfolioCalendarEvents publishes persisted CRYPTO_24_7 session events
  -> CalendarEventSignal selects market_close observations
  -> PortfolioRebalance persists complete or unfinished strategy state
  -> PortfolioWeights projects only executed-weight changes
  -> PortfoliosDataNode values current holdings at valuation-source observations
  -> optional PortfolioAnalytics samples canonical values for reporting

PortfoliosDataNode never creates a calendar or frequency-based index. A weekly CalendarEventSignal can therefore produce sparse weight rows while a daily valuation source produces daily portfolio values. ImmediateSignal is reserved for true execution at a signal's original observation timestamp. CalendarEventSignal requires an explicit persisted calendar identifier and an explicit calendar-event updater or table reference. It does not read a calendar behind the dependency graph or fall back to a process-local calendar. Ensure the required CalendarSession horizon exists before executing the graph.

Each execution or valuation window is seeded with the latest eligible row for every required asset using one set-based get_last_observation(...) request. For shared signal storage, each range coordinate includes both signal_uid and asset_identifier, preventing another signal's rows from entering the seed. Portfolio valuation also rechecks the response boundary: executed-weight seed rows are kept only when they are strictly earlier than the window start, and duplicate (time_index, asset_identifier) coordinates fail before pivoting.

This example uses one concrete strategy; it does not define the architecture. Under ADR 0040, every strategy declares the observations it needs and implements the same state transition contract. TimeWeighted consumes observed price bars, VolumeParticipation consumes price and volume bars, and TrailingAverageDailyVolumeParticipation uses completed historical daily VWAP-times-volume only to estimate a daily notional cap while executing at an observable intraday price, and LiquidityConstrained consumes price plus available-liquidity observations. Missing capacity leaves persisted work pending or partial. Adding a strategy does not add a branch or date generator to PortfolioRebalance, PortfolioWeights, or PortfoliosDataNode.

For a bounded five-percent trailing daily participation policy:

from mainsequence.meta_tables import TimeIndexTableRef
from msm_portfolios.rebalance_strategy import (
    TrailingAverageDailyVolumeParticipation,
)

strategy = TrailingAverageDailyVolumeParticipation(
    daily_liquidity_instance=TimeIndexTableRef.from_uid(daily_table_uid),
    execution_bars_instance=TimeIndexTableRef.from_uid(intraday_table_uid),
    daily_vwap_column="vwap",
    daily_volume_column="volume",
    execution_price_column="close",
    execution_volume_column="volume",
    lookback_observations=20,
    history_lookback_days=60,
    session_timezone="America/New_York",
    execution_start="09:30",
    execution_end="16:00",
    max_daily_participation=0.05,
    max_bar_participation=0.10,
    total_notional=50_000_000,
)

Inject strategy as BacktestingWeightsConfig.rebalance_strategy_instance. The daily source must timestamp each row when the completed bar is available; the intraday source supplies the actual price used by the execution assumption. See examples/msm_portfolios/portfolio_trailing_adv_participation_example.py for the focused configuration helper. To preview real transitions against two registered source tables without persisting portfolio state, run:

python examples/msm_portfolios/portfolio_trailing_adv_participation_preview.py \
  --daily-liquidity-table-uid <daily-table-uid> \
  --execution-bars-table-uid <intraday-table-uid> \
  --signal-time 2026-01-05T14:25:00Z \
  --start 2026-01-05T14:30:00Z \
  --end 2026-01-05T21:00:00Z \
  --target BTC-USD=0.60 \
  --target ETH-USD=0.40

The preview filters both reads to the requested target assets, expands only the strategy's bounded daily-history window, and prints execution price, quantity, remaining target weight, trailing capacity, and consumed daily cap. Use the full PortfolioRebalance graph when those transitions should be persisted and projected into canonical portfolio weights.

Migrate values produced by the former daily resampler

If an existing portfolio has midnight-indexed values from the former combined portfolio path, run examples/msm_portfolios/portfolio_midnight_timestamp_repair.py without --apply first. Set --end at or after the latest stored portfolio value; the plan refuses to delete a tail it has not fully inspected and validates every candidate against the portfolio's persisted calendar and historical close_time. After reviewing the plan, run it with --apply for one portfolio while scheduled writers are paused, then immediately rerun the portfolio workflow to rebuild the scoped tail. A post-replay dry run must report no rollback.

The core configuration uses valuation_alignment_policy to bound per-asset as-of freshness. In strict mode, freshness is required when the current or immediately preceding executed weight is nonzero. This keeps entry and exit prices mandatory without making an unchanged zero-weight row economically required or removing that row from the stored signal. It does not accept portfolio_prices_frequency; configure a separate PortfolioAnalytics node when a chart or analysis needs daily, weekly, or monthly sampling. Analytical rows keep the actual selected source observation in both time_index and source_time_index, with bucket boundaries in period_start and period_end.

Preview position cash flows and FX valuation

The full configuration, input-grain, ledger, restart, and extension contracts are documented in Position-Aware Portfolio Accounting.

Position-aware accounting is opt-in and writes a separate canonical event ledger. The existing weight-only example and its identity do not change. Start with the offline fixture before wiring registered dependencies:

uv run --extra portfolios python \
  examples/msm_portfolios/portfolio_cashflows_and_fx_valuation_example.py

The fixture starts with USD cash, buys ten shares quoted in EUR, recognizes a EUR 1-per-share dividend, sells the shares, and then settles the retained EUR receivable. Explicit EUR/USD observations value every component in USD. Inspect the event-level summary rows: entitlement recognizes income once; the later cash receipt has zero recognized P&L because it only exchanges receivable state for settled cash.

To see the user-extension boundary, run:

uv run --extra portfolios python \
  examples/msm_portfolios/portfolio_custom_cashflow_model_example.py

That example defines a module-level PositionCashFlowModel subclass and emits two royalty events in one vectorized calculation partition. Production models must inject every source as a declared TimeIndexTableUpdater or TimeIndexTableRef; the in-memory example passes frames directly so it remains deterministic and safe to run without platform writes.

To verify derivative settlement and same-time event ordering, run:

uv run --extra portfolios python \
  examples/msm_portfolios/portfolio_perpetual_funding_example.py

This fixture configures TargetWeightExecutionModel on the existing ImmediateSignal rebalance strategy. Explicit instrument terms declare notional-weight sizing, contract units and rounding, USD quote currency, and variation-margin settlement. The strategy opens ten contracts without deducting their full notional from cash and applies commission once. At the next timestamp, a funding lifecycle model reduces NAV before the same-time target is sized, so the position is reduced to eight contracts; a later funding event changes cash and NAV without a rebalance. Portfolios never accept broker executions or Account holdings; all execution facts in this path are internal deterministic simulation output.

Next → Pricing Instruments