Skip to the page
API REFERENCE/FORKS AND COUNTERFACTUALS

Forks and counterfactuals

Reference for World, agree, compare, their result types, and the tools for checkpoints and replay. A counterfactual runs one agent in two copies of a market that differ in one variable.

Checkpoints and forks

A fork copies a running market so you can run two futures from one past. Everything before the fork is identical in both copies, bit for bit, so a difference that appears afterwards comes from the input you changed.

import tradefloor as tf

universe = tf.Universe.random(40, seed=7)
engine = tf.Engine(seed=42, universe=universe)
engine.run_days(60)

mark = tf.Checkpoint.of(engine, universe=universe, seed=42)
text = mark.to_json()                        # outlives the process
calm, hiked = mark.branch(2)                 # two engines, identical up to day 60
hiked.pin_macro(corporate_bond_yield=0.09)   # they diverge only from here
calm.run_days(20)
hiked.run_days(20)
CostSurvives the process
tf.branch(engine, 2)under 1 msno
Checkpoint.resume()seconds, growing with the order logyes

branch copies the engine state, every column and each random stream's position. Checkpoint stores the order log and rebuilds the engine by replaying it, so cite the checkpoint in a published result. A checkpoint records the roster's fingerprint and refuses to load against a roster that changed, because two rosters can share tickers and differ in fundamentals. Two futures from one past runs a fork with its output.

Counterfactuals

A World holds the market, one agent, its portfolio and the macro path. Forking a world copies all of them, so the same agent meets two markets that differ in one input:

class Mine:
    def act(self, obs):
        return {"AAA": 300} if obs.step == 0 else None

u = tf.Universe.random(24, seed=7)
world = tf.World(seed=7, universe=u, agent=Mine())
world.run(50)                                    # a shared past

control, stress = world.fork("control", "stress")
stress.apply(tf.Scenario.load("liquidity_crisis"))
started = tf.agree(control, stress)              # identical, bit for bit

control.run(80)
stress.run(80)
result = tf.compare(control, stress, agreement=started)
print(result.render())

agree checks that two arms match in engine state, order books, portfolio, agent state and stream positions. compare finds the first step where they differ, separately for the macro path, the agent's decisions, its orders, the prices and the portfolios, and summarizes both arms. world.manifest() records the run like any other, and an external agent runs here the same way, through an adapter. A counterfactual with an agent runs this example with its output and reads the comparison.

The answer describes this simulated market under the one change you made. A claim about a real market needs evidence from a real market.

World

A market, the agent or agents trading in it, and the macro path they run under. It is available at the top level as tradefloor.World.

