0033. Pricing Valuation Position Boundary
Status
Accepted - implementation complete
Context
msm_pricing currently has an exported Position object under
msm_pricing.instruments.position. It is an in-memory Pydantic object that
contains PositionLine entries:
Position
position_date optional
lines:
instrument
units
extra_market_info optional
The current class is not a SQLAlchemy model, is not part of
msm_pricing.models, and is not included in pricing_sqlalchemy_models().
There is no pricing PositionTable today.
The current pricing architecture has moved away from that older shape:
- instrument terms are identity-free Pydantic payloads;
- durable asset linkage lives in
AssetCurrentPricingDetailsTableandAssetPricingDetailsStorage; - pricing market-data source selection is explicit through
PricingMarketDataSetandPricingMarketDataSetBinding; - instruments are valued for an explicit valuation date through
InstrumentModel.set_valuation_date(...); - bond and swap pricing methods accept market-data-set context.
The old Position class does not participate in that lifecycle. It is exported
and mentioned in package documentation, but it has no tests or examples in the
current repository.
Current Implementation Problems
The existing Position object should be treated as legacy for these reasons:
- It does not apply
position_dateto the contained instruments. Current instruments requireset_valuation_date(...)before pricing. Position.price()does not acceptvaluation_dateormarket_data_set, so pricing context must be hidden in pre-mutated instrument instances.- It calls
instrument.price()with no arguments, even though current instruments can require market-data-set selection. - It assumes
instrument.content_hash(), butInstrumentModeldoes not define that method. - It has stale registry setup for option instrument names that are not current package exports.
- It silently skips instruments without expected cashflow methods. Valuation workflows should fail clearly when a requested valuation output cannot be produced.
extra_market_infois untyped and does not define a stable valuation input contract.- The name
Positioncollides with account and portfolio position concepts. Account holdings, target positions, and portfolio weights are business exposure concepts; pricing should consume them, not redefine ownership.
The useful concept is still valid:
instrument + units + valuation context
But that concept should be explicit and valuation-scoped, not a persisted
position registry and not a vague package-level Position object.
Decision
Do not introduce a pricing PositionTable now.
Replace the old msm_pricing.Position public concept with an explicit
in-memory valuation basket. The target naming should avoid pretending that
pricing owns business positions. Preferred names:
ValuationLine
ValuationPosition
ValuationPosition is a transient valuation input. It links priceable
instrument terms to units for one valuation context.
Target shape:
class ValuationLine(BaseModel):
instrument: InstrumentModel
units: float
asset_uid: UUID | None = None
metadata_json: dict[str, Any] = {}
class ValuationPosition(BaseModel):
valuation_date: datetime
market_data_set: PricingMarketDataSetSelector = None
lines: list[ValuationLine]
The required semantics are:
valuation_dateis required at the basket level.market_data_setis selected at the basket level. Line-level market-data-set overrides are not supported; mixed source valuation must build separate baskets or normalize source selection before constructing the basket.unitsis a multiplier on the instrument-defined economics.- Each instrument is valued only after the basket applies the valuation date.
- Pricing failures are strict. If a requested output cannot be produced for one line, the basket valuation fails instead of silently dropping the line.
asset_uidis optional because ad hoc instrument valuation may not be tied to a persisted asset. When present, it is the canonical asset reference for the line, not a generic provenance mechanism.metadata_jsonis optional caller metadata. It must not be required for core pricing semantics.
The valuation basket may expose methods such as:
position.price()
position.analytics()
position.price_breakdown()
position.get_cashflows()
position.get_net_cashflows()
Those methods should consistently:
- set the valuation date on every instrument;
- pass
market_data_setto instrument methods that accept it; - scale per-line values by
units; - include enough line-level detail in breakdown outputs to map results back to
the submitted input order and optional
asset_uid; - fail loudly on unsupported output requests.
Unit Semantics
units is not the instrument definition. It is the quantity multiplier applied
to an already defined instrument.
For example, a bond instrument still owns economics such as face value, schedule, coupon, index reference, spread, and redemption terms. The valuation line owns how many such instrument units are held for this valuation.
Adapters from accounts or portfolios must normalize their domain quantity into this contract. If an upstream account stores nominal amount, the adapter must choose a consistent conversion into:
instrument economic notional
units multiplier
That conversion belongs at the adapter boundary, not inside the low-level instrument pricing methods.
Ownership Boundary
Pricing owns valuation of instrument terms.
Pricing does not own durable business position state:
- account holdings remain account data;
- target positions remain account allocation data;
- portfolio weights remain portfolio construction data;
- valuation baskets are transient unless a later ADR defines valuation-run persistence.
Future persistence should not start with a generic PositionTable in
msm_pricing. If persistence is needed later, model the actual durable concept:
- account or portfolio position state, owned by
msmormsm_portfolios; or - valuation-run audit input/output, owned by
msm_pricingonly if the purpose is reproducible pricing audit.
A future pricing-owned valuation-run persistence design should use explicit valuation-run language, not position language. The expected shape is:
ValuationRun
uid
valuation_date
market_data_set
requested_outputs
created_at
metadata_json
ValuationRunLine
valuation_run_uid
line_index
asset_uid nullable
units
instrument_type
instrument_dump
pricing_details_date nullable
result_json
metadata_json
That persistence should require a separate ADR before implementation because it would define an audit artifact, not account holdings or portfolio positions.
Construction Paths
The valuation basket should support multiple sources without owning them:
- Ad hoc valuation: caller passes instruments and units directly.
- Asset valuation: caller supplies asset rows and units; pricing loads the relevant instrument payload from pricing details.
- Account valuation: account services provide asset exposure rows; an adapter turns them into valuation lines.
- Portfolio valuation: portfolio services provide asset weights, quantities, or notional exposures; an adapter turns them into valuation lines.
- Mixed valuation: callers can combine lines from several sources as long as they normalize each source into the same minimal line contract.
ValuationPosition should not query account or portfolio tables by itself.
Those packages own their own source selection and should pass normalized
valuation lines into pricing.
Pricing should provide only the asset-to-instrument bulk loader, because current pricing details are pricing-owned:
from msm_pricing.api import load_instruments_from_assets
instruments_by_asset_uid = load_instruments_from_assets(assets)
Account and portfolio code should use that mapping after they resolve their own
source rows into (asset_uid, units) pairs.
This implementation does not add generic source_type or source_uid fields.
Those fields are too arbitrary without a concrete consumer. If an account or
portfolio adapter needs provenance later, that adapter should own its own
mapping or a later ADR should introduce a specific provenance contract.
Non-Goals
This ADR does not:
- create a pricing
PositionTable; - define persisted valuation-run tables;
- define generic source/provenance fields such as
source_typeandsource_uid; - make
msm_pricingown account holdings or portfolio weights; - add portfolio construction behavior to pricing;
- preserve the old
PositionAPI as a compatibility surface.
Consequences
Positive consequences:
- valuation context becomes explicit;
- account and portfolio ownership boundaries stay clean;
- pricing can value arbitrary baskets without pretending to own business positions;
- future persistence can be designed around the real durable artifact.
Costs:
- callers using
msm_pricing.Positionmust migrate; - pricing examples and docs need to move to the new valuation-basket API;
- tests must cover strict failure behavior and context propagation.
Implementation Tasks
Completed:
- [x] Remove
PositionandPositionLinefrom the top-levelmsm_pricingexports. - [x] Remove or replace
src/msm_pricing/instruments/position.py. - [x] Add the new in-memory valuation-basket implementation under a clear
module such as
msm_pricing.valuation. - [x] Add tests proving
valuation_dateis applied to every instrument before valuation. - [x] Add tests proving
market_data_setis passed through to supported instrument methods. - [x] Add tests proving price, analytics, and cashflow outputs are scaled by
units. - [x] Add tests proving unsupported requested outputs fail clearly instead of silently dropping lines.
- [x] Add an example showing ad hoc fixed-income valuation from instruments and units.
- [x] Update
examples/msm_pricing/bond_pricing_example/to showValuationPositionusage for at least one bond valuation, so the example demonstrates both single-instrument pricing and instrument-plus-units basket valuation. - [x] Update
src/msm_pricing/README.md,docs/knowledge/msm_pricing/index.md, and the pricing tutorial text to remove the oldPositionsurface and document the valuation basket. - [x] Update the pricing skill after implementation so agents stop recommending
msm_pricing.Position. - [x] Add account and portfolio adapter docs showing how each owning package
normalizes source rows into
ValuationLineinputs. - [x] Decide line-level
market_data_setoverrides: not supported in this implementation; aValuationPositionis homogeneous by market-data set. - [x] Add a pricing-owned bulk loader,
msm_pricing.api.load_instruments_from_assets(...), next to pricing-details persistence. - [x] Design the future valuation-run persistence boundary: it must be a
valuation-run audit artifact introduced by a later ADR, not a pricing
PositionTable.
Open Questions
- Should
asset_uidremain optional forever, or become required for helpers that specifically build lines from persisted pricing details? - If valuation-run persistence is added later, should result snapshots include only requested output JSON or also normalized tabular cashflow/analytics rows?