A reproducible evaluation environment for financial AI agents

Run controlled, reproducible experiments on trading agents in a market with realistic execution - not a backtest over historical prices. A deterministic market simulator with order-book execution, macro dynamics, causal ground truth and agent-native interfaces.

import tradefloor as tf

# you name the companies yourself
u = tf.Universe([
    tf.Instrument("AMZN", "technology",
        initial_price=397.17, eps=12.55,
        book_value_per_share=20.19,
        shares_outstanding=5.1e9),
    # GOOGL, MSFT, AAPL, NVDA alike
])
# or Universe.random(5, seed=7) to
# generate one: AAA, AAB, AAC...

e = tf.Engine(seed=776644, universe=u)
e.run_days(30)

bars = e.bars(grain="day")   # prices
truth = e.truth()            # and why
print(u.fingerprint, e.model_fingerprint)
index, 100 = day 0 close 80 to 125
day 0 day 30
AMZN99.1 GOOGL99.6 MSFT100.4 AAPL96.7 NVDA101.5 VIX14.89
DAY 1 / 30
VIX 14.89
CYCLE expansion
BIGGEST MOVE AAPL -3.31%
MEAN MISPRICING 0.0134
RETURN SPREAD 4.83 pts

Reproducible

A universe of companies, a macro state for day zero, and a seed. The same three inputs produce the same market on every supported platform, so a run is reproducible by anyone who has them.

Execution-aware

Agents trade through an order book and pay the spread, the queue and their own impact. Run the same seed without the orders and the difference is the cost of trading.

Inspectable

The simulator computed each price, so it can say why. The nine factor contributions sum to the move - an identity, not a fitted explanation. The residual is near 1e-16.

Agent-native

A Python object with an act method, a Gymnasium environment, or a model calling the simulator as a tool over MCP. The harness never asks what is inside.


WHAT IT ANSWERS

Two different questions

Historical backtest

How would this strategy have performed on this observed history?

tradefloor

How does this agent behave when I can control the market, isolate one variable and reproduce the experiment?

tradefloor complements historical backtesting rather than replacing it. It is a controlled environment for understanding how an agent behaves, before a strategy is validated against historical or live markets.

ONE EXPERIMENT

Ask, run, compare

1Ask a questionDoes my agent actually respond to deteriorating macro conditions, or is it trading momentum and finding out later?
2Run the experimentRun it against one market. Fork it the day before a rate shock, and change only the macro path on one branch.
3CompareDecisions, positions, execution, P&L, and the causal exposure behind each of them.
import tradefloor as tf

u = tf.Universe.random(40, seed=7)
e = tf.Engine(seed=776644, universe=u)
e.run_days(120)           # the day before

calm, shocked = tf.branch(e, 2,
    universe=u, seed=776644)
shocked.pin_macro(federal_funds_rate=0.05)

calm.run_days(20)          # same past,
shocked.run_days(20)       # one change

Same world. One changed variable. A measurable difference in behaviour. Both branches carry the same past bit for bit, so the difference between the futures is attributable to the input you changed.

REPRODUCIBILITY

Git for market experiments

A market here is an explicit configuration and a seed, so an experiment can be recorded, re-run, compared and branched without depending on a market nobody controls. It is the working practice of version control applied to an experiment rather than to code: share the configuration, share the seed, re-run the experiment and get the same market back.

What a RunManifest pins

seedThe integer the whole market unrolls from.
universeEvery instrument, in roster order. The order is contractual.
macroDay-zero rates, inflation and the cycle.
scenarioThe macro path driven through it, if there was one.
strategyThe StrategySpec, carried in full and cited by fingerprint.
modelThe preset that priced it, by name, or custom-XXXXXXXX if modified.
fingerprints.inputsOne digest over the seed and all of the above. This is the experiment's identity.
result.digestWhat the market did. Re-running the inputs has to reproduce it.
runrecordreproducecomparefork

