Scenarios
Purpose¶
The scenario registry defines the triplet (market + portfolio + strategy legs) for every scenario ID that appears in Virtufin trading / position / risk / P&L topics. The registry is the source of truth for what a scenario means; topics carry the opaque scenario ID, and the triplet lives here.
Related specs:
- Pub/Sub Topics — the sc.<scenarioId>.* topic pattern and the envelope extensions (scenarioid, marketworld, marketuniverse, portfolioworld, strategyworld) that this spec defines the values for.
Requirements¶
Requirement: Scenario Triplet¶
Every scenario SHALL be defined by exactly three legs:
| Leg | Values | Notes |
|---|---|---|
market |
{ world: "act" \| "hyp.<name>", universe: string } |
universe names the data universe and SHALL be present for both act and hyp.<name> (symmetric — e.g. act EQ_EUROPE, act ALL, hyp.STRESS EQ_2008_2009); use "ALL" for "no restriction". <name> identifies a specific hypothetical reality (see Requirement: Named Hypothetical Worlds) — bare hyp is never a valid value here. |
portfolio |
{ world: "act" \| "hyp.<name>" } |
Which portfolio state backs this scenario |
strategy |
{ world: "act" \| "hyp.<name>", id?: string } |
Which strategy code drives this scenario |
Any combination of legs is permitted (e.g., all-act, all-hyp, act-market + hyp-portfolio + hyp-strategy for shadow mode) — legs need not share the same hyp.<name> world.
Scenario: Production scenario¶
- WHEN
scenario.LIVEis bootstrapped at service start - THEN the triplet SHALL be
{ market: { world: "act", universe: "ALL" }, portfolio: { world: "act" }, strategy: { world: "act" } }
Scenario: Paper trading scenario¶
- WHEN a paper-trading scenario is registered
- THEN the triplet MAY be
{ market: { world: "act", universe: "ALL" }, portfolio: { world: "hyp.PAPER" }, strategy: { world: "hyp.PAPER", id: "strategy_v1" } } - AND the strategy's
idSHALL name the strategy code under test
Scenario: Backtest scenario¶
- WHEN a backtest scenario is registered
- THEN the triplet MAY be
{ market: { world: "hyp.BACKTEST", universe: "EQ_2008_2009" }, portfolio: { world: "hyp.BACKTEST" }, strategy: { world: "hyp.BACKTEST", id: "strategy_v10" } } - AND the market
universeSHALL name the data universe being replayed - AND the world name (
BACKTESThere) identifies the hypothetical reality independently of which universe or strategy it's paired with — a different backtest campaign would use a different name (e.g.hyp.BACKTEST_2022), not the same name with a different universe
Scenario: Live scenario scoped to a market subset¶
- WHEN a scenario needs live data restricted to a named subset (e.g. European equities only)
- THEN the triplet MAY be
{ market: { world: "act", universe: "EQ_EUROPE" }, portfolio: { world: "act" }, strategy: { world: "act" } } - AND
acttriplets SHALL name auniverseexactly likehyp.<name>ones do —actcarries no implicit "everything" meaning on its own; useuniverse: "ALL"to say so explicitly
Scenario: Shadow mode scenario¶
- WHEN a shadow-mode scenario is registered (real market, real portfolio, experimental strategy)
- THEN the triplet MAY be
{ market: { world: "act", universe: "ALL" }, portfolio: { world: "act" }, strategy: { world: "hyp.SHADOW", id: "strategy_v_candidate" } }
Requirement: Scenario Lifecycle¶
Scenarios SHALL have one of four statuses: active, paused, archived, deleted.
Scenario: Scenario states¶
- WHEN a scenario is first registered
-
THEN its status SHALL be
active -
WHEN a scenario is
paused -
THEN it SHALL NOT receive new events but retained state SHALL be queryable
-
WHEN a scenario is
archived -
THEN it SHALL be read-only; the registry entry and all
sc.<scenarioId>.*state keys SHALL be retained -
WHEN a scenario is
deleted - THEN the registry entry and ALL
sc.<scenarioId>.*state keys SHALL be purged irreversibly - AND the deletion SHALL be confirmed via an explicit second confirmation (no accidental drops)
Requirement: Scenario ID Constraints¶
Scenario IDs SHALL be constrained as follows.
Scenario: LIVE is reserved¶
- WHEN a service attempts to register a scenario with
scenarioId = "LIVE" - THEN the registration SHALL be rejected (LIVE is reserved and pre-seeded at bootstrap)
- AND
sc.LIVE.*publishes SHALL be permitted without explicit registration
Scenario: Scenario ID format¶
- WHEN a service registers a scenario
- THEN the
scenarioIdSHALL match the regex^[A-Z0-9_]{1,32}$(uppercase alphanumerics + underscore, max 32 chars) - AND the ID SHALL be opaque to routing logic — semantic meaning lives in the triplet, not the ID string
Scenario: Concurrent runs¶
- WHEN two backtests run concurrently against the same scenario
- THEN they SHALL publish to the same
sc.<scenarioId>.*topics - AND their events SHALL be disambiguated by the
runidenvelope extension - AND the registry SHALL NOT track individual runs (run lifecycle is per-publisher, not per-registry)
Requirement: Scenario Registry Storage¶
The scenario registry SHALL live in the Dapr statestore (Valkey) state store under the scenario.<scenarioId> key space.
Scenario: Registry key shape¶
- WHEN a scenario is registered
- THEN the registry entry SHALL be stored at key
scenario.<scenarioId> - AND the value SHALL be JSON:
{ "scenarioName": "<human-readable>", "status": "active" | "paused" | "archived" | "deleted", "market": { "world": "act" | "hyp.<name>", "universe": "string" }, "portfolio": { "world": "act" | "hyp.<name>" }, "strategy": { "world": "act" | "hyp.<name>", "id": "string?" }, "createdAt": "<rfc3339>", "updatedAt": "<rfc3339>" }
Scenario: Scenario index¶
- WHEN a scenario is registered
- THEN the
scenarioIdSHALL be added toscenario.index(a Valkey set) - AND
scenario.indexSHALL be the canonical list of registered scenarios
Scenario: LIVE bootstrap¶
- WHEN a service starts
- THEN it SHALL ensure
scenario.LIVEexists in the registry - AND if absent, it SHALL seed
scenario.LIVEwith the production triplet (act/ALL,act,act) - AND if present, it SHALL NOT modify the entry (LIVE is reserved; updates require elevated role)
Requirement: Scenario Lookup¶
Consumers SHALL resolve a scenario ID to its triplet via the registry, not by any other means.
Scenario: Resolve scenario ID to triplet¶
- WHEN a consumer needs the triplet for a
scenarioId - THEN it SHALL read
scenario.<scenarioId>from the state store - AND cache the result in-process (TTL ≤ 60 s; refresh on cache miss)
Requirement: Scenario Lifecycle via Existing State RPCs¶
Scenario CRUD SHALL use the existing virtufin-api State.SaveState /
State.GetState / State.DeleteState / State.QueryState RPCs. No new
RPCs SHALL be added to virtufin-api for scenario management — virtufin-api
is pure infrastructure and SHALL NOT carry domain logic.
Scenario: Register scenario¶
- WHEN a service registers a scenario
- THEN it SHALL call
State.SaveState(scenario.<scenarioId>, triplet_json) - AND add the
scenarioIdto thescenario.indexValkey set - AND reject
scenarioId = LIVE(reserved)
Scenario: Read scenario triplet¶
- WHEN a service needs the triplet for a
scenarioId - THEN it SHALL call
State.GetState(scenario.<scenarioId>) - AND cache the result in-process (TTL ≤ 60 s)
Scenario: Status transition¶
- WHEN a scenario's status changes (active / paused / archived)
- THEN the caller SHALL call
State.SaveState(scenario.<scenarioId>, new_status_json) - AND consumers caching the triplet (Scenario Lookup, TTL ≤ 60 s) SHALL observe the new status on their next refresh -- this does not gate publishes, which are not validated against scenario status (see below)
Scenario: Delete scenario (irreversible)¶
- WHEN a service deletes a scenario
- THEN it SHALL:
- Call
State.SaveState(scenario.<scenarioId>, status=deleted) - Call
State.SaveState(scenario.index, remove scenarioId) - Call
State.QueryState({"filter": {"PREFIX": "sc.<scenarioId>."}})+ per-keyState.DeleteState(...)for all matching keys - Call
State.DeleteState(scenario.<scenarioId>) - AND log at WARN level; this operation is irreversible
- AND step 3 depends on the deployed Dapr state-store component supporting the query API — if unsupported, the caller SHALL fall back to tracking
sc.<scenarioId>.*keys via its own index rather than assumingQueryStatesucceeds
Requirement: No Pre-Publish Scenario-State Validation¶
No service SHALL validate a scenario's registration or lifecycle status before publishing to its sc.<scenarioId>.* topics — see Pub/Sub Topics spec §No Topic or Scenario-State Validation Anywhere. scenario.<scenarioId> lookups (see Scenario Lookup above) are for consumers resolving a triplet's meaning, not a pre-publish gate.
Scenario: Publish to an unregistered or non-active scenario¶
- WHEN a service publishes to
sc.<scenarioId>.*for ascenarioIdthat is unregistered, paused, archived, or deleted - THEN the publish SHALL proceed unconditionally — the registry is a lookup for consumers, not a publish-time gate
Requirement: Universe Naming¶
Universes SHALL name data universes for market data, whether act or hyp.<name>. They appear in act.<universe>.exchange.<venue>.* / hyp.<name>.<universe>.exchange.<venue>.* topics AND in the marketuniverse envelope extension.
Scenario: Universe format¶
- WHEN a service defines a new data universe
- THEN the universe SHALL match
^[A-Z0-9_]{1,32}$(same regex as scenario IDs) - AND the universe SHALL be stable across runs (changing the universe name for the same data set breaks subscribers)
Scenario: Universe vs scenario ID¶
- WHEN a service needs to distinguish "the data universe" from "a scenario running against that universe"
- THEN the universe SHALL identify the data universe (in
marketuniverse) - AND the scenario ID SHALL identify the scenario (in
scenarioid) - AND one universe MAY back many scenario IDs (e.g.,
EQ_2008_2009for backtests with different strategies, orALLfor every LIVE-derived scenario)
Requirement: Named Hypothetical Worlds¶
A hypothetical world value SHALL always name a specific hypothetical reality: hyp.<name>, where <name> matches ^[A-Z0-9_]{1,32}$ (same format as scenario IDs and universes). Bare "hyp" SHALL NOT be assigned as a world value anywhere — not a triplet leg, not a topic segment, not an envelope extension. Bare hyp denotes the category of all hypothetical worlds (every world starting with hyp.), useful only for documentation and wildcard subscriptions, never a concrete value.
<name> is independent of universe and of scenarioId: it does not need to be registered anywhere, is not looked up via the scenario registry, and carries no implicit relationship to any specific scenario. One hyp.<name> world MAY pair with many different universes and back many different scenario IDs, exactly as one universe already MAY back many scenario IDs (see Requirement: Universe Naming).
Scenario: Naming a stress-test world¶
- WHEN a service defines a hypothetical world for a stress-testing campaign
- THEN the world SHALL be named e.g.
hyp.STRESS - AND it MAY be paired with any universe, e.g.
{ world: "hyp.STRESS", universe: "EQUITY" }or{ world: "hyp.STRESS", universe: "FX" }— both are valid, distinct market legs sharing the same named world
Scenario: Bare hyp is rejected as a value¶
- WHEN a service attempts to register a scenario or publish a topic with a bare
world: "hyp"(no name) - THEN this SHALL be treated as a spec violation —
hypalone identifies a category, not a world