DEV Community

Christian Anderson
Christian Anderson

Posted on

My 5 trading bots reported every sale as a success. None of them ever sold anything.

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"
Enter fullscreen mode Exit fullscreen mode

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:

  1. Ask the LLM for suggested amounts.
  2. Discard the amounts, keep only their ratios.
  3. Pro-rate those ratios across 100% of free cash.
  4. 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/summarynot 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.


🤖 Drafted with AI assistance from my own homelab notes, logs and repos, then reviewed and edited before publishing.

Top comments (0)