Seven orders,
six refusals.
I spent an afternoon trying to make Vibe‑Trading misbehave. I widened permissions at the commit step, replayed authorisations, corrupted the kill switch, edited the audit log. Seven orders went in. Six came back refused, and each refusal named the rule it was enforcing.
The refusal I keep thinking about happened at the commit step. I had a permission file with a fifty‑dollar order cap and tried to raise it to five thousand on its way to disk. The system refused and named its reason: widening a permission must go through a fresh proposal.
You can narrow a permission at commit time. You cannot loosen one. Most evaluations of trading agents ask how smart the model is. The engineering below assumes the model will at some point be wrong, confused, or compromised, and builds for that day.
Every live order runs a fixed series of checks, in a fixed order, and they fail closed.
The picture from the top of the page, one check at a time. Seven orders are queued: one ordinary, six built to break something.
| Check | 1plain | 2widen | 3replay | 4garble | 5kill | 6log | 7over |
|---|
Seven orders, one afternoon
Order 1 is ordinary: a forty‑dollar buy, inside every limit. The other six each attack a different organ of the safety layer.
The finished picture sits faintly behind this card. Scroll, and it fills in one check at a time.
Is there a mandate, and is it still alive?
Before the agent can touch a live account you write a permission file the project calls a mandate: biggest allowed order, biggest total exposure, leverage, trades per day, which markets and instrument types, minimum liquidity and company size, and symbols never to touch.
The agent can propose a mandate. Only a function its loop cannot reach can commit one. And the committed file expires after thirty days by default.
“expires_at: … after which the mandate is dead and the gate fail‑closes until the
user re‑authorizes. Default lifetime 30 days from created_at (configurable per commit).”
agent/src/live/mandate/model.py2
The kill switch is a file on disk
Not a flag in memory, not a model decision. A file. It halts everything even when the model is stuck, and it halts everything when the file’s own contents are corrupted, because a kill switch that cannot be read is treated as a kill switch that is on.
I corrupted it. Order 5 never reached a broker.
“enforced at the filesystem layer, independent of the LLM cooperating … the file’s existence is the halt.”
agent/src/live/halt.py2
The log notices when you edit it
Decisions land in an append‑only, hash‑chained journal. I edited one entry. It detected the edit, and then it refused to write anything further on top of the broken chain.
A tamper‑evident log that keeps accepting writes is a diary. This one stops.
“editing or deleting ANY earlier record breaks the hash of every record chained after it.”
agent/src/governance/ledger.py2
An authorisation is good exactly once
I presented the same authorisation twice. The second one was refused. Replay is the attack an agent is most likely to perform by accident, halfway through a retry it should not have attempted.
If exposure cannot be read, nothing goes through
I fed the checks a positions payload they could not parse. The order was refused rather than waved through on an assumed zero exposure.
This is the whole design in one behaviour. When the system is confused, the answer is no. Every one of those refusals costs an outage when a data feed hiccups. Somebody accepted that cost on purpose.
The mandate’s own arithmetic
Order size against the cap, new exposure against the ceiling, leverage, trades already used today, the market and instrument type, minimum liquidity and company size, and the never‑touch list.
A fifty‑dollar cap bounds the damage of any single mistake at fifty dollars. Whether the strategy inside the cap makes money is a different question the cap cannot answer.
You can narrow a permission. You cannot loosen one.
This one is not really about the order. It is about the file the order cites. My fifty‑dollar cap, raised to five thousand on its way to disk, was refused at the moment the mandate was written, so the mandate was never committed and every order citing it has nothing to stand on.
My favourite artifact in the whole repo is a test asserting that the agent has no reachable function that could widen its own permissions. It walks the syntax tree of everything the agent can reach and fails if any of it can open the mandate store for writing. Most test suites check what the code does. This one checks what the code must never become able to do.
“Structural guarantee: nothing the agent can reach writes the mandate store.”
agent/tests/test_no_set_mandate_tool.py2
One order out of seven reached a broker
The ordinary one. Nothing retries, so a timeout leaves you unsure whether it arrived, and a separate reconciliation step gets read‑only broker access, structurally, so it cannot resend an order while working out what happened.
Six refusals, each naming its rule. That is the finished picture.
Now you try.
The same eight checks, running in your browser. Write the mandate, build an order, sabotage the machine if you like, and send it. Load one of my seven, or invent a worse one.
1 · The mandate
The permission file. An agent may propose one. Only a function outside its loop may commit one.
2 · The order
What you are asking the agent to send to a live account.
3 · The answer
A refusal always names the rule. That is the part worth copying.
No order sent yet
Waiting
Build an order, or load one of the seven above, then send it.
A reconstruction of the check series described in the teardown, running on your device. No account, no keys, nothing leaves this page. The eight checks and their order are ours: the article names the probes and the refusals, not a canonical sequence.1
The part that decides is the smallest part of the repo.
Vibe‑Trading is an open‑source finance research agent from the HKUDS lab at the University of Hong Kong. MIT licensed, created 1 April 2026, 33,164 stars. Inside, it is bigger than the name suggests: 2,488 files, of which ninety are skill definitions written for the agent itself and 462 are an alpha library.
Much of that bulk is research machinery. The decision layer, meaning the mandate, the checks, the kill switch and the journal, is a rounding error by file count, and it is the part that decides whether a bad afternoon stays bounded.
SKILL.md files: 90 bundled skills plus the agent briefagent/src/live/: the mandate, the checks, the kill switch, the journalSources. File counts read from the repository tree on 10 September 2026.2 Safety‑layer squares are placed illustratively, not by path.
Thirteen connectors audited. Not one will attach a stop at order entry.
I checked all thirteen that existed when I wrote this. Twelve had no concept of it, and the one function with stop parameters is disabled on live accounts. A fourteenth, Zerodha, landed the day before this page pulled its data, and has not been checked. If entering with a stop attached is how you trade, this is not yet your tool, though a connector is a few hundred lines of MIT‑licensed code if you want to change that.
The sharpest symptom is on eToro, where editing a stop on a live position is switched off entirely, because loosening a stop can pull more account funds into margin and the system refuses to authorise what it cannot measure in advance. That refusal is the safety layer being consistent, declining what it cannot bound, at the price of a feature you might want.
Zerodha, the fourteenth, arrived after the audit and is not shown.
The permission model thinks in dollars of stock. Forex and CFDs are margin products, where a position can be far larger than the cash behind it.
Sources. Connector audit as published in the teardown.1
China, India and Korea get dedicated engines. Europe gets one data loader.
US coverage is solid. There are smaller signs of a fast‑moving young project too: stale docstrings, and a run‑fingerprint module the README advertises but nothing feeds yet.
Sources. Coverage assessment as published in the teardown.1
The agent removes the effort. It does not remove the arithmetic.
Testing fifty strategy ideas in an afternoon used to be impossible, and the impossibility was a kind of protection: the more strategies you try against the same history, the more certain you are to find one that fits the past by luck.
Below, every strategy is pure noise. None of them has any edge at all. Turn the dial up and watch the best of them start to look like a strategy.
With five years of data, Bailey and co‑authors put the ceiling at 45 independent configurations before pure luck is almost guaranteed to hand you an in‑sample Sharpe of 1.3 The wider literature agrees on the direction: Harvey, Liu and Zhu ask a newly discovered factor to clear t > 3.0 once the 316 already published are priced in,4 and Hou, Xue and Zhang found 82% of 452 published anomalies fail to replicate under a multiple‑testing hurdle.5
Computed in your browser: 45 random walks with zero drift, ranked by in‑sample Sharpe. Fixed seed, so every number beside the chart is reproducible.7
Two teams, arriving independently, at the same four organs.
At Purple we build in this space, so weigh this accordingly. Our Axiory MCP server connects an AI agent to a cTrader account over FIX 4.4, and it arrived at the same four safety mechanisms independently. Two teams landing on the same four organs is the strongest evidence I have that these are not decoration.
It hits the same stop‑loss wall from the other side, solving it with an automated three‑step workflow rather than stops at entry, and FIX 4.4 denies it something Vibe‑Trading has: any view of account balance or margin. It is still pre‑production.
Pre‑order risk checks
A fixed series that fails closed. Confusion resolves to a refusal, never to a guess.
A kill switch
Outside the model’s reach. A file, not a flag, and unreadable counts as on.
A paper mode
The default, not the demo. Weeks of it before a mandate touches real funds.
An append‑only journal
Hash‑chained, tamper‑evident, and it stops writing once the chain breaks.
Run it yourself, in twenty minutes.
The entire safety layer runs on the Python standard library plus one common data package. You can exercise the part of this project that decides whether an order is allowed without an account and without a key. That is the experiment, and it is where my seven orders came from.
- Install Python 3.11, 3.12 or 3.13. Nothing older, nothing newer: 3.14 fails deep inside a dependency with no prebuilt wheel.2 The research half also wants a model API key you pay for and free data‑source keys for the markets you care about, so get those first.
- Clone the repo. Point your own coding agent at
agent/SKILL.md, a 23 KB brief written for AI agents working on this codebase, and at the wiki, before it touches anything else. - Have it drive the pre‑trade checks with a made‑up mandate and made‑up orders. No brokerage account: you will not need one for a long time, and paper is the default for weeks before a mandate touches real funds.
- Read the refusals it brings back. That is the whole exercise.
When the system is confused, the answer is no. Somebody accepted that cost on purpose.
Every one of those refusals buys an outage when a data feed hiccups. That is the trade, and it is the right way round. Trace a single number in any answer back to the data call that produced it. If you can, you are looking at research. If you cannot, you are looking at generated prose with figures in it, which is worse than useless because it is persuasive.
Own the stack
Run Vibe‑Trading yourself. Your keys, your uptime, your upgrade path on a young repo. Most operations, most learning.
Keep the agent, drop the plumbing
Our Axiory MCP server handles the broker plumbing while the agent stays yours. Pre‑production today, so the operations cost is unknown.
The platform, operated for you
Takumis runs the agent, the connectors, the keys and the account plumbing for you. The operations cost is theirs, not yours, and you work inside what the platform exposes.
Every number on this page, and where it came from.
Every figure here traces to a published source, or is computed in your browser from assumptions stated beside it.