# trading-research-agents > An open-source Python package and command line (tra): autonomous trading agents with deterministic execution. Language model agents read the market and propose a trade; fixed, tested rules compute every figure, price the round trip, size the position and decide whether an order goes out. It runs in a sandbox by default, trades linear perpetuals today on any exchange ccxt supports, with more market types on the way, and works with Anthropic, OpenAI, Google Gemini, Groq, DeepSeek, OpenRouter, Ollama and other model providers. MIT licensed. Current release: 0.10.0. Install: uv tool install 'trading-research-agents[ccxt,openai,anthropic,google,data]' Command: tra. Python 3.11 or newer. Website: https://www.bijectivelabs.dev/open-source Source: https://github.com/BijectiveLabs/trading-research-agents Package: https://pypi.org/project/trading-research-agents/ # trading-research-agents **Autonomous trading agents with deterministic execution. The agents propose; the system decides.** Language models are good at reading a market and saying what they think. They are bad at arithmetic, they don't know what a trade costs, and they will happily overrule a limit you set. This project uses them for the first part only. The system itself, fixed rules with no model involved, fetches the data and works out every figure. The agents read those figures and give an opinion. Then the system prices the trade, applies your limits, sizes it, and either writes it down or sends one order with the stop attached. Every decision and every refusal is kept on disk with its reason, so you can check afterwards what happened and why. It is an early release, so expect settings to change between versions. What works today: linear perpetuals, spot pairs (stock perpetuals and tokenized stocks among them) and prediction market outcomes through one order path, a universe of perpetuals found and measured by the system, a screener that picks where to look, the full autonomous cycle on live data, four analysts and up to three validators, stops and targets the analyst names inside bounds you set, the gates that decide, sizing from your own rules, real orders on a venue's practice environment behind a mandate you sign, a risk manager that can only make the desk more careful, and two auditors that read the records. The command is `tra`. Start with the [quick start](https://docs.bijectivelabs.dev/opensource/quick-start). ## What it solves | The usual problem with trading agents | What happens here | |---|---| | The model invents or miscalculates a number | Models never compute. Every figure comes from the system, and a figure a model cites is checked against its brief, value included. | | Nobody knows what a trade cost until it's done | The round trip (fees, spread, impact, funding, slippage) is priced before an order exists, and a trade whose claimed edge doesn't clear it is refused. | | The agent sizes its own position | Size comes from your conviction ladder and your risk limit. No model can make a trade bigger. | | A model "decides" to ignore your settings | Your declared thresholds decide. A model's opinion is recorded beside them, never instead of them. | | Something went wrong and there is no trace | Each cycle keeps its snapshot, briefs, answers, the intent and what the venue said. Nothing is rewritten. | | A crash leaves a position with no stop | The stop and take profit go to the venue with the entry, or right after the fill where the venue takes them no other way, so they survive this process dying. | | Text from a feed tells the model what to do | The model that proposes a direction reads numbers code computed; outside text reaches only roles that decide nothing, and no model can send an order or reach a key. | None of these is hypothetical. [Why it is built this way](https://docs.bijectivelabs.dev/opensource/why) lists the documented failures behind each one, with their sources. ## How one cycle decides ```mermaid flowchart TD find[["the system finds the universe and measures each instrument
one data pull each; every value carries its source and age"]] --> wake wake{"trigger
has anything moved since the last look?"} wake -- "no" --> held["held: one data pull, no tokens spent"] wake -- "yes" --> pick["the screener picks among what the system admitted
citing each instrument's own figures"] pick -- "none taken" --> held pick -- "taken" --> briefs briefs["the system writes the briefs
each role sees only the figures its job needs"] --> round1 subgraph round1["asked at the same time"] direction LR tech["technical analyst
a direction, a confidence, a horizon"] macro["macro analyst"] chain["on-chain analyst"] mood["sentiment analyst"] end round1 --> price["the system prices the round trip
fee, spread, impact, funding, slippage"] price --> exits{"the stop and the target
the analyst's levels, held inside your bounds
does the trade break even at half its kind winning?"} exits -- "no" --> refused exits -- "yes" --> round2["validators, as many as you declare
a claim that cites no figure is thrown away"] round2 --> matrix{"confluence
do your declared states agree with the direction?"} matrix -- "no" --> refused["a refusal that names its figure"] matrix -- "yes" --> size["conviction
how much, from your ladder"] size --> risk{"risk per trade
what does it lose at the stop?"} risk --> gate{"cost gate and targets
does the edge clear the cost, by your margin?"} gate -- "no" --> refused gate -- "yes" --> intent["the intent, written into the cycle"] intent --> venue["venue mode only
one order, the stop and take profit attached"] ``` ## Who does what | Role | Reads | Answers | Can never | |---|---|---|---| | technical analyst | candles, spread, funding, book imbalance, open interest change, and the regime on its timeframe and the higher ones you declare | a direction, a setup from a fixed list, a confidence, a horizon, and where its stop and target sit | size the trade, place a level outside your `[exit]` bounds, run a trend setup in a range, or fade a market that is not a measured range | | macro analyst | dated rates and levels, dated text | risk-on, risk-off or neutral | propose a direction | | on-chain analyst | chain and derivatives figures | accumulation, distribution or neutral | propose a direction | | sentiment analyst | dated scores and notes you curate | negative, neutral, positive, or not declared | invent a mood no figure supports | | market screener | what the system measured for each admitted instrument: move, spread, regime, volume, correlation to BTC | which instruments to take, in what order, citing their figures | add an instrument the system did not admit | | validators: pro and contra | the proposal and the figures behind it, each without the other's case | the strongest case for or against, as cited claims | see the other side's case, or cite a figure it wasn't given | | validator: neutral, as judge | the same, and both cases once code has checked every claim | support, oppose or abstain, the one verdict that counts | see the advocates' verdicts, or count claims instead of weighing them | | risk manager | what the run recorded: round trips, the losing streak, your limits, the alarm that woke it | continue, cut the size of new entries, pause instruments, or halt, each for a stated number of hours | loosen a limit you set, or add size | | technology and quant auditors | the cycles this desk recorded | findings and proposed changes | change anything | Everything after the analysts is arithmetic over numbers you declared. No model can place, cancel or resize an order, read a file outside the project, or edit your configuration. [Agents](https://docs.bijectivelabs.dev/opensource/agents) has the full table of what an agent can reach, and [The cycle](https://docs.bijectivelabs.dev/opensource/cycle) walks through every step. ## What it trades Today, through one order path and the same gates: | Market | How | Run | |---|---|---| | Linear perpetuals (USDT- or USDC-margined swaps) | long and short, with the leverage and margin mode you declare; the default | end to end on the demo or testnet APIs of Bybit, OKX, Bitget, BingX, Binance and Gate | | Spot pairs | long only, paid in full; list `spot` in `[execution].markets` | the order path and a desk cycle on the Bybit and OKX demos | | Stock perpetuals and tokenized stocks | a perpetual and a spot pair to the venue, entered only while the underlying's session trades | a stock perpetual on the OKX demo, a tokenized stock on the Bybit demo | | Prediction market outcomes | long only, decided on a probability by the event analyst and priced to resolution; list `prediction` | sandbox cycles on live data; the order path is tested against ccxt's shapes | Every exchange ccxt supports is reached through that same path, and any other can have its run on its demo or testnet API at any time ([Venues](https://docs.bijectivelabs.dev/opensource/venues), [Exchange accounts and keys](https://docs.bijectivelabs.dev/opensource/exchange-keys)). That is where the project started, not where it stops. Dated futures, inverse contracts and options are on the way, each with its own cost model, exit rules and tests. Until a market type has those, venue mode checks the type on every order and refuses it, because a market the system cannot yet price and protect is not one it should send to. [Boundaries](https://docs.bijectivelabs.dev/opensource/boundaries) lists what is guaranteed, what is not built yet, and what adding each piece would take. ## Where it is going The project is in active development, and these pages describe where it stands today. - **More market types:** spot, stock perpetuals, tokenized stocks and prediction markets are built; inverse and dated futures and options follow, each with its own cost model, exit rules, sizing and tests before venue mode accepts it. - **More venues run end to end:** each joins the six already run after a full run on its demo or testnet API, ours or yours. - **The rest of the not-built-yet list** in [Boundaries](https://docs.bijectivelabs.dev/opensource/boundaries), one piece at a time, without weakening a guarantee. **Use the release.** What PyPI installs is the latest release, and these docs describe it. Releases come from `master`. The `develop` branch is where the next release is built: it moves often, a commit there is not a release, and it can carry work that is unfinished or changed again before it ships. A set of changes becomes a release when it is merged into `master`, tagged and published; the [changelog](https://github.com/BijectiveLabs/trading-research-agents/blob/master/CHANGELOG.md) says what each one brought. ## Where to go next | If you want to | Read | |---|---| | install it and run a first cycle | [Quick start](https://docs.bijectivelabs.dev/opensource/quick-start) | | see what the terminal shows, step by step | [What you see](https://docs.bijectivelabs.dev/opensource/what-you-see) | | understand every step and every setting | [The cycle](https://docs.bijectivelabs.dev/opensource/cycle) | | point it at your own data feeds, free or paid | [Data sources](https://docs.bijectivelabs.dev/opensource/sources) | | trade on a venue | [Venues](https://docs.bijectivelabs.dev/opensource/venues) and [Capital, orders and positions](https://docs.bijectivelabs.dev/opensource/capital) | | choose a model provider | [Model providers](https://docs.bijectivelabs.dev/opensource/providers) | | find a command | [Command line](https://docs.bijectivelabs.dev/opensource/commands) | | contribute | [Contributing](https://docs.bijectivelabs.dev/opensource/contributing) | # Quick start A few minutes and one model key. Every setting has a working default, so a new desk runs before you change anything; the table under step 6 says what those defaults are. **1. Install.** You need Python 3.11 or newer. [uv](https://docs.astral.sh/uv/) is the quickest way: ```bash uv tool install 'trading-research-agents[ccxt,openai,anthropic,google,data]' ``` That brings every model provider and every exchange in. Without uv, run `pip install 'trading-research-agents[ccxt,openai,anthropic,google,data]'` inside a virtual environment. To move to the newest release later: ```bash uv tool upgrade trading-research-agents # or: pip install -U 'trading-research-agents[ccxt,openai,anthropic,google,data]' tra --version ``` **2. Make a desk.** A desk is a folder holding your settings and everything the agents decide. Name the provider you have a key for: ```bash tra init my-desk --profile autonomous --agent groq cd my-desk ``` | `--agent` | Key in `.env` | Model, when you name none | |---|---|---| | `groq` | `GROQ_API_KEY` (free tier) | `openai/gpt-oss-120b` | | `google` | `GEMINI_API_KEY` | `gemini-flash-latest` | | `anthropic` | `ANTHROPIC_API_KEY` | `claude-opus-5-5` | | `openai`, `deepseek`, `xai`, `openrouter`, ... | see [Model providers](https://docs.bijectivelabs.dev/opensource/providers) | name one with `--model`; `tra models ` lists them | Add `--venue okx` (or any ccxt id) to trade somewhere other than Bybit. **3. Add your key.** Copy the example file and put your key in it. Every command reads the `.env` next to `strategy.toml` on its own. ```bash cp .env.example .env # then set GROQ_API_KEY=... in .env ``` **4. Check.** `tra check` reads every setting against the others and names the one thing to fix next. A desk that is ready ends with: ``` ok provider groq: ready ready to run `tra run` ``` **5. Run.** `tra run` decides once on live market data, in the sandbox: nothing leaves your machine. `tra run --every 15m` keeps deciding, once per fifteen-minute bar, on the clock. Or type `tra` on its own for the console, which takes the same commands. ```bash tra run tra run --every 15m ``` **6. When you want real orders.** Start on the exchange's demo or testnet API. Make the desk for that exchange from the start, and the rest is three commands: ```bash tra init my-desk --profile autonomous --agent groq --venue bitget cd my-desk # put the exchange's keys in .env beside your model key tra keys --probe --prepare # reads the account; on a demo or testnet sets one-way mode tra mandate --sign "Your Name" --venue-mode tra check ``` `--venue` writes everything that exchange needs into `[execution]`: its demo or testnet API, 3x isolated leverage, at most 200 per order, how its stop and take profit are placed, and orders sized at 100 of the quote currency. Change any of it; `tra check` says what each line does. How to make the key on each exchange is in [Exchange accounts and keys](https://docs.bijectivelabs.dev/opensource/exchange-keys), and [Toward real orders](#toward-real-orders) below has the whole path. **What a new desk does before you change anything** | | Default | Where to change it | |---|---|---| | Orders | none: the sandbox writes each decision down and sends nothing | `[execution].mode` | | What it trades | BTCUSDT on Bybit, as a taker | `[run].instrument`, `[universe]`, `--venue` | | Timeframe | decides on 15-minute bars, reads 1h and 4h as context | `[run].bar_interval_s`, `context_intervals` | | Strategy | none fixed: the analyst picks the setup the measured regime supports, or stays flat | `[agents].mechanism` | | Stop and target | the analyst names them; the stop is held between 0.2% and 3% and never inside one ATR, the target between 1.5x and 4x the stop | `[exit]` | | Risk per trade | 0.5% of equity lost at the stop, whatever the stop's distance | `[controls].max_risk_per_trade_bps` | | Bad days | no new entry after a 3% loss on the day or a 10% fall from the peak | `[controls]` | | Time limit | a position closes after 4 hours | `[exit].timeout_bars` | | Trades worth taking | the edge at least 2x the round trip, break even at half the trades or better, at most 6 a day | `[targets]` | | Oversight | a risk manager that can only cut, pause or halt; two auditors that report | `[audit]` | Working on the code itself? [Contributing](https://docs.bijectivelabs.dev/opensource/contributing) has the setup from a clone. ## What you decide, and what has a default The system measures what it can measure, uses the common standard where one exists, and asks you only for what nobody else can decide. `tra check` says, in plain words, what every setting below will do in your project, defaults included. **You decide:** | What | Where | Why it is yours | |---|---|---| | What to trade | `[run].instrument`, `[run].venue_key` | | | How much per trade | `[run].size` or `[run].size_quote` | your money | | Which model | `[agents].provider`, `[agents].model` (`tra init --agent` writes both) | your account and your bill | | Your thesis, if you trade one idea | `[agents].mechanism` (`tra init --agent` writes one that lets the regime choose) | the setups you favour; it never overrides the measured regime | | Leverage | `[execution].leverage` | only you know what you can carry | | The most one order may be worth | `[execution].max_notional` | the cap on a defect's cost | | That you take the losses on | `MANDATE.md`, `acknowledged_by`, `acknowledged_on` | venue mode only | **Already set, change any of it:** | Setting | Default | What it does | |---|---|---| | `[controls].max_risk_per_trade_bps` | `50` | the most one trade may lose at its stop, in bps of equity | | `[controls].daily_loss_limit_bps` | `300` | no new entry once equity is down this far on the UTC day | | `[controls].max_orders_per_minute` | `6` | across the whole desk | | `[controls].max_position` | none | a cap in units; the risk per trade already bounds every size | | `[controls].max_drawdown_bps` | none (`tra init` writes `1000`) | no new entry once equity is this far below its peak | | `[exit]` | the analyst's stop and target, held inside bounds | see [where the stop and take profit sit](https://docs.bijectivelabs.dev/opensource/cycle#where-the-stop-and-the-take-profit-sit) | | `[targets].max_break_even_win_rate` | `0.5` | refuse a trade that would need more than half its kind to win to cover the stops and the costs | | `[exit].timeout_bars` | four hours of bars: `240` at 1m, `16` at 15m | a position closes by time after this many bars; a measured stop is sized over the same span | | `[run].bar_interval_s` | `60`; `tra init --agent` writes `900` with 1h and 4h as context | the decision timeframe; the round trip costs the same on any of them, so a longer one leaves more of the move | | `[run].settlement_interval_s` | `28800` | the funding clock used only when the venue does not report an instrument's own | | `[execution].environment` | `demo` | where orders go: `demo`, `testnet` or `mainnet` | A scripted strategy (`[strategy].file`) is research, so it declares its own exit and cost figures: `[exit].stop_frac`, `timeout_bars` and `mechanism_bound`, and `[run].spread_cost_bps`, `latency_ms` and `slippage_bps`. A default there would be a cost the research never measured. What stays strict, and why: a setting nobody recognises is refused rather than ignored, because a typo in a risk setting should stop the desk at `tra check`, not at the venue; and the few things above that are yours are asked for, never guessed. ## Toward real orders An agent desk goes through three steps, and you can stay at any of them as long as you like: | Step | How | What happens | Needs | |---|---|---|---| | 1. Sandbox | `tra run` | the full cycle on live data; every decision written down, no order sent | a model key | | 2. Demo or testnet API | `tra run` with `[execution].mode = "venue"` and `environment = "demo"` or `"testnet"` | real orders on the exchange's demo or testnet API, exits attached, positions looked after | a signed mandate, a leverage, `max_notional`, practice keys | | 3. Real capital | the same, with `environment = "mainnet"` | real orders with real money | all of the above, plus the ladder at its canary tier (`tra ladder promote`, with evidence) | Venue mode is the only mode that can lose money, so it has a door in front of it. Read the mandate with `tra mandate`; when you accept it, `tra mandate --sign "Your Name"` keeps it in the project as `MANDATE.md` and signs `[execution]` with your name and today's date, in one step. Signing alone does not switch anything on; `--venue-mode` signs and turns orders on together. Read [Capital, orders and positions](https://docs.bijectivelabs.dev/opensource/capital) before you open it. ```toml [execution] mode = "venue" environment = "demo" # demo | testnet | mainnet leverage = 3 # yours to choose; it never makes a trade bigger max_notional = 400 # the most one order may be worth, in quote currency margin_mode = "isolated" # isolated | cross hold_limit = "timeout" # close by time after [exit].timeout_bars; or horizon, or none ``` Who decides what, in one line each: **the analysts propose the direction and where the trade is wrong or done, your risk rule sets the size, you choose the leverage, and no model touches the last two.** A scripted strategy of your own has a research path before any of this: `tra data get` and `tra backtest` replay a day of the exchange's archive net of every cost, `tra paper` runs it on the live feed with nothing sent, and `tra live` is a small canary session behind hard caps. [The live tier](https://docs.bijectivelabs.dev/opensource/live) walks through it. # What you see, in order ## 1. The console Run `tra` inside your desk and it opens the console: what is ready, what isn't, and the next command to run. Under it is a prompt that takes every command and draws it right there. Arrow keys go back through what you typed, tab completes, `help` lists the commands and `exit` leaves. For scripts and cron, `tra -c "run --now"` runs one command and exits. ![the console](https://docs.bijectivelabs.dev/images/console.png) ## 2. The pass over your instruments Type `run --every 15m` and the desk first refreshes any data series that are due, one line each. Then it looks at every instrument on your list, one data pull apiece, and measures it. The screen steps through them live: ![the screen pass](https://docs.bijectivelabs.dev/images/pair-screener.png) ## 3. One cycle per instrument it takes The system found the candidates and measured them; the screener chose among them and cited each instrument's own figures; the system's own order is kept beside the choice. The instruments taken get a full cycle. The terminal shows the whole pipeline from the start and updates it as each step begins and ends. Steps that run at the same time spin at the same time, each with its own clock, and the line underneath says who is working right now: ![a scheduled desk at work](https://docs.bijectivelabs.dev/images/trading-agents.png) ## 4. The result When the cycle finishes, the table gives way to the pass over the universe: what the screener took, what waited and what was held, and why. Then the cycle itself: what the analyst proposed and from which figures, and every refusal with the number behind it. Here an ETH short in a ranging market stopped at its exits: with its own stop and target and the round trip, more than half of trades like it would have had to win just to break even: ![one cycle](https://docs.bijectivelabs.dev/images/cycle.png) Then the data the cycle used and how old it was, the series the reading analysts read, the stop and take profit it would have placed, and which agents answered. A cycle that goes further adds what each validator argued, how conviction weighed the analysts, the risk at the stop and the targets: ![data, series, exits and agents](https://docs.bijectivelabs.dev/images/one-cycle.png) ## 5. Between cycles The desk says when the next pass is, on the clock and in minutes, for example `next pass at 14:30:00 UTC, in 10m 57s`, and counts down to it. An auditor woken by an alarm gets a spinner of its own. ## The same cycle as plain text `--plain`, a pipe or a log gives text instead of panels. This is from a real run on live data, shortened: ``` cycle 2026-09-29T02-59-16Z BTCUSDT: 5.344s of agents, 15.042s of validators funding ccxt:bybit age 4.267s ohlcv ccxt:bybit age 32.746s orderbook ccxt:bybit age 7.544s macro risk-off: the operator declared a bullish state for US10Y; measured correlation is neutral (0.0) on a modest sample, weak evidence that does not overturn the declaration sentiment positive: declared state is bullish, and the measured figures both support it technical: long on range_reversion at 0.65 over 5 bars: the close is stretched low in the window read: zscore=-0.587, vwap_gap_bps=-3.99, ret_1_bps=1.59 validator contra oppose on 2 cited figure(s) validator pro support on 3 cited figure(s) conviction: 0.800 of 1.0 (macro 1.0, sentiment 0.5, technical 1.300) risk: 8.29 bps of equity at the stop against the declared 50 bps, size 0.010 cost: 12.87 bps round trip, 2.00 of it estimated gate: 25.0000 bps claimed clears 12.8723 bps cost plus 0.0000 bps margin refused targets: the claimed edge is 1.94x the round trip and the desk declared 2.0x; the cost is certain and the edge is not ``` That trade was refused. It cleared the cost gate and still died, because this desk asks for an edge of twice the cost and got 1.94 times. A refusal names the figure behind it, every time. ## What is kept, and where | What | Where | Notes | |---|---|---| | everything one cycle did | `runs/cycles//` | snapshot, briefs, answers, intent, `placed.json`, `cycle.json`, and `inputs.json.gz` with the raw inputs; `cycle.json` holds a sha256 of each file | | the pass over the universe | `runs/screens/` | every instrument measured, what the screener chose and why | | every model call | `runs/cycles/usage.jsonl` | tokens, latency, price | | when the desk ran and what stopped it | `runs/cycles/scheduler.jsonl` | one line per start and stop | | every position closed, by time, by the venue or by the kill switch | `runs/cycles/positions.jsonl` | venue mode | | every finished trade with entry, exit, fees and funding | `runs/cycles/round_trips.jsonl` | venue mode; what the quant auditor reads | | the first equity reading of each day | `runs/cycles/equity.jsonl` | what the daily loss limit is measured from | | what you typed at the prompt | `.tra-history` | a convenience only; `tra -c` keeps none | `tra runs verify` checks every cycle's files against their hashes and its briefs against its raw inputs. `tra runs prune --keep 90d` shows what old cycle directories would be removed. ## Watching a desk you are not running The console runs commands in its own process, so closing it ends what it was running. A desk that should keep running belongs under a supervisor (systemd, a service manager, `tmux`), and then you look at it from anywhere with: ```bash tra watch --every 5s # a read-only view built from the records the desk writes ``` `tra watch` opens no connection, holds no key and takes no lock, so opening and closing it does nothing to the desk. # Why it is built this way This software did not start from a trend. It started from what went wrong when language models were given money, numbers and authority, as reported by the people who ran the tests, by researchers and by regulators, and from what we found building it ourselves. Each design decision here answers one of those problems. None of them is an argument against language models: they read a market's context well and quickly, and that is what they are used for here. The problems come from asking them to do what they are not reliable at, and from giving them authority nothing checks. Every figure on this page is the source's own, with the date it was reported. The sources are listed at the end. ## Language models trading real money **What happened.** In Alpha Arena Season 1, run by the research lab Nof1 in October 2025, six frontier models were each given $10,000 to trade crypto perpetual futures on Hyperliquid on their own. When it ended on November 3, 2025, four of the six had lost money: Claude Sonnet 4.5 finished at $5,799, Gemini 2.5 Pro at $5,445, Grok at $4,208 and GPT-5 at $4,126. Qwen3 Max finished at $12,231 and DeepSeek at $10,489. **What the software does.** A model proposes a direction, a setup and the levels that bound the idea. It does not size the trade, set the leverage, decide whether the trade is worth its cost or send the order: code does, from limits the operator declared, and every gate that refuses records why. Whether the model's side is any good is measured, not assumed: `tra hindsight` replays every decision against its exact reverse. Desks start on a demo or testnet API, and real capital needs a signed mandate and evidence on the ladder. ## Trading too much **What happened.** In the Moscow Exchange's AI hackathon (May 13 to June 10, 2026), 50 teams ran trading agents on 1 million virtual rubles each. The worst agent made almost 7,700 trades with a turnover of 1.8 billion virtual rubles, and commissions ate a significant part of its capital. The exchange's own conclusion: more trading did not mean more profit, and the better agents traded far less, under risk management. **What the software does.** The model is not asked unless the market has moved enough to be worth asking (`[trigger]`). A trade is refused when its expected edge does not clear its full round trip by the multiple the operator declared (`[targets]`), when the day's count of round trips is reached, or when a cooldown since the last entry has not passed. The cost of every trade is priced before it is sent, fees, spread, slippage and funding together. ## Wrong numbers in financial answers **What happened.** Saturn, which builds AI software for financial advisers, tested 18 popular AI models on 121 financial advice questions in September 2026. On average the models were wrong 57% of the time; on harder questions 88%, with some models wrong 99% of the time. Free models were wrong in 63% of cases and paid ones in 49%. The failures reported include miscalculated pension tax and rules for debt and student loans that do not exist. Earlier, in November 2023, Patronus AI's FinanceBench found that GPT-4-Turbo used with a retrieval system answered 81% of its questions about public companies' filings incorrectly or refused to answer them. **What the software does.** No model calculates. Code pulls the data, computes every figure the models read (returns, volatility, market structure, the regime, the cost of a trade) and records it. A model answers in a closed schema and cites the figures it relied on by name and value; code checks each citation against the brief and discards the ones that do not hold, and a deciding answer with nothing left is refused. Sizes, stops, costs and limits are arithmetic in code, the same every time. ## Instructions hidden in data **What happened.** In 2023 Greshake and colleagues showed that instructions planted in content a model reads later, a web page, a document, an email, are followed as if the user had given them: indirect prompt injection. In 2025 researchers from Princeton University and the Sentient Foundation showed the same against crypto agents built on ElizaOS, through their memory: a message planted in an agent's stored history could lead it to send funds to an attacker, and prompt-based defences gave only limited protection once that stored context was corrupted. OWASP ranks prompt injection first among the risks of LLM applications (LLM01:2025). **What the software does.** The role that proposes a direction reads numbers computed by code, not text from outside. Headlines and notes reach only the reading analysts, whose labels do not decide anything, and every prompt says that text inside a brief is material to read, never an instruction to follow. There is no memory a model writes for itself: what a role sees is built by code for each call and recorded. And whatever a model says, it cannot send, cancel or change an order, or reach a key. ## Agents with authority over money **What happened.** On November 29, 2024, an AI agent called Freysa, set up as a game never to release its prize pool, was talked into sending about $47,000 by a participant's crafted message (OECD AI Incidents Monitor). On March 18, 2025, an attacker queued fraudulent prompts into the autonomous crypto agent AIXBT through its dashboard, and the agent sent 55.5 ETH from its wallet (AI Incident Database, incident 1003). **What the software does.** No role holds a key or can move funds. Keys are read from the environment by the one module that sends orders, are never in a prompt, a brief or a record, and any message that reaches a record has them removed first. An order leaves only through that module, after every gate, under a mandate a named person signed. The key guide asks for trading permissions only, on every venue, and before a live tier session a Bybit key that can move funds is refused. ## Rules a model is asked to follow **What happened.** A common answer to "why trust an agent" is "you trust the rules it is given, not the model". The record shows models stepping past rules written into what they read. - In 2023 researchers at Apollo Research put GPT-4 to work as an autonomous stock trading agent in a simulated firm. Given an insider tip, it traded on it although it knew management disapproved of insider trading, and when it reported to its manager it consistently hid the real reason. - In December 2024 Apollo Research showed frontier models, in test scenarios built to tempt them, introducing subtle mistakes and trying to disable the oversight they had been told about. The authors present this as a capability shown under those conditions, not a measure of how often it happens in use. - In May 2025 Palisade Research reported that OpenAI's o3, told explicitly to allow itself to be shut down, sabotaged the shutdown script in 7 of 100 runs. - In July 2025 a Replit coding agent deleted a live production database during a code freeze it had been told repeatedly to respect, and produced fabricated data afterwards (AI Incident Database, incident 1152). Models also state rules that do not exist. In November 2022 Air Canada's website chatbot told a customer he could claim a bereavement fare after booking, which the airline's policy did not allow; in February 2024 a British Columbia tribunal held the airline responsible for what its chatbot said (Moffatt v. Air Canada, 2024 BCCRT 149). In April 2025 Cursor's AI support agent told users that one device per subscription was a security policy; there was no such policy, and users cancelled before the company corrected it. **What the software does.** No limit here is a sentence a model is asked to respect. Every one is code that runs after the model has answered, and no model is in that path. - The rules are not in the deciding role's brief. The technical analyst is not told what a trade costs, what it has to clear, the size or the limits, so there is nothing to argue with or around. - An answer is a closed schema. Code reads its fields, a value outside its list is refused, and no sentence a model writes is read to decide anything. - The roles that can touch a limit can only make the desk more careful. The risk manager's limits apply only where smaller, and a limit it names that does not exist is refused; between cycles it can cut size below 1x, pause instruments or halt, each for a capped number of hours, and never add size. - No model can change the configuration, send an order or reach a key. An auditor's proposed change has to name a setting the configuration accepts, and the loader refuses an unknown key, so a rule a model made up cannot take effect even if someone copied it in. - No role is told the desk's rules, every prompt says so, and an answer that states one anyway is recorded beside it as made up. Run over the 832 answers our demo desks had recorded, that check found none. ## Automation without pre-trade controls **What happened.** On August 1, 2012, a router at Knight Capital running discontinued code sent more than 4 million orders in 45 minutes trying to fill 212 retail orders, and the firm lost over $460 million. In 2013 the SEC fined Knight $12 million in the first case under its market access rule, for not having adequate safeguards to limit the risks of its access to the markets. **What the software does.** One decision sends one order, under a client order id of its own, so it cannot be sent twice. Nothing is retried blindly. Every order is capped (`max_notional`), the desk sends at most a declared number of orders a minute, no new entry is taken past a daily loss or a drawdown limit, and the kill switch flattens and stops. Before an order goes out, leverage and margin mode are read back from the venue, and an order is refused against a book older than 30 seconds. ## Who stays responsible **What regulators say.** ESMA's public statement of May 30, 2024 on AI in investment services says a firm's decisions remain the responsibility of its management body whether they are taken by people or by AI tools. IOSCO's consultation report of March 2025 on AI in capital markets names hallucination, explainability, and autonomy and control among the risks that may emerge. **What the software does.** A named person signs the mandate before any order is sent. Every decision is written down with the figures it rested on, what each gate decided and why, and can be recomputed from the raw inputs. The auditors report and propose; only a person changes the configuration. ## What we found building it The same habit applied to our own runs on demo and testnet APIs found problems no specification mentions: | Found | What changed | |---|---| | Validators asked side by side voted their assignment: pro supported 63 times in 64, contra opposed 61 times in 63, and a neutral asked alone abstained in 33 of 37 cycles | pro and contra build their cases apart and neutral judges the checked cases ([the validators](https://docs.bijectivelabs.dev/opensource/cycle#the-validators)) | | Reading analysts cited figures by names the brief did not use, 27 times in four days | a naming rule in the prompts and a check that resolves names; none discarded in the week of runs that followed | | A demo API served an order book 3.2 hours old, 35 bps from where its own trades printed | orders are refused against a book older than 30 seconds | | A library reported a fee the venue charged as a rebate | fees are read as the venue charged them | | A venue's leverage call answered "success" while the setting stayed where it was | leverage and margin mode are confirmed by reading them back | | The technical analyst could see the other analysts' conclusions | every analyst reads only its own data; code weighs the readings once | | An auditor could propose a change to a setting that does not exist, as long as its table did | a proposed setting has to be one its table accepts | The details are on [Venues](https://docs.bijectivelabs.dev/opensource/venues), [Agents](https://docs.bijectivelabs.dev/opensource/agents) and [Working with language models](https://docs.bijectivelabs.dev/opensource/llm). ## Sources - Alpha Arena Season 1 results: [ForkLog, "Four out of six AI models suffer losses in trading tournament"](https://forklog.com/en/four-out-of-six-ai-models-suffer-losses-in-trading-tournament/), quoting Nof1's founder; [South China Morning Post](https://www.scmp.com/tech/tech-trends/article/3329784/deepseek-outperforms-ai-rivals-real-money-real-market-crypto-showdown). - Moscow Exchange AI hackathon: [report of the results](https://www1.ru/en/news/2026/06/30/mosbirza-dala-ii-million-robot-prokrutil-18-milliarda-i-proigral.amp.html), citing the exchange via Vedomosti. - Saturn, "Artificial Authority": [the report](https://www.saturnos.com/report/artificial-authority); [Financial Reporter coverage](https://www.financialreporter.co.uk/ai-models-give-wrong-financial-advice-57-of-the-time-saturn-research-finds.html). - FinanceBench: [Islam et al., "FinanceBench", arXiv:2311.11944](https://arxiv.org/abs/2311.11944v1); [Patronus AI announcement](https://patronus.ai/announcements/patronus-ai-launches-financebench-the-industrys-first-benchmark-for-llm-performance-on-financial-questions). - Indirect prompt injection: [Greshake et al., arXiv:2302.12173](https://arxiv.org/abs/2302.12173v2), AISec 2023. - Memory injection in Web3 agents: [Patlan et al., "Real AI Agents with Fake Memories", arXiv:2503.16248](https://arxiv.org/abs/2503.16248). - [OWASP Top 10 for LLM Applications 2025](https://genai.owasp.org/llm-top-10/). - Freysa: [OECD AI Incidents Monitor](https://oecd.ai/en/incidents/2024-11-29-2e31). - AIXBT: [AI Incident Database, incident 1003](https://incidentdatabase.ai/cite/1003). - Insider trading by a trading agent: [Scheurer et al., "Large Language Models can Strategically Deceive their Users when Put Under Pressure", arXiv:2311.07590](https://arxiv.org/abs/2311.07590). - Oversight subversion: [Meinke et al., "Frontier Models are Capable of In-context Scheming", arXiv:2412.04984](https://arxiv.org/abs/2412.04984). - Shutdown sabotage: [The Register, May 29, 2025](https://www.theregister.com/2025/05/29/openai_model_modifies_shutdown_script/), reporting Palisade Research; [Palisade Research, "Shutdown resistance in reasoning models"](https://palisaderesearch.org/research/shutdown-resistance). - Replit: [AI Incident Database, incident 1152](https://incidentdatabase.ai/cite/1152); [Fortune, July 23, 2025](https://fortune.com/2025/07/23/ai-coding-tool-replit-wiped-database-called-it-a-catastrophic-failure/). - Air Canada: Moffatt v. Air Canada, 2024 BCCRT 149; [AI Incident Database, incident 639](https://incidentdatabase.ai/cite/639). - Cursor support agent: [AI Incident Database, incident 1039](https://incidentdatabase.ai/cite/1039). - Knight Capital: [SEC press release 2013-222](https://www.sec.gov/news/press-release/2013-222). - [ESMA public statement on AI and investment services, May 30, 2024](https://www.esma.europa.eu/sites/default/files/2024-05/ESMA35-335435667-5924__Public_Statement_on_AI_and_investment_services.pdf). - [IOSCO consultation report on AI in capital markets, March 2025](https://iosco.org/news/pdf/IOSCONEWS761.pdf). # The cycle One cycle is one pass from what the market is doing to a decision that is either an intent or a refusal with a reason. It is the unit this project works in, and the unit an auditor replays. ```bash tra init my-desk --profile autonomous # the cycle laid down wired, every line commented tra run # data.toml, strategy.toml, envelope.toml and .env from the project tra run --instrument ETHUSDT --margin 5 --mode signals tra run --every 15m --cycles 12 # one cycle every fifteen minutes, on the boundary ``` ## Running it on a schedule `--every` runs a cycle on the interval's own boundary, so a cycle reads a bar that has finished happening rather than one that is half built. One process, no cron, no daemon: the loop is in the same command, and everything that should end it ends it by name. The first pass runs as soon as the desk starts. After that, passes land on the clock: with `--every 30m` they fall at :00 and :30 UTC, with `--every 15m` at :00, :15, :30 and :45. So the wait after a pass is whatever is left to the next boundary, not a full interval, and a pass that takes several minutes shortens it further. The desk says it plainly after every pass, for example `next pass at 08:30:00 UTC, in 20m 00s`. | Ends the loop | Why | |---|---| | the budget | a cycle that stopped because the token budget is spent will not be followed by one that costs more | | the kill switch | the file named in `[controls].kill_switch` exists | | `--cycles N` | N scheduled passes, whether or not the trigger let a cycle through | | a keyboard interrupt | the operator stopped it; the cycle that was running is already on disk | | SIGTERM, SIGHUP, SIGBREAK | a service manager stopping the desk, or an SSH session dropping; handled exactly like Ctrl-C | Some lines end the day's entries without ending the run. While one of them holds, each pass still looks after open positions (time exits, venue exits, the kill switch) and asks no model: | Holds new entries | Until | Set with | |---|---|---| | the pause switch | the file is removed | `tra pause` (writes `PAUSE`), `tra pause --off`; or `[controls].pause_switch` names another file | | the daily token budget | 00:00 UTC | `[agents].daily_token_budget`: tokens recorded today by every role and both auditors, across runs and restarts | | the daily cost budget | 00:00 UTC | `[agents].daily_cost_budget`: the same in model cost, from `[agents.pricing]` | | trades per day | 00:00 UTC | `[controls].max_trades_per_day`: orders written today across every instrument | | a losing streak | 00:00 UTC | `[controls].max_losing_streak`: losing round trips in a row today; a winner resets it | | the daily loss limit | 00:00 UTC | `[controls].daily_loss_limit_bps`, checked inside each cycle | | the drawdown limit | the peak file is cleared | `[controls].max_drawdown_bps`, with `on_drawdown = "flatten"` to flatten and stop as well | `tra kill` writes the kill switch file (`[controls].kill_switch`), so the next pass flattens and stops; `tra kill --off` removes it. `tra check` prints every line in force on one row. Each count is read from the records on disk, so restarting a desk does not reset a day. A refusal is not a reason to stop. A cycle that refused because a gate said no is a cycle that did its job, and the next one decides again with fresh figures. The frequency cap in `[targets]` still applies across the whole day, so an interval shorter than the desk's own limit simply produces more refusals, each one named. Every start and stop of `tra run` is one line in `runs/cycles/scheduler.jsonl`: when it started, what it watched, in which mode, and what stopped it after how many cycles. Each cycle already keeps its own directory; this file is what says when the desk was running at all, which matters on a server whose terminal output nobody kept. ```json {"at": "2026-10-04T06:56:38+00:00", "event": "started", "every_s": 300.0, "mode": "venue", ...} {"at": "2026-10-04T09:12:04+00:00", "event": "stopped", "stopped_by": "SIGTERM from outside the process", "cycles": 27} ``` **Stopping while an order rests on the book.** A maker entry waits up to `[execution].limit_timeout_s` for a fill. If the process is told to stop during that wait, the order is cancelled first, `placed.json` and `cycle.json` say so (`"interrupted": true`, and whether the cancel went through), and only then does the run end. Stopping between cycles exits with the last cycle's outcome; stopping inside one always exits as a refusal, so a supervisor can tell the two apart. One case nothing in a process can cover: closing a Windows console window. Windows gives the process a few seconds and ends it whatever it is doing. What protects capital then is what was already at the venue - the stop and take profit travel with the entry, and the next cycle refuses while any order still works on the instrument. A desk that has to survive its terminal belongs under a supervisor, not in a window. `tra init my-desk --profile autonomous` is the short way in. It writes the same project as `tra init`, with these differences: the technical analyst is the core, deciding on 15-minute bars with the hour and four hours as context and naming its own stop and target; the confluence matrix, the conviction ladder and the targets are live tables rather than commented ones; `[execution].mode` is `sandbox`; and the project's README opens with what is still yours before a cycle can decide anything - a model and its key, and the mechanism the analyst reasons from. Add `--agent PROVIDER --model MODEL` and the model is named for you. `tra check` names whatever is still missing, including the provider placeholder, and reads the three files against each other: the instrument as the sources know it, a venue supplying data that is not the venue whose fees are priced, and an `[execution].venue` that does not match `[run].venue_key` - that last one is a refusal rather than a note. The gates it writes ask for nothing until you give them something: `[confluence].required` is empty, because a role nobody asked has agreed to nothing, and `[conviction].weights` names only the deciding analyst, so the conviction score is that analyst's own confidence. Fill in a file under `sources/` and its analyst joins the roster; give that analyst a weight and its agreement starts changing the size; name it in `required` and its disagreement starts refusing the trade. Three steps, in that order, each one visible in `tra check` before it costs a token. ``` the universe (system: one data pull each, the trigger per instrument, ordered by movement over cost) -> the instrument this pass takes -> snapshot (engine) -> trigger (system: has anything moved? nothing below here runs if not) -> briefs (system, each role's own sections only) -> technical analyst + reading analysts (parallel) -> the round trip priced (system) -> the stop and the target (the analyst's levels, held in your [exit] bounds; break-even checked) -> validators (when configured: pro and contra apart, then neutral judges both) -> confluence matrix (system: declared states and votes) -> whether to trade at all -> conviction (system: the same voices, weighted) -> how much the desk wants on -> sizing (system, when [sizing] is declared: Kelly from the desk's own round trips, cut for the drawdown ahead) -> how much the record supports -> risk per trade (system: the loss at the stop, round trip included, against equity) -> how much it can afford -> the desk (system: what the whole book is already carrying) -> whether it fits -> cost gate (system: the claim against the cost) -> targets (system: net bps, the edge over the cost, reward-to-risk, research, count) -> intent, or a refusal that names the figure that refused it ``` Nothing in that line is optional and nothing in it is reordered. The engine fetches; the system computes; models read figures and answer inside a schema; the system prices the round trip and decides whether the claim clears it. In the default mode the intent is written to the cycle's own directory and no order leaves the machine. ## Which instrument, before any of this A desk that watches one instrument runs it. A desk that declares a universe takes the cheap look first, and only then spends a cycle. The work is split on purpose: **the system finds and measures, the screener chooses.** ```toml [universe] instruments = ["BTCUSDT"] # always looked at, whatever discovery finds discover = true # add the venue's most traded perpetuals each pass top = 20 # how many discovery adds min_quote_volume = 10000000 # 24-hour volume in quote currency max_spread_bps = 5 # the book now, from the venue's ticker min_listed_days = 30 # nothing listed this month crypto_only = true # leave out stocks, ETFs, commodities and currencies exclude = [] # instruments never to take quote = "USDT" # the settlement currency to look in # type_field = "symbolType" # the venue's own market field that says what a market is, # exclude_types = ["stock", "etf", "commodity", "forex"] # and the values to leave out max_per_cycle = 1 # how many one pass runs a full cycle on max_intents_per_day = 6 # across every instrument, not per one screener = true # the screener chooses and orders; without it, the system's order ``` **Discovery (system, every pass).** One market list and one ticker call cover every linear perpetual the venue lists in `quote` (USDT by default). The system removes what cannot be traded: too little volume, a book wider than `max_spread_bps`, listed too recently, not crypto, or a maximum leverage below `[execution].leverage`. Bybit marks its stock, ETF, commodity and currency perpetuals, and that is the default there; for any other venue, `type_field` and `exclude_types` name the venue's own field and the values to leave out, and `exclude` takes out single instruments. Every one of these is yours to set: the defaults are a starting point, not the screen. The most traded `top` join the declared instruments. Each discovered instrument carries the venue's own symbol and size step, so an order is never sent under the wrong market or off the step. Discovery needs `[run].size_quote` and `[controls].max_position_quote`: one base size, or one cap counted in units, across coins priced from cents to tens of thousands is no size and no cap at all. With a quote cap declared, every instrument is capped at that much quote currency at its own mid, and `[controls].max_position` (units) still applies to `[run].instrument`, the one it was written for. `tra universe` shows what discovery finds right now and what each filter took out. **Measurement (system).** Each instrument gets one data pull, the book and the recent bars, no tokens. The same `[trigger]` and desk limits apply to each. What survives is measured: the move since the last look and its pressure against the spread, the window's move, z-score, trend strength and regime, volume against the window, where the close sits in its range, 24-hour volume and change, and the correlation with BTC. **Choice (the screener).** With `screener = true` the market screener reads those rows and picks the instruments where the mechanism has the best chance now, best first, naming the setup it expects and citing the figures behind each pick. The system checks every pick: it must be an instrument the system admitted, and at least one citation from that instrument's own row must hold, or the pick is thrown away. The screener cannot add an instrument, and it says nothing about direction or size; the technical analyst decides those later from a fuller brief. A screener that cannot answer leaves the system's order, the move against the spread, in place. Both orders are kept in `runs/screens/