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:
| Cost | Survives the process | |
|---|---|---|
tf.branch(engine, 2) | under 1 ms | no |
Checkpoint.resume() or Checkpoint.branch() | seconds, growing with the order log | yes |
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.