Frontend
My 5 trading bots reported every sale as a success. None of them ever sold anything.
Christian Anderson DEV Community
2 views
I spent a few months building an automated trading system: five bots wired
together in n8n, a couple of local LLMs making the calls, Trading 212 for
execution, Discord for the play-by-play. Every morning my phone lit up with
green ticks. Position Closed ✅. Rebalanced ✅. Trade executed ✅.
Then I actually read the account.
Demo account realised P&L: −£424.
Live account balance: £4.83. No bot-initiated trade on it in months.
One bot still trading — and it was selling a position at a loss every single
morning to dump the cash into a tracker fund.
The bots weren't losing money because the strategy was bad. They were losing
money because almost none of them did what the Discord messages said they
did, and nothing in the system was capable of noticing. This is the autopsy.
Every bug below shares one signature, and it's the only lesson that matters:
the system reported success at exactly the point it was failing.
Not financial advice. This is a debugging story about automation, and the
reason it's worth your time is that the failure mode isn't specific to
trading — it's specific to any pipeline that reports its own results.
Bug 1: the sell order that sold a string
The swing-trading bot's "close position" step POSTed this as the order body:
"=$json.ticker"
If you've used n8n you already see it. That leading = marks an expression,
but there are no {{ }} around the field — so instead of interpolating the
ticker symbol, it sent the literal seven characters $json.ticker to the
broker's API. Every sell was rejected. Not some. Every sell the bot ever
attempted, for months.
Here's the part that turns a bug into a disaster. The node was configured with
onError: continueRegularOutput and alwaysOutputData: true. So when the
broker rejected the order, the workflow carried on to the next step as if it
had succeeded — and the next step posted to Discord:
Position Closed ✅
I have a Discord channel full of hundreds of successful sales that never
happened. The positions from April and May were still open in July. The
"+£324 unrealised gain" I'd been feeling good about was just two stocks the bot
had bought and was now structurally incapable of selling.
Lesson: an error handler that swallows the error and continues is not error
handling. It's a machine for manufacturing false confidence.
Bug 2: the circuit breaker that computed whether to stop, and then didn't
The same bot had a safety system I was quite proud of. Four rules: stop trading
after a 15% drawdown, three losses in a row, six trades in a day, or a 5% daily
loss. The node dutifully computed two variables — circuitBreakerTriggered and
pauseReason — on every run.
Nothing read them. No downstream node referenced either variable. The
circuit breaker was a function that calculated the correct answer and then threw
it in the bin. All four safety rules were unreachable code. I had built the
smoke detector and never connected the battery, and because it lit up on the
dashboard when it computed a trigger, I believed it was armed.
Bug 3: the LLM that was fed its own source code
The "autonomous trader" asked a local model (qwen2.5:7b) what to buy. When I
finally logged the exact prompt going over the wire, the model wasn't receiving
a prompt. It was receiving ~2.4 KB of copy-pasted JavaScript — the source of
a Code node someone had pasted into the prompt field, ${cash} and
${ownedTickers} and all, as literal text.
So the model never saw the research, never saw the portfolio, never saw how much
cash was available. It was pattern-matching against raw JS and emitting plausible
trades into the void. And the only guard rails were amount > 0 and
amount <= cash — applied per trade, not in aggregate. Nothing stopped it
proposing five trades that each spent 100% of the cash.
The circuit breaker here read a file off an NFS mount with
cat ... 2>/dev/null — so if the mount was down, the check returned empty and
the breaker failed open. The one condition under which you most want a
safety system to hold is the one under which it silently disabled itself.
Bug 4: the "rebalancer" with no rebalancing in it
This is the only bot that was still trading, and it's the one that did the
measurable damage. Its name promised portfolio rebalancing — target allocations,
drift thresholds, trim-the-winners logic.
There were no target allocations. No drift thresholds. No rebalancing
algorithm of any kind. What it actually did:
Ask the LLM for suggested amounts.
Discard the amounts, keep only their ratios.
Pro-rate those ratios across 100% of free cash.
Sell a whole position to raise that cash first.
So every weekday at 07:30 — the cron said weekly; it was set to daily — it
liquidated a holding and shovelled the proceeds into a single tracker fund. The
realised losses on those forced sales, from the logs: −£7.70, −£1.87,
−£114.82, −£35.26, −£6.84. And one step (Calculate Quantity) would throw
after the sells had already executed, with no error handler on any of its 27
nodes — so on a bad run it would sell everything and then strand the cash,
silently.
Bug 5 and friends: the ones that never touched reality at all
Two more bots rounded out the set:
The daily briefing bot fetched account status from
/equity/account/summary — not a real endpoint on the broker's API. So
every "your portfolio is worth £X" message it ever sent read £0.00, and I'd
stopped noticing.
The market scanner filtered for cheap stocks assuming prices came back in
pounds. The London exchange quotes in pence (GBX). Its price * 100 maths
meant it only ever admitted sub-1p stocks, so its watchlist was permanently
empty — and when the price feed (an unauthenticated Yahoo scrape, the actual
oracle for three bots' order sizing) had an outage, a null fell through to
the else branch and the bot cheerfully reported "market tide AGAINST
you 🔴". An outage looked exactly like a bearish signal.
Oh, and the workflow JSON had a live API key and a Discord webhook token sitting
in it in plaintext. Which I found while writing this. Rotate your secrets.
The one lesson
Look at what these bugs have in common. It isn't the language, or n8n, or LLMs,
or trading. It's this:
Every single failure was silent, and every single one reported success.
The rejected sell announced "Position Closed ✅."
The disconnected circuit breaker lit up as if armed.
The unfed model returned confident trades.
The endpoint-that-doesn't-exist reported a tidy £0.00.
The price-feed outage rendered as a market signal.
I didn't lose £424 because I made bad trades. I lost it because I built a system
that could not tell me it was broken, and then I trusted the only thing it
could tell me — that everything was fine. Green ticks are not evidence. They're
the cheapest thing in the entire pipeline to fake, and a system under no
pressure to be honest will fake them by default.
If you take one thing from this into your own automation, make it this:
The most important question about any automated system is not "does it
work?" It's "if it stopped working right now, how would I find out?"
If the honest answer is "a message would stop arriving" — you're fine, absence
is loud. If the answer is "the messages would keep coming and they'd all say ✅"
— you have already built my trading bots. You just haven't read the account yet.
What "fails loudly" actually looks like
I threw the whole thing away and rebuilt it as the most boring system I could:
plain Python, under systemd timers, no visual workflow canvas, no LLM in the
decision path. It makes far fewer decisions than the bots did. The difference
isn't the cleverness — it's that every part of it is built to tell me when it's
broken:
A missed run pages me within the hour. I tested that by deliberately
breaking it, because — and this is the whole point of the article — a healthy
system reports ok under both the correct schedule and the broken one, so a
quiet week proves nothing. You have to make it fail on purpose and watch the
alert arrive. (Mine didn't, the first time. The detector had a 24-hour blind
spot. I only found it by faking a dead run.)
The signals are a pure function of the indicators, not an LLM's free text
parsed with a regex. Same inputs, same output, every time — auditable, and it
can't quietly drift.
It prints its real state, including the ugly parts. When the price feed is
down it says "data unavailable," not "market tide against you." An outage
looks like an outage.
It's covered by a small pile of tests that assert the arithmetic, so a
refactor can't silently reintroduce a price * 100 unit bug.
It is dramatically less impressive to look at than five bots and a wall of green
Discord ticks. It is also the first version I actually trust, precisely because
it is willing to tell me it failed. That trade — clever-but-lying for
boring-but-honest — was worth every penny of the £424 it cost to learn.
I wrote the full forensic breakdown up as a short guide — every node, every
rejected order, and how I rebuilt each piece to fail loudly — plus a checklist
you can run against anything you leave unattended. It's
*pay-what-you-want, including free*.
Take it, use the checklist, pay nothing if you like.
Nothing here is financial advice or a recommendation to trade. It's a
debugging story; the numbers are my own account's, warts and all.
Read original: https://dev.to/c1-anderson/my-5-trading-bots-reported-every-sale-as-a-success-none-of-them-ever-sold-anything-220c
← Previous
Fortmail: agent-operated email in one Cloudflare Worker (MIT)
Next →
Putting Authelia in front of my whole homelab: what worked, what didn't, and the £0 lesson that nearly undid all of it
Related
Yul Dark Arts: Understanding the EVM from first principles
Frontend
1
Dev.to (EN Zone)
Browser first: the one rule behind every tool on KitDev Space
Frontend
1
DEV Community
The one tell of AI-built sites, and a CSS block that fixes it
Frontend
2
DEV Community
The money was already approved. It stopped at a sheet of paper nobody could read.
Frontend
2
Dev.to (EN Zone)
Comments0
No comments yet — be the first