RFC — Forecast / Scenario domain types (Sprint S6)
- Status: Proposed (2026-08-10) — do not implement types in this change set
- Sprint: S6 (Phase 4 prep) of alphaswarm-first-class-module-transformation-plan.md
- Authors: Platform architecture
- Related ADRs (approved): ADR 033, ADR 034, ADR 037
- Inventory (S5): alphaswarm-legacy-domain-import-inventory.md
1. Motivation
Track B / plan gap E11 and Phase 4 call for additive first-class types that today exist only as ad-hoc arrays, string fields, or pricing-context behaviour knobs:
| Gap type | Today | Pain |
|---|---|---|
Forecast | Model / ML / economic helpers return bare arrays or dicts | No shared identity, horizon, or measure semantics |
CalibrationResult | PricingContext.behaviour="Calibrated" is a string mode, not an entity | Cannot audit or replay a calibration |
Benchmark | Referenced in analytics / promotion prose | No typed instrument or series handle |
Scenario | curve_overrides / stress dicts on PricingContext | Not addressable; cannot compose Simulation (ADR 034) cleanly |
Listing | InstrumentBase.primary_listing_venue: str | None | Instrument×Venue product missing |
| Fill history | ExecutionReport + legacy fills table + OrderFilled event | Open plan question: additive Fill table vs enrich reports |
This RFC freezes schemas and placement so Phase 4 can land additive modules without inventing parallel public APIs (ADR 033) or a second research→money path (ADR 037).
2. Non-goals
- Implementing dataclasses, ORM tables, migrations, MCP tools, or facade re-exports in this ticket.
- Promoting these types into
alphaswarm_core(wrong layer — ADR 033). - Collapsing
ScenariointoSimulation/Workflow/WorkRequest/DeploymentSpec(ADR 034). - Letting forecasts or scenarios emit venue orders; tradeable crossings remain
OrderIntent/StrategyPromotionRequest(ADR 037). - Claiming unverified latency or “identical semantics” across engines (ADR 034).
- Removing legacy
Direction,orders/fillstables, orcore/types.pyshims (ADR 038 / inventory Phase 6).
3. Placement
| Concern | Home | Notes |
|---|---|---|
| Canonical types | alphaswarm.core.domain.{forecast,calibration,benchmark,scenario,listing} | Mirror existing orders.py / positions.py style |
| Public re-export | alphaswarm.core.domain.__init__ + optional thin alphaswarm.facade.domain | Facade = re-export only (ADR 033); no logic copy |
| Pricing measure enum (existing) | alphaswarm.risk.pricing.measures.RiskMeasure | Stay in risk package; domain types reference it |
| Fill audit trail (existing) | alphaswarm.trading.execution.ExecutionReport + execution_reports | Enrich / view; see §6 |
| Persistence (later Phase 4) | New additive Alembic migration(s) only if UI/MCP need durable rows | Prefer value objects first; tables when pickers/ledger require them |
Do not place these types in alphaswarm_core.
4. Measure semantics (P vs Q)
Forecasts and pricing must not silently share a probability measure.
Propose a small domain enum (name TBD at implement time):
MeasureSemantics
P # physical / real-world / predictive (statistical forecasting)
Q # risk-neutral / pricing measure
MIXED # explicit composite; requires documented components
UNSPECIFIED
Hooks:
| Type | Default | Rule |
|---|---|---|
Forecast | P | Predictive distributions; never used as a direct money-plane price without a pricing bridge |
CalibrationResult | Q (or calibrated market measure) | Output of curve/surface fit consumed by PricingContext |
Benchmark | UNSPECIFIED (usually market-observed under P) | Clarify when used inside a Q pricing scenario |
Scenario | Required field | Stress under P vs pricing override under Q must be explicit |
RiskMeasure / PricingContext | Remains Q-oriented | Domain Scenario may supply overrides into a PricingContext; it does not replace it |
Forecast → money plane is forbidden. Path remains: forecast/advisory → validation → risk → authorization → OrderIntent (ADR 037).
5. Proposed type sketches (schemas only)
Field lists are indicative; final dataclasses land in Phase 4.
5.1 Forecast
Module: alphaswarm/core/domain/forecast.py
| Field | Type (sketch) | Notes |
|---|---|---|
forecast_id | str | Stable id for ledger / experiment stamp |
instrument_id | InstrumentId | None | Optional when forecasting a series/index |
series_ref | str | None | Dataset / economic series handle when not an instrument |
horizon | structured (e.g. bars / timedelta / event) | Required |
as_of | datetime | PIT issuance time |
measure_semantics | MeasureSemantics | Default P |
point | Decimal | float | None | Point estimate |
distribution | opaque payload or typed quantiles | Optional; prefer structured quantiles over raw ndarray in the public type |
model_ref | str | None | Spec / skill / predictor version id |
experiment_id / test_id | optional FKs | Hard rule 34 when persisted |
meta | dict | Extensibility |
Consumers: alphaswarm_models predictors, economic GdpForecast-style rows (bridge later), agent advisory tools (read-only via DataMCP when persisted).
5.2 CalibrationResult
Module: alphaswarm/core/domain/calibration.py
| Field | Type (sketch) | Notes |
|---|---|---|
calibration_id | str | |
as_of | datetime | |
behaviour | "Calibrated" | "ConstraintsBased" | Align with PricingContext |
measure_semantics | MeasureSemantics | Default Q |
inputs_digest | str | Hash of constraint quotes / market snapshot |
outputs_ref | curve/surface ids or artifact URI | Opaque to domain; storage elsewhere |
quality | metrics dict (rmse, arb gaps, …) | No free-form “good enough” for promotion |
pricing_context_run_id | optional link | To existing PricingContextRunRow |
Closes the gap where “Calibrated” is only a context flag.
5.3 Benchmark
Module: alphaswarm/core/domain/benchmark.py
| Field | Type (sketch) | Notes |
|---|---|---|
benchmark_id | str | |
name | str | Human label |
instrument_id | InstrumentId | None | When benchmark is a tradable/index instrument |
series_ref | str | None | Alternate: external index series |
currency | Currency | None | |
measure_semantics | MeasureSemantics | Usually observed under P |
meta | dict |
Used by analytics, promotion evidence, and scenario relative-return definitions — not an order type.
5.4 Scenario
Module: alphaswarm/core/domain/scenario.py
| Field | Type (sketch) | Notes |
|---|---|---|
scenario_id | str | |
name | str | |
measure_semantics | MeasureSemantics | Required |
as_of | datetime | None | |
curve_overrides | dict | Same shape spirit as PricingContext.curve_overrides |
surface_overrides | dict | |
shock_set | structured list | Named shocks (spot, vol, rates, credit) |
benchmark_id | optional | Relative stress |
simulation_binding | optional ref | Thin link to GraphSpec/backtest config — not a new Simulation runtime (ADR 034) |
meta | dict |
Scenario is an Activity-input / Simulation-input value object, not a Workflow and not a money-plane command.
5.5 Listing
Module: alphaswarm/core/domain/listing.py
| Field | Type (sketch) | Notes |
|---|---|---|
listing_id | str | |
instrument_id | InstrumentId | |
venue | Venue | Domain venue VO |
mic / symbol_at_venue | optional | Venue-local symbol |
status | enum (active / halted / delisted) | |
is_primary | bool | Replaces sole reliance on primary_listing_venue string |
meta | dict |
Additive beside InstrumentBase.primary_listing_venue; migration of string→Listing is a later strangler step, not a blocker for introducing the type.
6. Fills — enrich execution_reports, do not add a parallel domain Fills table
Decision proposed by this RFC: treat ExecutionReport (report_kind = fill / partial fill) as the canonical fill history. Do not introduce a first-class domain Fill aggregate or a new fills domain table in Phase 4.
Rationale (evidence-backed):
ExecutionReportalready keys on(venue, venue_execution_id)and carrieslast_quantity,last_price, cumulative qty, commission, liquidity side — seealphaswarm/trading/execution/execution_report.pyand orders hard-rule set (legacyfillsis dual-write only).- Domain already has event
OrderFilledunderalphaswarm.core.domain.orders. - Plan open question #7; inventory / ADR 033 preference is Strangler Fig without duplicate ledgers.
- A separate
Filltype would invite dual writers and break the “no new readers against legacyfills” rule.
Allowed later (still non-implementing here):
- Optional typed alias / view DTO
FillViewthat is a narrowed projection ofExecutionReportwherereport_kindindicates a fill — lives next to execution, or as a thin facade helper, not a second persistence root. - Additive columns on
execution_reportsif multi-partial reconstruction needs them (e.g. explicitfill_seq,leaves_qty) — single table enrichment only.
7. Direction.NET vs PositionSide.FLAT remapping hazard (from S5 inventory)
Inventory §2 and §7 document a value conflict, not a rename:
Legacy (alphaswarm.core.types.Direction) | Domain (PositionSide) | Meaning |
|---|---|---|
LONG | LONG | Long exposure |
SHORT | SHORT | Short exposure |
NET | ≠ FLAT | NET = netting / absolute-size accounting mode; FLAT = zero position |
Forbidden: Direction.NET → PositionSide.FLAT automatic mapping.
Required remapping policy (implement with Phase 3–4 strangler, not in this RFC):
- Treat
NETas an accounting mode (net book / absolute size), not a side. - When converting positions: derive
PositionSidefrom signed quantity (>0LONG,<0SHORT,==0FLAT); never from theNETtoken alone. - Hotspots called out by inventory:
backtest/broker_sim.py,strategies/execution.py— must get explicit adapter helpers before Symbol/Direction mass swap. - Optional future enum
PositionAccountingMode { GROSS, NET }may coexist withPositionSide; do not overloadFLAT.
Forecast/Scenario types that express directional bias MUST use PositionSide or OrderSide, never legacy Direction.NET.
8. Migration phase
| Step | When | Work |
|---|---|---|
| This RFC | S6 | Schemas + decisions only |
| Phase 4a | After RFC acceptance | Add domain modules + unit schema tests; re-export from core.domain |
| Phase 4b | Optional | Facade re-export; DataMCP / EntityPicker only if persisted |
| Phase 4c | Optional | Additive migrations for Forecast/Scenario/Listing rows if product requires |
| Phase 3 overlap | Parallel | Direction/PositionSide remapping helpers before strategy mass-migration (inventory) |
| Phase 5+ | Later | Scenario binding to Simulation facade; golden replay may consume Scenario digests |
Rollback: unused additive types; no forced cutover (plan Phase 4 rollback row).
9. Tests (when implemented — not now)
- Schema / round-trip tests under
tests/core/domain/for each new module. - Measure-semantics invariants:
ForecastdefaultP; rejecting unspecifiedScenario.measure_semantics. - Explicit unit tests that
Direction.NETmapping helpers do not yieldFLAT. - Golden fixtures:
ExecutionReportfill sequence reconstructs cumulative qty without a separate Fill table. - Boundary: no import of new domain forecast types from
alphaswarm_controller/alphaswarm_cli. - ADR 037 guard: no test path from
Forecastdirectly to venue submit.
10. Open questions
- Persist Forecast/Scenario in Postgres in Phase 4 or keep value-object-only until a UI picker needs them?
- Should
CalibrationResultown curve bytes or only digests + artifact URIs? - Is
FillViewworth a public name, or is documentingExecutionReportenough? - Does
Listingneed temporal validity (list/delist windows) in v1?
11. Acceptance for this RFC
- Types proposed with homes under
alphaswarm.core.domain - Fills decision: enrich / project
execution_reports, no parallel Fill root -
Direction.NET≠PositionSide.FLAThazard documented with remapping policy - P vs Q measure hooks defined
- ADRs 033 / 016 / 019 and S5 inventory cited
- Status remains Proposed; no production type implementation in S6