Skip to content

The arena ​

tra arena replays every row over the same archive and settles every idea the same way, so a policy is judged beside rules that cost nothing and beside chance, net of what the desk pays.

One command ​

bash
tra data bars BTCUSDT 2025-07-01 --to 2025-09-30 --venue bybit
tra data bars ETHUSDT 2025-07-01 --to 2025-09-30 --venue bybit
tra arena --instruments BTCUSDT,ETHUSDT --from 2025-07-01 --to 2025-09-30 \
  --spread-bps 1 --slippage-bps 0

Fees come from the venue's taker-taker channel in envelope.toml (--channel names another). Spread and slippage are yours to declare: the arena refuses to run without them rather than assume a number for you. The same settings can live in an arena.toml:

toml
[arena]
instruments = ["BTCUSDT", "ETHUSDT"]
from = 2025-07-01
to = 2025-09-30
spread_bps = 1
slippage_bps = 0
bash
tra arena --config arena.toml

How a row is replayed ​

  • A decision is taken at every close of the decision interval (one hour by default), with the four-hour view beside it and BTCUSDT as context.
  • A view holds only bars that had closed and derivatives values published by then. A bar from after the decision is refused, and the run fails instead of reporting.
  • An idea enters at the open of the first traded minute at or after its decision, never at the price it was decided on.
  • Each row holds at most one idea per market at a time.
  • Ideas are settled on one-minute bars: take, stop or time limit, whichever comes first. A minute that reaches both levels counts as the stop; a level is filled at the level.
  • A minute nobody saw inside the hold, or a funding settlement the archive has no rate for, leaves the idea unsettled. The report counts every unsettled idea by its reason.

Costs ​

ComponentWhere it comes fromCertainty
Feesthe channel in envelope.toml, per sidedeclared
Spread--spread-bps, the full quoted spread per round tripdeclared
Slippage--slippage-bps, per round tripdeclared
Fundingthe archive's settlements over the hold, by sidemeasured

--funding none is for a market that pays no funding, such as spot. Every idea in ideas.jsonl carries each component and its certainty.

Rows ​

RowDefinition
buy_holdlong at the window's first decision, held to its end
momentumlong when mom_z_24 is at least 1, short when it is at most -1; stop 1.5 ATR, take 3 ATR, held 24 bars
fadethe opposite side of momentum, on the same triggers and exits
randoma seeded side at the times momentum fires, with its exits: the null every row is compared with
forecastthe expected-value decision: a calibrated probability of the take before the stop, fitted walk-forward on the archive (below)
pathsthe side whose value over the instrument's own candles drawn forward, at the baselines' exits and net of the round trip, is above zero; nothing fitted (the paths)
v1_recordthe decisions the desk sent, from runs/cycles, held for --v1-hold; a live record, not a replay

The baselines are fixed and versioned, not tuned; changing one would change every comparison, so the version is written into every result. A package adds its own row by registering a callable that returns a policy under the trading_research_agents.policies entry point group.

The forecast row ​

forecast is the decision being built to replace the vetoes of the live cycle: a probability that the take is reached before the stop, calibrated on outcomes, and one expected value net of the round trip. It runs only in the arena until the arena shows it is better.

StepWhat it does
labelevery past decision, each side, settled with the baselines' exits (stop 1.5 ATR, take 3 ATR, 24 bars): 1 when the take came first, 0 for a stop or for time running out
fitevery 7 days, on the decisions of the last 60 days whose ideas had settled by then: one logistic model per side, on the older 70 %
featuresfrom a fixed, versioned list of price, path and order-flow figures, those present in at least 95 % of the settled decisions
calibrateon the newer 30 %: Platt or isotonic, whichever scored better on held-out halves
decidep x take - (1 - p) x stop - round trip for each side; the better side if it is above zero

A decision is never fitted on an outcome it could not have known: an idea that had not settled by the refit is left out, whatever the archive says happened later. Until 200 settled decisions per side exist, the row looks again each day and makes no idea. An idea that runs out of time counts as a stop, so the value it reads is conservative. The row's version is written into every result.

On BTCUSDT and ETHUSDT, Bybit, 2025-07-05 to 2025-07-13, it first fitted on 2025-07-10, on 462 settled decisions, and made 14 ideas at a mean of +87 bps net: a sample far too small to say anything, which is the verdict the arena gives. What it shows is the pipeline end to end on real data.

Reading the table ​

For every row: ideas, independent bets (ideas on a market that do not overlap an earlier one), the mean and median net in basis points, the hit rate, the mean R (net over the stop distance plus cost), the sum, and the Sharpe of daily net (365 days). The last columns compare the row's mean net with random's: the difference and its 95 % interval, resampling whole days 4,000 times with seed 7, because one day's tape moves every idea on it together. Then the verdict:

VerdictMeaning
beats randomthe whole interval lies above zero
worse than randomthe whole interval lies below zero
no clear difference from randomthe interval crosses zero
sample too smallfewer than 30 independent bets or fewer than 10 days
contaminatedthe policy's knowledge cutoff falls after the window starts

A policy that exposes a score also gets its per-date rank IC against the next bar's return, with a t statistic. Coverage closes the report: minutes per market, the minutes not seen, and every feature that was absent, with the reason.

What a run writes ​

text
runs/arena/<id>/arena.json    configuration, archive hashes, package version, rows, coverage
runs/arena/<id>/ideas.jsonl   every idea, its settlement and each cost component
runs/arena/<id>/run.json      when it ran

<id> is a hash of the configuration and of the archive files read, so a run on a changed archive never overwrites an earlier one. The same archive, configuration and seed give the same arena.json, byte for byte; the time lives in run.json only. --fit-days and --test-days add walk-forward test windows, each after an embargo of the longest hold, to the report.

Measured on BTCUSDT and ETHUSDT, Bybit, 2025-07-05 to 2025-07-13, on one machine: the run took 4 seconds.

Released under the MIT License.