Skip to the page
API REFERENCE/SCENARIOS

Scenarios

Reference for Scenario, Intervention, Firing, the target registry and run_scenario.

Using a scenario

A scenario is a named set of changes to a market, with the assumptions behind them stated. Apply one to see how an agent behaves under a change you control, against the same market without it.

import tradefloor as tf

scenario = tf.Scenario.load("rate_shock")
print(scenario.describe())           # shocks, then the author's assumptions

u = tf.Universe.random(20, seed=101)
scores = tf.evaluate(tf.reference_agents(seed=3), seed=7, universe=u,
                     days=60, scenario=scenario)

Seven scenarios ship with the package: curve_shock, geopolitical_conflict, liquidity_crisis, oil_price_spike, policy_regime_shift, rate_shock and recession. Each file keeps what happened (the shocks) apart from what the author assumes it caused (the transmission), and describe() prints the two under separate headings. tradefloor does not decide what a war or an oil shock does to a market. The file states it, and the engine runs the market with and without it.

recession and liquidity_crisis were calibrated against 2008 and March 2020. On pt-v20, paired against the same seeds without the scenario, the recession takes the index down 44.7% at 120 sessions, against 45% from Lehman to March 2009.

A scenario applies to an evaluation, a run loop, or one arm of a fork. Its days count from the day it is first applied, so on a branch at: 50 means fifty days after the fork.

Writing one

A scenario written in Python and the same one written in YAML give the same document and the same fingerprint, a sha256 of the resolved scenario that a RunManifest records.

crisis = (tf.Scenario(name="my_liquidity_crisis")
          .shock("market.liquidity", operation="multiply", value=0.40,
                 at=20, duration=25)
          .assume("macro.corporate_yield", operation="add", value=0.005,
                  at=20, duration=25))

path = tf.Scenario().hold(vix=15.0).ramp("vix", start=48.0, end=22.0,
                                         over=45, begin=60)

Operations are set, add and multiply; shapes are impulse, permanent, hold and ramp. An impulse writes the value once and lets the model carry it from there. A pin (hold, ramp, step) gives a whole path for one macro field.

The targets

A scenario can move fifteen targets: macro.corporate_yield, macro.policy_rate, macro.treasury_2y, macro.treasury_10y, macro.vix, macro.inflation, macro.growth, macro.unemployment, macro.cycle, macro.qe_pe_boost, macro.fear_greed, market.liquidity, market.earnings, commodity.oil and policy.tariff_rate. A name that looks like a target and is not one, such as market.volatility or execution.market_impact, is refused with an error that names what to use instead. tradefloor scenario targets prints each target with a note.

The command line

The scenario commands read scenario files and never run a market.

tradefloor scenario list
tradefloor scenario validate my.yml
tradefloor scenario show oil_price_spike
tradefloor scenario diff rate_shock recession
tradefloor scenario targets

Scenario

A macro path, meaning day-by-day values for economy-wide series such as VIX, plus a set of interventions. The scenario is applied one day at a time. It is available at the top level as tradefloor.Scenario.

Scenario(
    label: str = "",
    *,
    name: str | None = None,
    description: str = "",
    interventions: Sequence[Intervention] = (),
    shocks: Sequence[Intervention] = (),
    transmission: Sequence[Intervention] = (),
    vix_sets_variance: bool = False,
)
hold(**fields)Pin fields to a constant from day zero. Returns self. scenario.FIELDS lists the fifteen fields you can pin, and any other name raises ValidationError. Fourteen are macro series, including treasury_yield_2y and treasury_yield_10y from 0.8.5, the yields the simulated rate indices read. The fifteenth, epicentre, names the sector that carries the next crisis episode, or "none" for a crisis with no such sector, as in hold(vix=65.0, epicentre="financial_services"). A misspelled sector raises an error in the call that names it. A pinned epicentre uses no random draw. It has no effect on a preset with crisis_epicentre_extra at 0.0, which is every preset before pt-v19.
ramp(field, *, start, end, over, begin=0)Move a field in a straight line from start to end over the given days. Before begin the field stays at start, and afterward it stays at end. over < 1 or begin < 0 raises.
step(field, *, before, after, at)Jump a field from before to after on day at. Several pins on one field run as back-to-back segments, and each must start later than the one before or the call raises.
shock(target, *, operation="multiply", value=None, at=0, duration=None, shape=None)Add an intervention for an event from outside the market. Returns self.
assume(...)Works like shock and takes the same arguments, but files the intervention as assumed transmission. describe() prints the two under separate headings.
intervene(intervention)Add one Intervention you have already built. The order you add them in is kept, and it decides which of two same-day interventions goes first.
apply(engine, day)Apply pins, then shocks, then transmission to one day of one run. Returns the list of Firings. Days count from when the scenario is first applied.
at(day) / table(days)The pinned values for one day, or for the whole path, so you can check them before running.
load(name)Classmethod. Loads a shipped scenario by name: curve_shock, geopolitical_conflict, liquidity_crisis, oil_price_spike, policy_regime_shift, rate_shock, recession.
from_yaml(source) / from_json(text)Classmethods. Parse a scenario document. Validation errors name the target and the operation.
rate_shock(*, start=0.025, end=0.05, over=30, begin=0, credit_spread=0.02)Constructor. Moves the policy rate and the corporate yield together, keeping them credit_spread apart. Raises if called on an instance.
vix_shock(*, calm=15.0, peak=45.0, at=10, over=20)Constructor. A VIX spike that steps up from calm to peak on day at, then ramps back to calm over the given days.
document() / fingerprintThe resolved document in its standard form, and the sha256 hash of it. The YAML and Python forms of one experiment have the same fingerprint. vix_sets_variance appears in the document only when it is on, so a scenario written before 0.8.0 keeps its fingerprint.
describe()The scenario as text, shocks above assumptions.
log / firing_table()The last run's record of each firing, with the values it saw.
without_interventions() / copy()The same macro path with interventions removed, and an independent copy for driving two runs at once.
name / description / sourceThe scenario's name, its description, and the file name it was read from, if any.
vix_sets_varianceWhether a VIX that this scenario forces also sets the market's volatility. It is read-only, and False unless the constructor, a YAML scenario: block or a to_json document turned it on. See below.

