I am building an automated trading system on my own. Rust + Python + React, with quotes, backtesting, automated orders, and signal trading all in place.

Looking through my PR history, the work has been BackgroundTask supervisor, pre-condition guard, DeploymentStatus::Abandoned transition, stuck monitor, LocalSim prices — one safety lock after another for a system I haven’t even turned on. At first this felt off. Why build the off switch instead of more features?

Automated trading is just like that. Mistakes move real money, no one is around to wake me up at night, and there is no real-data verification yet. With those three stacked, “halt safely” came before “run well,” and I started building from there.

ETF rebalancing and automated portfolio management

What I wanted to build was an asset management system for securities. A system that defines a portfolio with a mathematical formula and adjusts weights to match, automatically, without any human in the loop.

The starting point was ETF-based rebalancing. ETFs come with diversification built into a single symbol, so the decision cost of picking stocks stays low, and the trading frequency is low enough that costs stay manageable. That made them a fit for the first stage of automation at the allocation level. The flow is: validate an allocation strategy with backtesting, deploy the validated strategy to an account, and let it adjust weights on a schedule.

Next came the single-stock level. Factor screening narrows down the candidates, backtesting validates the result, and signal trading automates entry and exit. The decision unit is finer than allocation. The underlying flow is the same — define something mathematically and let it run on its own.

Rust + Python split and broker abstraction

The initial code skeleton had two pieces. The Rust + Python split and the broker abstraction. Neither was made for safety.

The Rust + Python split was a workload separation.

The Rust server handles the real-order flow — order placement, balance sync, signal evaluation. I picked Rust because the operating environment (Oracle Cloud ARM Always Free) is resource-constrained, so a small memory footprint helped, and the latency of signal trading felt safer without GC pauses. Wanting to learn Rust was part of the motivation too.

Stateless computation like backtesting and factor screening lives in the Python quant engine, with no DB dependency. vectorbt covers backtesting well, so there was no reason to reimplement it in Rust.

The broker abstraction puts KIS and LocalSim behind the same trait. The order executor takes a trait, and the real KIS implementation and the LocalSim implementation satisfy the same interface. LocalSim started out as a way to develop in environments without KIS access.

Looking back before launch, the two of them had become the first layer of safety. The Rust + Python split divided the blast radii of two processes, and the broker abstraction made every automated flow runnable as a dry-run on LocalSim.

Halt, block, detect, and simulate are the safety layers I deliberately added to the automated trading flow.

BackgroundTask supervisor and abandoned transition

Halt was the first safety layer I added.

The first piece was the BackgroundTask supervisor. Long-running background tasks — the signal engine, risk monitor, US market poller — can die from a panic, and I thought the dangerous state was “dead, and nobody knows.” The supervisor detects the panic and restarts or permanently halts based on the policy. Defining restart policy as a trait + enum let me plug a different policy into each task, which felt clean.

Next, I added the DeploymentStatus::Abandoned transition. Originally, if liquidation didn’t finish, the operator had to stop it manually, but leaving something stuck for days felt risky. I made the deployment transition to Abandoned when liquidation shows no progress for a defined period. In Abandoned, automated trading halts and no further orders are issued.

The force-abandon API came in the same wave. As a backup for when the automatic transition doesn’t fire, I added a way to stop everything from the outside with one call. The goal was to make sure no state could ever become “impossible to stop.”

The last piece was the scheduler’s automatic abandoned transition. At a fixed time each day, the scheduler checks liquidation states and marks abandoned anything that meets the criteria. Even without daily manual checks, abnormal states don’t pile up.

pre-condition guard and chat_id gate

Block came next. If halt is a lock during operation, block is a lock at the entrance.

The first was the deployment pre-condition guard. With no broker credentials registered, both deployment activation and liquidation are refused. Originally, activation fetched credentials mid-flow and failed when they were missing, but the failure came too late. I moved the check just before activation, so a missing credential gets rejected at that point.

The Telegram chat_id onboarding gate was a similar decision. Once automated trading starts, fill notifications, signals, and reconciliation alerts need to arrive immediately, but if a deployment activated without an alert channel set up, there was no way to know when an issue arose. So I added a gate that throws a strong warning when a user without a registered chat_id tries to create a deployment.

Both the pre-condition guard and the chat_id gate started at activation time and gradually moved earlier. Blocking late lets bad state seep deeper in, and the cost of rolling it back gets bigger.

stuck monitor and balance reconciliation

For halt and block to work, the abnormal state has to be visible first. Detection was the next batch of work.

The LiquidationStuckMonitorTask came first. It periodically checks the progress of liquidations in flight, and if nothing changes for a defined period, it fires a Sentry alert. It pairs with the automatic abandoned transition — the signal that “it’s time to auto-halt” becomes visible to a human.

Balance reconciliation was a different shape. The real KIS balance is compared against the strategy_position ledger I maintain, and a Telegram notification fires on mismatch. It’s a check against the broker’s view of truth to see whether automated trading reflects exactly what was intended. When drift shows up, the reconcile API corrects it.

Last, I cleaned up the alert channels. I split Sentry and Telegram into separate Trading and System channels — real-order flow events go to Trading, and operational/system anomalies go to System. With solo operation, alerts buried in noise make detection itself meaningless, and that was the motivation.

LocalSim and dev seed

Simulation was last. I wanted to be able to run the whole circuit before any real capital moved.

The LocalSim price stream was the starting point. Originally I faked prices with a monotonic function. That was enough when I only needed to confirm the circuit ran, but when I wanted to verify signal evaluation and rebalancing against varied patterns, it wasn’t enough. So I swapped the model to GBM. Giving each symbol a different drift and volatility produces a stream where rises, falls, and flat periods mix.

The dev trading_credentials seed was another tool I built for simulation. Running the real-order flow locally needs credentials, and injecting them manually each time was tedious. I wrapped an idempotent UPSERT seed command into a Makefile target. Since reproducing the dev environment became one line, I started running checks more often.

Without a simulator, the first real order becomes the first integration test. In automated trading, I think that’s the most expensive kind of test. Only after seeing a clean run on LocalSim for days do I think I’ll have the confidence to move to real orders.

Retrospective

Building the off switch for a system that hasn’t been turned on felt odd at first, but after going through it, I think it was the most reasonable order of work for the combination of automated trading + solo + pre-launch. Building safety locks before more features cuts down on the shakiness at the moment of going live.

There’s still a way to go before turning it on. I want to see the full automated flow run cleanly on LocalSim for a meaningful period. Moving to real orders without confirming that signal generation, order placement, fill, balance sync, and liquidation all cycle through cleanly is risky. Each safety layer also has to be broken at the circuit level once — does the supervisor actually catch panics, does the guard actually refuse bad activations, does the stuck monitor actually fire its alert?

The first real order is itself a safety boundary. I plan to verify with a small position for a period, then scale up in stages. Going to full size in one step would go against everything I built.

The next retrospective will be written after going live.

References