Skip to content

Account Virtual Funds

Virtual funds are core msm account-allocation views over real account holdings. They are not assets, not custody accounts, and not portfolio construction objects.

The canonical custody record remains:

AccountTable
  <- AccountHoldingsSetTable
       <- AccountHoldingsStorage

Virtual funds derive allocation views from that custody record:

AccountTable
  <- VirtualFundTable
       target_portfolio_uid -> PortfolioTable.uid

VirtualFundTable
  <- VirtualFundHoldingsSetTable
       source_account_holdings_set_uid -> AccountHoldingsSetTable.uid

VirtualFundHoldingsSetTable
  <- VirtualFundHoldingsStorage
       asset_identifier -> AssetTable.unique_identifier

msm_portfolios remains the portfolio construction engine. It can provide portfolio weights or run portfolio DataNodes, but it does not own virtual-fund identity or holdings storage.

Storage Shape

VirtualFundTable stores the account-owned allocation view:

uid PK
unique_identifier unique
account_uid FK -> AccountTable.uid
target_portfolio_uid FK -> PortfolioTable.uid

VirtualFundHoldingsSetTable groups one allocation result:

uid PK
virtual_fund_uid FK -> VirtualFundTable.uid
source_account_holdings_set_uid FK -> AccountHoldingsSetTable.uid
time_index

VirtualFundHoldingsStorage stores timestamped allocation rows keyed by:

(time_index, virtual_fund_uid, asset_identifier)

The row stores a positive allocated_quantity, a first-class allocation_strategy, and a signed direction. Short exposure is represented with direction = -1, not a negative quantity.

allocation_strategy records the strategy used to create the row. Low-level explicit publications use explicit; rows published from apply_account_virtual_fund_allocation_plan(...) use the allocation policy mode, such as proportional_attribution or strict_feasible.

Planner Flow

ADR 0029 defines the account holdings to virtual-fund allocation planner. The planner is a dry-run service: it computes a deterministic plan and performs no writes.

Use the canonical account target snapshot path. Do not pass a raw account UID, raw holdings-set UID, raw source holdings, or raw target demand rows into the public planner.

from msm.services.accounts import (
    AllocationPolicy,
    HoldingsSelectionPolicy,
    plan_account_virtual_fund_allocations,
)

plan = plan_account_virtual_fund_allocations(
    position_set_uid=position_set.uid,
    valuation_time=workflow_time,
    valuation_asset_uid=usd_asset.uid,
    holdings_selection_policy=HoldingsSelectionPolicy(),
    valuation_resolver=valuation_resolver,
    allocation_policy=AllocationPolicy(),
)

The resolver follows one deterministic path:

PositionSetTable.uid
  -> AccountTargetAllocationTable.uid
  -> AccountHoldingsSetTable(account_uid, valuation_time)
  -> AccountHoldingsStorage rows
  -> TargetPositionsStorage rows
  -> AssetTable identity rows
  -> portfolio-target expansion
  -> valuation resolver

Core msm owns the account and virtual-fund allocation logic. Portfolio weights still come from an explicit portfolio-target expander at the workflow boundary, so msm does not import msm_portfolios.

Allocation Policy

The default policy is proportional_attribution.

For each asset, the planner allocates virtual-fund claims first, using a vectorized per-asset scale factor:

source_capacity = abs(account signed holding)
virtual_demand  = sum(abs(requested virtual-fund signed quantity))
scale           = min(1, source_capacity / virtual_demand)

Each virtual fund receives:

allocated_signed_quantity = requested_signed_quantity * scale

The direct account sleeve is the balancing residual:

direct_account_sleeve = signed_account_holding - sum(virtual_fund_allocations)

The direct target is diagnostic intent. It is not included in the virtual-fund fill denominator. This keeps opposite-signed direct targets from incorrectly reducing virtual-fund allocations.

strict_feasible is a validation mode. If virtual-fund gross demand exceeds available source capacity for any asset, the plan is marked infeasible and must not be applied.

Valuation Resolver

The planner does not own pricing, derivative valuation, FX conversion, or NAV calculation. A valuation resolver supplies valuation metrics and target quantity demand when the workflow starts from notional targets.

The resolver is called with:

requested_metrics = ("nav",)
source_holdings
target_notional_demands
valuation_time
valuation_asset_uid
valuation_policy

valuation_asset_uid is an AssetTable.uid, not an ISO code or ticker. Any currency mapping or provider-specific logic belongs inside the resolver.

The resolver returns totals and optional lines per asset, source row, or target row. For account virtual-fund allocation, nav is the required metric.

For notional target rows, the resolver must also return target_quantity_demands. The planner uses those quantity demands for the vector allocation step and keeps the notional values for diagnostics.

Apply Flow

Apply only after inspecting a feasible or attributed plan:

from msm.data_nodes.accounts import VirtualFundHoldings
from msm.services.accounts import apply_account_virtual_fund_allocation_plan

node = VirtualFundHoldings(config=VirtualFundHoldings.default_config())
frame = apply_account_virtual_fund_allocation_plan(
    plan,
    data_node=node,
    run=True,
)

The apply step may create or reuse VirtualFundTable rows, creates the holdings-set row, builds the VirtualFundHoldingsStorage frame, and optionally publishes it through the DataNode.

See examples/msm/accounts/account_portfolio_full_workflow.py for the complete end-to-end account workflow. Run it with --with-virtual-fund-allocation to extend the account-plus-portfolio workflow with a dry-run allocation plan, or with --apply-virtual-fund-allocation to publish the resulting VirtualFundHoldings rows after the dry-run plan is printed. The focused examples/msm/accounts/account_virtual_fund_allocation_example.py script is a thin wrapper around that same full workflow extension.

Low-Level Publisher

VirtualFund.allocate_from_account_holdings_set(...) remains a low-level explicit publisher. Use it only when the allocation quantities are already known and policy-approved. It is not the allocation policy engine.