Quant Trading Bot Devlog

한국어로 보기

Quant Trading Auto-Execution System - Going Live

This is the English version of a post originally written in Korean. See the Korean original and other posts(new tab).

Over the past few weeks, bits and pieces of live-trading prep kept showing up scattered across my devlog — catching a bug in a code audit, switching brokers, sending the first real order. Today I want to pull those pieces together into one structural writeup.

One thing up front: this post doesn't cover the logic that actually picks which stocks to buy (what I've been calling the "AI ensemble"). This is about the layer that wraps that judgment so it safely turns into a real order — not "what to buy," but "how to buy it safely."

There should be exactly one door money goes out through

The first principle I set when starting live-trading prep was that there should be exactly one code path that actually sends an order to the broker.

Before, each feature (rebalancing, signal-based trading, etc.) had its own order-sending code. Convenient, but risky — fixing a safeguard in one place could leave another path untouched.

So I merged every path into a single gateway. No matter which feature wants to place an order, it now has to go through this one gateway to actually reach the broker. The biggest win was being able to concentrate every safeguard in one place.

The broker needs to be swappable

This gateway isn't glued to any specific broker. Broker-specific details (authentication method, request format) live in a small adapter, and the rest of the logic is broker-agnostic.

I actually benefited from this. When I recently decided to switch brokers, the only thing I touched was this one adapter — none of the safeguards or the rest of the logic changed at all. Without this separation up front, that would have been a much bigger job.

Success, failure, and "unknown"

When you send an order and get a result back, it's tempting to split it into just success or failure. But in practice there's a third case — never getting a response at all, so you can't tell whether it succeeded or failed.

Lumping that in with "failure" is dangerous. If you assume "must have failed" and manually resend the order, but the original order had actually already gone through, you end up buying twice.

So I split the result into three explicit states — success, failure, and unknown. When "unknown" comes back, instead of immediately resending, it goes through a process that first confirms what actually happened.

How to avoid sending the same order twice

Preventing duplicate orders relies on two mechanisms working together.

One is attaching a unique tag to every order. If an order with the same tag comes in again, it's recognized as already processed and filtered out.

The other is ordering: a record saying "I'm about to place this order" gets written safely first, before the order is actually sent. If that record itself fails to write, the order doesn't get sent either — because if the record fails but the order still goes out, that order's existence could end up known to nobody.

When the ledger and reality disagree, trust reality

After an order goes out, I reconcile what actually got filled against the broker's own records. At first this reconciliation only looked at regular trading hours, and I later found out that meant orders filled outside those hours fell into a blind spot, completely invisible.

Now it checks every possible fill path there is. Whatever our own ledger says, if it disagrees with the actual fill record, I trust reality and fix the ledger.

My share and the whole account are different things

This bot manages only one part of the account, not the whole thing. So it's dangerous to assume "the quantity the bot thinks it holds" must always exactly equal "the account's actual holdings" — the account could have other holdings mixed in that the bot doesn't know about.

Instead I use a weaker but actually-correct condition: "the share the bot thinks it holds must always be less than or equal to the actual holdings." If that condition breaks, something is wrong, and it stops immediately and sends an alert.

Stopping is also a design decision

Loss-prevention is split into two layers.

One layer is automatic. If losses cross a set threshold, that part stops on its own.

The other layer is human. There's a separate master switch that can freeze everything instantly at any time — a last line of defense for situations the automatic layer doesn't catch.

Stopping and liquidating are kept as separate concepts. Stopping just freezes the current state — it's not the same as selling off what's held.

Safeguards also need to survive on their own

The processes actually running these safeguards are kept separate from the chat program I talk to. In the past, some background jobs used to run inside the same process as the chat bot, and restarting the chat program would cut those jobs off too.

Now monitoring, settlement, balance logging, and similar jobs are all split out into separate always-on services. Pending approval requests are also written to disk instead of memory, so they survive even if the bot restarts.

Wrapping up

Taken individually, none of these are exotic — duplicate-order prevention, status checks, ledger reconciliation, kill switches. Names you've probably heard before, wherever you look.

But building all of these out as a solo project meant less "designing it perfectly from the start" and more filling in the gaps one mistake at a time. Most of what's described here came from patching a hole an actual incident or code audit turned up.

If you're curious about the overall architecture, it's worth reading the project architecture post(new tab) first. This post is a deeper dive into how the "trading bot" piece of that is actually built.