Skip to the page
GUIDES/FORK A MARKET

Fork a market

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. This guide forks a bare market, saves the point it forked from, and then runs a counterfactual with an agent in it. The signatures are in the reference, Forks and counterfactuals.

Every code block on this page runs on its own.

Two futures from one past

tf.Checkpoint.of marks a point in a running engine, with its seed and universe, and branch builds independent engines at that point. Change one input in one copy and run both on.

import struct

import tradefloor as tf

def last_price(engine, ticker):
    # prices() is each last price as little-endian float64 bytes, in roster order
    prices = struct.unpack(f"<{len(engine.tickers)}d", engine.prices())
    return prices[engine.index_of(ticker)]

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

mark = tf.Checkpoint.of(engine, universe=universe, seed=42)
calm, hiked = mark.branch(2)                     # two engines at day 60
print("identical at the fork:", calm.state_hash() == hiked.state_hash())

hiked.pin_macro(corporate_bond_yield=0.09)       # one change, in one copy
calm.run_days(20)
hiked.run_days(20)
print(f"AAA after 20 more days: {last_price(calm, 'AAA'):.2f} calm, "
      f"{last_price(hiked, 'AAA'):.2f} with credit at 9%")

text = mark.to_json()                            # outlives the process
again = tf.Checkpoint.from_json(text).resume()
print("resumed from text:", again.state_hash() == engine.state_hash())
identical at the fork: True
AAA after 20 more days: 471.65 calm, 327.47 with credit at 9%
resumed from text: True

The state hash covers prices, every column, the random generators and the economy, so equal hashes mean the two copies are the same market. Pinning the corporate bond yield at 9% is the only difference between the two futures, and AAA ends about 31% lower under it.

Branch or checkpoint

There are two ways to copy a market, and they trade speed for durability:

CostSurvives the process
tf.branch(engine, 2)under 1 msno
Checkpoint.resume() or Checkpoint.branch()seconds, growing with the order logyes

tf.branch copies the engine's state directly: every column and each random stream's position. A checkpoint stores the order log and rebuilds the engine by replaying it, so a checkpoint is what to save and cite in a published result. A checkpoint records the universe's fingerprint and refuses to load against a universe that changed, because two universes can share tickers and differ in fundamentals. It also refuses to load on a build whose arithmetic differs from the one that wrote it, and the error names both builds. That digest changed in 0.8.0, so a checkpoint written by 0.7.x loads only on 0.7.x.

A counterfactual with an agent

A World holds the market, one agent, its portfolio and the macro path. Forking a world copies all of them, so the same agent, holding the same position, meets two markets that differ in one input. Here the agent buys 300 shares of AAA on the first step and holds them, and one arm gets the packaged liquidity_crisis scenario.

import tradefloor as tf

class BuyOnce:
    # buy 300 AAA on the run's first step, then hold
    def act(self, obs):
        return {"AAA": 300} if obs.step == 0 else None

universe = tf.Universe.random(24, seed=7)
world = tf.World(seed=7, universe=universe, agent=BuyOnce())
world.run(50)                                    # 50 shared days

control, stress = world.fork("control", "stress")
stress.apply(tf.Scenario.load("liquidity_crisis"))
started = tf.agree(control, stress)
print("identical at the fork:", started.identical)

control.run(80)
stress.run(80)
result = tf.compare(control, stress, agreement=started)
first = result.as_dict()["divergence"]
print("prices first differ on day", first["prices"] // first["steps_per_day"])
print(result.render())
identical at the fork: True
prices first differ on day 100
                                    control           stress
  ----------------------------------------------------------
  --- agent behaviour ---
  final gross exposure                0.10x            0.08x
  steps it traded on                      0                0
  orders sent                             0                0
  unusable responses                      0                0
  turnover                               $0               $0
  --- execution ---
  trades filled                           0                0
  partial fills                           0                0
  refused trades                          0                0
  cost against arrival                   $0               $0
  the same, in bps                +0.00 bps        +0.00 bps
  --- portfolio ---
  cash                             $852,894         $852,894
  final value                      $952,768         $925,182
  P&L since the fork               $-54,385         $-81,971
  return since the fork              -5.40%           -8.14%
  max drawdown since                  6.28%           10.19%
  P&L since inception              $-47,232         $-74,818
  return since inception             -4.72%           -7.48%

Reading the comparison

agree checks that the two arms match in engine state, order books, portfolio, agent state and random-stream positions, and compare with agreement= states that in the result. The scenario's shocks start on its own day 50, and apply counts that from the day it is applied, so the arms run identically for 50 more days and first differ on day 100. compare finds that first step separately for the macro path, the agent's decisions, its orders, the prices and the portfolios.

This agent never trades after the fork, so both arms show no orders and the same cash, and the whole difference in value is the scenario's effect on the 300 shares it holds. An agent that reacts to prices would show its decisions diverging too. An external agent, such as a model behind an LLM adapter, runs in a world the same way, and world.manifest() records the run as a manifest, as in Record and replay.

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

Next steps

  • Scenarios builds what World.apply takes.
  • World lists every method of a world, including several agents in one market.