reproduce() refuses on a mismatch rather than returning a number that looks fine. A checkpoint branches a run at a decision point, so two hypotheses share one past instead of being statistically similar.

Same world. Same conditions. Different agent. Two agents run against the identical market path, so a difference between them is the agent and not a different roll of the market.

Same experiment. Different hypothesis. Change one component, re-run, and attribute the difference to the change. Determinism is what makes the attribution mean anything.

Fork without changing the past

checkpoint · day 100IDENTICAL MARKET HISTORY UP TO HERECHANGE ONE INPUT, HERE
fork Apolicy A
future A
fork Bpolicy B
future B
compare
1Run once, then mark the stateA fork captures the engine at a chosen day: every column, the order book, and the position of the generator that draws the next random number.
2Branch ittf.branch(engine, 2, ...) returns two engines identical to that state, in under a millisecond. Checkpoint is the slower form that replays the order log and outlives the process.
3Change exactly one inputThe agent, the policy, an order size, a pinned macro field. Everything else on both branches is already identical, so there is nothing else it could be.
4Let both run onEach branch carries its own copy of the generator, so the futures are independent from the fork onward and neither disturbs the other.
5CompareThe two runs shared one past exactly, not approximately. What differs between them is the input you changed.

This is a fork of an experiment, not of a repository. Both branches carry the same past bit for bit, so they do not start from similar conditions - they start from the same one, and the difference between the futures is attributable to the input you changed rather than to a different roll of the market.


GROUND TRUTH

Why the price moved

Every price came out of a computation, so the move decomposes into the nine contributions that produced it and the accounting closes on the realised move. A backtest cannot do this: nobody recorded why the market did what it did, so an attribution over historical prices is a model fitted after the fact.

Questions it makes answerable

  • Did the agent trade on momentum, the macro state, liquidity, or noise?
  • Did its stated reason match the state the market was actually in?
  • What happens to the result when one causal channel is removed?
  • Did two agents reach the same P&L for different reasons?

What it is not

Ground truth about this market, not about any real one. The decomposition is exact because the simulator did the arithmetic, which is also the reason it says nothing about why a real price moved.


STEP 01 - SEE IT

The seed fixes every random number

The same universe, macro state and seed give the same market on Linux, macOS and Windows, digit for digit. Every release does a check of this. It builds five wheel targets, runs one fixed simulation in each, and compares the digests. A disagreement stops the release. Determinism also makes a fork possible: checkpoint at day 100, then branch, and the two branches share one past. The two runs below are the repository's reference runs, and they have a different seed and a different roster.

GROUND TRUTH · DAY 1 expansion
ticker sector close daily return mispricing_s
AMZN technology 393.46 -0.93% -0.0086
GOOGL technology 123.40 -0.40% +0.0015
MSFT technology 407.63 +0.39% +0.0028
AAPL technology 544.10 -3.31% -0.0372
NVDA technology 161.47 +1.52% +0.0167
The nine factors that sum to every move

One row per instrument per tick. The nine contributions sum to the total log move. The residual is near 1e-16. An explanation here is therefore an identity, not a model that fits after the fact.

reversion momentum crowd_lean company_news order_flow_impact short_squeeze_effect random_noise circuit_breaker jump

Three of them are the model's own dynamics, not shocks. The answer can therefore be "nothing happened, the price drifted back toward fair value". That is the correct answer most of the time.

STEP 02 - BUILD A UNIVERSE

Choose who exists

A universe is the roster and its fundamentals. The generator fills twelve sectors round-robin, so a generated roster is more balanced than any real index. Set one up here and take the code.

Companies40
Universe seed7
Roster shape
Sector mix
technology 4
financial services 4
healthcare 4
energy 4
consumer discretionary 3
consumer staples 3
industrials 3
import tradefloor as tf

u = tf.Universe.random(40, seed=7)

