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,
)
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"
)
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.
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.