A forced VIX that sets volatility

A scenario forces the VIX when it sets it with a pin or an intervention. By default a forced VIX reaches volatility the same way the model's own VIX does. Each close moves the variance of the market factor, the part of every price move that all companies share, one step toward the level that VIX implies. The fast component closes about 2% of the gap each trading day, a half-life of about 33 trading days. So a VIX that jumps from 15 to 80 in three weeks shows up in prices weeks later.

Scenario(vix_sets_variance=True) changes that. On every trading day that the scenario forces the VIX, with a pin or an intervention on macro.vix, the close sets both variance components to the level the variance law (the model's rule for how variance moves) reverts to at that VIX, clamped as usual. The next day trades at that level. While the VIX is forced, the day's own market shock does not feed into the factor's variance. Volatility for each company and each sector, and jumps, still cluster on their own shocks. When the scenario stops forcing the VIX, the variance law carries on freely from that level with no jump. When the real 2020 VIX is replayed on pt-v19, the model's worst month comes 2 trading days after the real one with the switch on, and 20 trading days after it with the switch off.

In YAML the switch is vix_sets_variance: true in the scenario: block. In a to_json document it is "vix_sets_variance": true beside "path". It must be a boolean, so a quoted "true" is refused. A scenario that turns the switch on but never forces the VIX raises ScenarioValidationError when it is applied, because the switch would do nothing. With the switch off, every earlier scenario runs, serializes and fingerprints as before. At the engine level the switch is Engine.pin_macro(vix=..., vix_sets_variance=True), which marks the current day's close. Engine.vix_sets_variance_pending reports the mark until that close clears it.

Intervention

One change to one target at one time. An Intervention is immutable and is checked when you build it, and it is available at the top level as tradefloor.Intervention.

Intervention(
    target: str,
    *,
    operation: str = "multiply",   # "set" | "add" | "multiply"
    value: Any = None,
    at: int = 0,                   # days from scenario start
    duration: int | None = None,
    shape: str | None = None,      # "impulse" | "permanent" | "hold" | "ramp"
    role: str = "shock",           # "shock" | "transmission"
)
as_dict() / from_dict()The serialized form, with every default filled in, so the fingerprint is the same whether or not you typed the defaults.
active_on(day) / last_dayWhether the intervention acts on a day, and the last day of its window.
describe()One line, in the units the target declares.
ScenarioValidationErrorRaised for a malformed scenario document or an unknown target. A subclass of ValidationError.
FiringOne application of one intervention, with its target, its day, the value read and the value written. as_dict() returns a dict and str() returns readable text.

The target registry

tradefloor.TARGETS maps the fifteen target names to Target objects. macro.treasury_2y, macro.treasury_10y and market.earnings joined in 0.8.5. tradefloor.UNSUPPORTED_TARGETS maps each refused name to what to use instead. The targets, above, lists the names, and tradefloor scenario targets prints each with a note.

Target.read(engine) / write(engine, value)How the target reads and writes the engine.
Target.check(operation, value)The checks run when an intervention is built. Rates must be fractions in [-0.05, 0.50], prices must be positive, and a cycle phase must be given by name.
Target.show(value)The value in the target's own units, for messages and audit trails.

run_scenario

tf.run_scenario(
    scenario: Scenario,
    *,
    seed: int,
    universe: Sequence[Instrument],
    days: int,
    macro: Macro | None = None,
    ticks_per_day: int = 390,
    start: tuple[int, int, int] = (9, 30, 3),
    record: bool = False,
    model: str | ModelParams | None = None,
) -> Engine

Runs a market under the scenario and returns the finished engine. The scenario is applied at the start of each day, so the path already applies on day zero. days < 1 raises ValidationError. tf.evaluate, tf.rank, tf.tca.analyse and tf.run_many take the same scenario= keyword.

See also

Forks and counterfactuals for World.apply. Agents and evaluation for the evaluation entry points that accept a scenario.