len(u)            # 40
u.fingerprint     # sha256, order included
u[0].ticker, u[0].sector
# heaviest sectors: technology 4, financial_services 4

macro = tf.Macro(federal_funds_rate=0.025,
                 corporate_bond_yield=0.052, vix=16.0)

e = tf.Engine(seed=42, universe=u, macro_state=macro)
e.run_days(252)   # one trading year
Roster order is contractual

Reorder the roster and you get a different market from the same seed. Sort your tickers alphabetically upstream and you change the world by accident. Do a check with universe.fingerprint.

STEP 03 - RUN YOUR STRATEGY

Write the strategy down

A StrategySpec is JSON with a sha256, so someone else can check your result with the package and nothing else of yours. Compose one here and take it.

Signal
Concentration, top_k5
Participation cap0.02
Cadence
{
  "spec_version": 1,
  "signal": {
    "kind": "momentum",
    "lookback_days": 1
  },
  "portfolio": {
    "gross": 1,
    "top_k": 5
  },
  "execution": {
    "cadence": "step",
    "max_participation": 0.02
  },
  "seed": null
}
spec = tf.StrategySpec.momentum(
    lookback_days=1.0,
    top_k=5)

scores = tf.evaluate(
    {"mine": spec}, seed=7,
    universe=u, days=10)

scores["mine"].return_pct
scores["mine"].impact_bps
scores["mine"].strategy_fingerprint
# sha256 -- cite this

How an order fills

Price-time priority, one book per name, no slippage formula anywhere. Step through the engine's own reference program.

submit buy 175 market step 15 of 21
mm 105.00 140
mm 104.00 130
mm 103.00 105
spread 4.00 · mid 101.00 · last 103.00
mm 99.00 100
alice 99.00 50
carol 99.00 25
mm 98.00 110
mm 97.00 120
mm 96.00 130
mm 95.00 140
What this step tests

market buy sweeps several levels

BEST BID 99.00 held
BEST ASK 103.00 moved
SPREAD 4.00 moved
RESTING 7 / 3 moved
Fills
50 @ 101.00 110 @ 102.00 15 @ 103.00
175 filled at a VWAP of 101.80 against the 101.00 best offer resting when the order arrived: +79 bps
STEP 04 - DRIVE IT

Three ways to use it

Three modes of agent interaction: observe the market, trade inside it, or train against it. A model over MCP, a reinforcement-learning policy, or trading logic you wrote.

Agents over MCP

No simulator integration code. Claude or another model calls tradefloor through MCP, observes the market and submits trades as tool calls. Eleven tools, everything composed as data.

pip install "tradefloor[mcp]"

claude mcp add tradefloor \
  -- tradefloor-mcp

# then just ask it a question

Reinforcement learning

Passes gymnasium's env_checker. Actions are target weights in [-1, 1]. Reward is measured after the market moves, so it already carries the cost of the agent's own footprint.

import tradefloor.gym as ptg

env = ptg.TradingEnv(universe=u,
    seed=42, days=20)
obs, info = env.reset(seed=42)
obs, r, done, trunc, i = env.step(a)

Your own trade logic

A TWAP, a VWAP, an iceberg, or your own logic. Any object with an act method will do. The observation is narrow on purpose. It exposes only information available to a trader inside the simulated market.

class Mine:
    def act(self, obs):
        return {"AAA": 0.2}

tf.tca.analyse(Mine(), seed=42,
    universe=u, days=5)

Ten agents, one market each

Hand evaluate a dict of ten agents and each one runs in its own copy of an identical world: same seed, same roster, same macro path. Your algorithm, a trained policy, an LLM agent and the five baselines land in one table.

Then rank repeats that across seeds and separates them with a paired sign test. The Oracle reads the true mispricing and says so. Read it as a reference point, not a ceiling: it spends the same gross on a naive rule, and mean-reversion beats it on 5 of the 12 markets.

SHIPPED · EACH IN ITS OWN COPY