World(
    *,
    seed: int,
    universe: Sequence[Instrument],
    agent: Any = None,
    agents: dict[str, Any] | None = None,
    pins: dict[str, Any] | None = None,
    macro: Macro | None = None,
    cash: float = 1_000_000.0,
    max_leverage: float | None = 2.0,
    steps_per_day: int = 6,
    ticks_per_step: int = 65,
    start: tuple[int, int, int] = (9, 30, 3),
    model: str | ModelParams | None = None,
    label: str = "",
    on_refusal: str = "raise",
)
run(days=1)Advance the world by the given number of days. Each day runs steps_per_day decision steps, the points where the agent decides, and each step runs ticks_per_step ticks. Returns self.
act(obs) ordersAn agent's act(obs) returns a dict of ticker to shares, positive to buy. A number is a market order, which the agent's portfolio fills through Portfolio.execute. From 0.8.5 a value can instead be tf.Limit(quantity, price), which takes what the book holds at price or better and leaves the rest waiting, or tf.Cancel(), which cancels the agent's waiting orders on that ticker. A new Limit on a ticker replaces the order the agent has waiting there. A Limit on a rate index is refused and recorded in the row's refused list. From 0.8.5 evaluate and rank take them too, and tca.analyse refuses a Limit.
book_fillsNew in 0.8.5. A waiting order can fill during a session, while no agent is being asked. The fill reaches the agent's portfolio after that session through Portfolio.sync, and the step's trace row lists it under book_fills, per agent in a cohort. A row has book_fills only when a waiting order filled.
agents={label: agent}Runs several agents in this one engine in place of agent, each in its own portfolio with its label as the portfolio's owner. Every agent starts with the same cash, in its own account: cash is one number, and cash={'a': 1e6, 'b': 5e6} is refused. max_leverage is one setting too, applied to each agent's book on its own. They are asked in label order, and their fills are summed and reach the market once, as the session's fills. With Engine.book_live False, as on pt-v1 through pt-v19, agents in one step meet the same book and take no levels from each other. On pt-v20, the default, book_shared is on, so label order is also arrival order: a later agent meets the book an earlier one left and can fill against its waiting limit order. Two identical buyers of 10% of a day's volume paid 19.5 to 30.7 bp apart on seeds 1 to 10, the later label paying more, so rotate the labels across runs when you compare agents in one market.
on_refusal="raise""raise" ends the run when an agent cannot produce a decision. "skip" records the refusal, trades nothing that step and carries on.
tf.externalities(world, days=1)Measures what each agent in a cohort did to the others, without advancing world. It forks the cohort once per agent plus once, freezes one agent in each arm, runs every arm for days and returns an Externality. matrix[a][b] is the change in b's P&L when a stops trading. From 0.8.5, levels[a][b] is the change in b's execution cost against each step's opening mid when a stops trading, positive when a made b's fills dearer, and live records Engine.book_live. levels is zero off its diagonal wherever the book is not shared. A frozen agent's waiting orders are canceled in its arm.
fork(*labels)Returns one World per label. Each copy, called an arm, matches this world at the fork in its engine, agent, portfolio and random generator position. Each arm records the step it was forked at.
apply(scenario)Apply a Scenario document to this arm, shifted onto this world's own day numbering. Returns self.
intervene(**fields)Change macro fields in this arm on the current day. The change is recorded next to the day-zero pins, so the world's scenario can still be rebuilt.
checkpoint(label="")A Checkpoint of the run so far, which you can save and resume later.
manifest(*, strategy=None, label="")A RunManifest, the record of a run's inputs, so others can cite and replay the counterfactual.
replay()Re-execute the order log into a fresh Engine and return it.
summary(*, since=None) / net_worthA summary of results, optionally from a given step onward, and the portfolio's current value.
day / step / digestThe next day to run, the next decision step counted across the whole run, and a hash of the run state.
scenario / firings / order_logThe reconstructed Scenario for this world, the interventions that fired, and every input that reached the engine.

agree and compare

tf.agree(a: World, b: World) -> Agreement

tf.compare(
    control: World,
    treatment: World,
    *,
    agreement: Agreement | None = None,
) -> Comparison
agreeChecks that two arms are identical in engine state, order books, portfolio, agent state and random generator position. Call it at the fork, before the arms run on.
comparePuts two forked worlds side by side and finds, for each series, the first step at which the two arms differed. The series are the macro values, the decision, the orders, the prices and the portfolios. Arms with different steps_per_day are refused. Pass agreement= so the published comparison states that the arms started identical.
Agreementidentical: bool, differences: list[str], as_dict(), render(width=22).
ComparisonPer-series Divergences and both arms' summaries. as_dict(), render(width=24).
DivergenceWhere one series came apart: the step, the day and the values on each side. as_dict(), render().

Checkpointing and replay

Checkpoint.of(engine, *, universe: Sequence[Instrument],
              seed: int, macro: Macro | None = None,
              label: str = "") -> Checkpoint

tf.branch(engine, count: int = 2, *,
    universe: Sequence[Instrument] | None = None,
    seed: int | None = None,
    macro: Macro | None = None) -> list[Engine]

tf.replay(log, *, seed: int,
    universe: Sequence[Instrument],
    macro: Macro | None = None,
    model: str | ModelParams | None = None,
    until: int | None = None) -> Engine
Checkpoint.ofCaptures an engine's history. universe and seed are required because an engine is built from them and keeps neither.
Checkpoint.resume / branch(count=2)Re-execute into one engine, or into count independent engines, at the saved point. Each build has an era digest that identifies its arithmetic. A checkpoint written by a build with a different era digest is refused, and the error names both builds. The digest changed in 0.8.0, so a checkpoint written by 0.7.x is refused even when it names its preset. Resume it under the version that wrote it.
Checkpoint.to_json / from_jsonThe serialized form, which you can save and load in a later process.
Checkpoint.fingerprintA sha256 hash of the checkpoint, serialized in its standard form. RunManifest.of(..., derived_from=checkpoint) records it as the checkpoint the run came from.
tf.branchCopies a running engine in constant time through Engine.fork. The copy includes every column, the random generator position, the news the engine generated that day, the tape and the order log.
tf.replayRe-executes a recorded order log. seed and universe are not in the log, so you must pass them.

See also

Scenarios builds what World.apply takes, and RunManifest documents what a manifest carries.