Skip to content

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.LIVE is 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 id SHALL 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 universe SHALL name the data universe being replayed
  • AND the world name (BACKTEST here) 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 act triplets SHALL name a universe exactly like hyp.<name> ones do — act carries no implicit "everything" meaning on its own; use universe: "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 scenarioId SHALL 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 runid envelope 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 scenarioId SHALL be added to scenario.index (a Valkey set)
  • AND scenario.index SHALL be the canonical list of registered scenarios

Scenario: LIVE bootstrap

  • WHEN a service starts
  • THEN it SHALL ensure scenario.LIVE exists in the registry
  • AND if absent, it SHALL seed scenario.LIVE with 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 scenarioId to the scenario.index Valkey 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-key State.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 assuming QueryState succeeds

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 a scenarioId that 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_2009 for backtests with different strategies, or ALL for 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 — hyp alone identifies a category, not a world