Ten agents, ten identical markets. Each agent runs in its own copy of the same deterministic market, so nobody eats another agent's depth and the comparison stays clean and reproducible. A single shared market with many agents is the extension described below.

agents = tf.reference_agents(seed=3)
agents["mine"] = Mine()
agents["policy"] = TrainedPolicy(net)

scores = tf.evaluate(
    agents, seed=7, universe=u, days=10)

ranking = tf.rank(
    lambda: dict(agents), seeds=range(12),
    universe=u, days=10, workers=4)
COMING UP

Coming next: shared markets

single-agent evaluationcompetitive agentsemergent market behaviour

Several agents against one shared order book, competing for the same liquidity and moving each other's execution. Not yet shipped. Today every agent in an evaluation runs in its own copy of an identical market, which is what keeps the comparison clean: Agents is what runs now.

STEP 05 - TRUST THE RESULT

What it reproduces

Real markets have habits. Prices jump by a typical amount. A calm week often comes after a calm week. Names move together in a panic. This page measures fourteen of those habits, each against the range that real equities hold. For the shipped preset, the thirty-seed median of each of the fourteen falls inside its range at 252 days, on the forty-name sector-balanced roster the certification was measured on and again on a held-out sixty-name universe. A median is not every seed: check the intervals before relying on one. There is no single score, because one number travels further than its caveats.

annualised_vol_pct 28.3103
excess_kurtosis 10.0043
return_acf1 0.0114
abs_return_acf1 0.0769
abs_return_acf5 0.0305
abs_return_acf20 0.0096
cross_sectional_corr 0.2616
volume_abs_return_corr 0.5108
leverage_effect -0.0258
volume_change_acf1 -0.2794
corr_asymmetry -0.0018
corr_asymmetry_lagged -0.0327
sector_excess_corr 0.2081
corr_persistence_acf1 0.1771
band floor band ceiling
Will my result hold up?
How long is your run?252 days, about a year
What is in it?
Yes.

This is the exact roster the certification was measured on: twelve sectors, filled round-robin, forty names. The thirty-seed median of all fourteen landed in range on it.

Run a crisis through it

A scenario drives the macro path. Without one, the economy finds its own path. Pick a scenario and see what the market did.

import tradefloor as tf

u = tf.Universe.random(40, seed=7)

shock = tf.Scenario.rate_shock(
    start=0.045, end=0.016, over=13)

tf.evaluate(agents, seed=7,
    universe=u, days=83,
    scenario=shock)

# leave corporate_bond_yield free:
# pinning it severs the channel
# the shock is travelling down.
Rate scare, 2021 2021-05-28
4270 4001 3731 -4.2% in 13 sessions 2021-02-01 2021-05-28
S&P 500 4204 VIX 16.76

Good results here do not predict real returns

The price process comes from a known model. A strategy that fits the structure of that model looks excellent and teaches you nothing. A strategy that fails here tells you more, because it broke against a live order book under honest impact costs.

One market is one sample

On the twelve-market grid the docs measure, mean-reversion pools at +0.783 capture and momentum at +0.259, so mean-reversion leads by three times. Paired across the same twelve markets it wins 9 to 3, at p = 0.15. One seed picks the pooled leader eight times in twelve, and what a seed says momentum is worth runs from -0.503 to +0.909.


WHO IT IS FOR

Built for financial AI research

LLM agents

Evaluate agents that reason, call tools, read state and make a sequence of trading decisions. The MCP server exposes the simulator as eleven tools.

Reinforcement learning

A Gymnasium environment with explicit rewards, execution costs and market state, and as many independent episodes as you want.

Quant and execution

Queue position, partial fills, honest impact, and macro scenarios to run a strategy through. Test under controlled dynamics rather than one historical path.

Benchmark builders

A manifest that names the package version, preset, seed, universe, macro, scenario and strategy, and a reproduce() that refuses on mismatch.

Start here