Autonomous bot fleet and backtest engine: reliability engineering
Personal, internal R&D in an NDA fintech domain: ten autonomous services that make real-time decisions off an external event stream. This case is not about returns — it's about the engineering: data collection with provenance, daemons that don't die quietly, backtesting against real history instead of faith in a pretty curve, and a risk loop that shuts things down before a human would.
An autonomous system breaks quietly
A background process has no user to complain. It keeps spinning, keeps writing logs and keeps honestly reporting "alive" — while it no longer receives data and makes decisions off stale state. Hours pass between the break and its discovery, and the whole time the system looks healthy.
Three real scenarios from this project. First: an external API changed a single word in an
instrument name — the parser stopped recognising part of the stream, and the breakage was
noticed 10 hours later. Second:
socket open, no exceptions, events flowing — but one specific event type hadn't arrived for
20+ minutes; the generic "last event" watchdog stayed silent because the other types kept
coming. Third: a watchdog logged
restarts=0 failures=0
for hours while two processes were dead —
pgrep -f
without an anchor was matching neighbouring shells whose argv contained the pattern, including
the checking script itself.
And a fourth class, the most expensive one: the system doesn't crash, it quietly lies. The offline simulator determined trade outcomes from the wrong reference moment — agreement with the real settlement rule turned out to be 51%, i.e. a coin flip. Everything built on top of that simulator looked like analysis for months and was noise. Until someone checked the settlement rule itself, there was no way to tell the two apart.
Five loops, each one verifiable on its own
A real-time decision system isn't "an algorithm". It's five independent loops: data collection, daemon liveness, observability, offline hypothesis testing, and risk control. Any one of them can break on its own — so each has to be verifiable without the others.
Event-driven WS stream + 5s REST catch-up. Every row lands in SQL with provenance: source, ingest time, writer
Python double-fork launcher instead of bash detach, systemd Restart=always + journald
A watchdog per event type, a canary for external-API schema drift, a status digest
Excursion replay over historical ledgers: real price excursions, dedup, regime segmentation
Scheduled multi-layer monitor, HALT marker file as kill-switch, resume only through the script
Bash cannot detach a daemon on macOS — tested, not assumed
Restarting a process from a script looks trivial right up to the first hang. Five backgrounding
patterns were tested empirically, and all five left the parent shell stuck in
__wait4
on the supposedly disowned child — one process hung that way for 1 h 32 min:
nohup … & disown— the classic that doesn't workset -m+ disown, subshell launch, nested fork variants — same result- macOS ships no
setsid(1), so there is no "correct" shell answer at all
The fix is one shared Python launcher:
fork → setsid → fork → execvp,
with the grandchild guaranteed to end up at
PPID=1.
Every restart in the project goes through it; launchd jobs additionally set
AbandonProcessGroup,
or the OS reaps the children along with the script that spawned them.
Liveness is measured per event type, not per process
The baseline watchdog tracks the timestamp of the last event of any type and kills the process if silence exceeds 360 seconds — a threshold found empirically, since 180 s produced false positives on normal cycle gaps. The supervisor brings the service back, so the watchdog can afford to simply exit with a distinct return code.
But that only cures a fully dead connection. The real incident was subtler: one event type stopped arriving while everything else flowed as usual — and the generic silence counter never fired. Hence the rule now applied to every service: liveness is not "is the process up", it's "when did I last receive the thing I exist for". Every critical event type gets its own counter, the health report shows "minutes since last" per stream, and there's a preventive re-subscribe on a timer.
A liveness check has to be honest
pgrep -f
patterns must be anchored to the end of argv
(…/book_recorder\.py$).
Without the anchor the check matches any neighbouring shell that happens to carry the string in
its command line — including the calling script itself. Monitoring that counts itself as the
live process is worse than no monitoring: it reports green on a dead system.
SQL-first, with provenance on every row
The rule for all new writers: data goes to SQL, and JSONL is allowed only as a rolling buffer
with ≤48 h retention and daily rotation. Every dataset gets a row in
data_provenance
(source, data status, audit notes, whether it's safe to delete); every record carries
ingested_at
and a link back to its source dataset. The trigger was mundane: an audit found tens of
gigabytes of duplicates that nobody could delete, because nobody could prove where they came
from.
- live writers open the connection on flush and close it immediately — the lock is held for milliseconds instead of the writer's entire lifetime
- a continuously written database is single-writer; analyst connections are
read_only=Trueonly - a new live writer gets its own database file rather than sharing one with an existing writer
Decision records are never deleted
The postmortem that produced a rule of its own: roughly five weeks of decision history vanished under disk pressure — routine archive rotation removed files that cannot be reconstructed. The answer is append-only storage: harvest every entry and close record from live runs and from archives, dedup by line hash (idempotent, so it can run every 15 minutes), sync to an offsite copy. The key rule is baked into the pruning code: harvesting runs before any deletion, and if it fails, the deletion is cancelled entirely. Disk is cheaper than data, always.
Event-driven reaction instead of timer polling
A 30-second poll is a perfectly good design when events are rare. Here the decision windows live for seconds, so reaction is driven by the incoming WS event and REST stays a catch-up scan on a 5 s interval. The full path is measured component by component, so latency arguments can be settled with numbers rather than opinions:
- connect and subscribe — ~40 ms; detail request — ~26 ms; external state read — ~35 ms; submit — ~41 ms
- cryptographic signing — 0.24 ms after swapping the pure-Python backend for a native one; before that it was three orders of magnitude more expensive and ate the entire budget
- background caches and parallel requests instead of sequential waits — tens of milliseconds more
- fast path end to end — 250–350 ms; you can't go lower without an architecture change, the rest is network-bound
Nothing exotic — it all runs on discipline
WS clients, decision loops, watchdog coroutines living next to the working code
Event-driven reaction as the primary path, polling only as a 5s catch-up scan
SQL-first storage, data_provenance, single-writer plus read-only analyst access
10 services on one host, restarts without human hands, logs in one place
Background jobs on macOS with guaranteed PPID=1 and AbandonProcessGroup
Ledger vault sync, outcome resolvers, periodic reconciliation
Only where a consumer genuinely needs the raw stream; everything else goes to SQL
A bug is closed by the test that would have caught it — otherwise it comes back
Status of every service plus "minutes since last" per event stream
Rules with the date they were proven, the incident, and their invalidation conditions
The system didn't become unbreakable — it became honest
per-event-type watchdog threshold plus a schema-drift canary
on one host, Restart=always + journald, restarts with no human in the loop
agreement with the real settlement rule after the fix — across the verified sample, which is still small
An invariants log instead of oral tradition
Every rule that gets discovered is written down as a numbered invariant: the date it was proven, the incident that produced it, a "how to apply" section, and — as a separate item — the conditions under which the rule stops being true. Rules don't come from opinion: they come from incidents and carry a way to reproduce them. That's what stops the same mistake from being rediscovered six months later on a new service.
A backtest that doesn't flatter you
Validating a fix on a live run means waiting days and still having no statistics. Instead, each fix is replayed over historical ledgers using excursion replay: every closed record stores the real extremes of the move, so the counterfactual is computed from how things actually moved rather than from a re-simulation. The method is fixed as a set of non-negotiable rules:
- assumption-free metrics first — distributions and hit rates at each level; if there's no movement at all, no exit tuning will help
- dedup replicas: the same signal is duplicated across configurations roughly 12 times — without dedup the sample is fake-inflated
- segment by market regime and by signal type; a blended number hides a system that wins in one regime and loses in another
- bounds for ambiguous cases: when the intra-bar ordering is unknown, compute the optimistic and pessimistic estimate and accept the conclusion only if both agree
- random-entry control: the same exit logic on random entries must lose — otherwise the "effect" came from the exit mechanics, not the signal
- n ≥ 30 per cell, out-of-sample and walk-forward folds, and a multiple-testing correction
The practical effect is unpleasant and useful at the same time: most ideas that looked like they worked don't survive this method. Of eight mechanical strategies tested, two survived. That is the result — knowing the other six don't work before paying for them.
The risk loop halts before a human would
A multi-layer monitor runs on a schedule and checks several independent halt conditions. A halt is expressed as a marker file that every process can see — a simple, dependency-free kill-switch. After that it's discipline: halt markers are never hand-edited, and resuming goes only through the dedicated script that checks why the halt happened. New filters get their own rule: audit mode first, where the filter only logs its decision, and only after enough data does it earn the right to block.
The overall outcome, stripped of detail: the numbers the system produces can be trusted, and failures became visible in minutes rather than hours. For an autonomous system that is the headline feature — everything else is built on top of it.
Where else the same methodology applies
This case isn't about trading. It's the generic problem of "an autonomous process makes decisions off an external event stream, and nobody complains when it breaks". The loop transfers almost unchanged:
- → External API integrations where the format changes without warning — a schema-drift canary instead of a postmortem
- → Background workers and queues where "the process is up" ≠ "work is happening": liveness per event type, not per PID
- → ML and analytics where offline evaluation disagrees with production — check how the target metric is computed before touching the model
- → Autonomous LLM agents that spend money or take actions — they need the same kill-switch, audit mode and limits
- → Auditable datasets — row-level provenance is the only way to answer, six months later, where a number in a report came from
- → Telemetry, IoT, logistics — the same class: event streams, tight reaction windows, failures without a single error message
- The double-fork launcher plus systemd and launchd unit templates — background jobs that genuinely detach
- Per-event-type watchdogs, an external-API schema-drift canary, and a health report with time-since-last-event
- The SQL-first layer with data_provenance, the open-on-flush pattern and a writer / read-only split
- Append-only record storage: harvest before any pruning, cancel the prune if harvesting fails, keep an offsite copy
- The offline validation method: dedup, segmentation, bounds for ambiguous cases, random-entry control
- The invariants-log format: date proven, incident, how to apply, and when the rule expires
If a process runs unattended, the question isn't whether it breaks — it's whether you'll find out
Two measurements are the sensible place to start: how many minutes until you learn about a silent failure, and whether your offline evaluation computes the same thing production does. Those two checks usually surface the first two or three holes — before the holes surface themselves.
The 5,000 ₽ audit — with a concrete report and quote
I'll tell you what to deploy in your business first, what the payback looks like, and whether you need AI for the task at all (sometimes you don't).
Or just send your question — I reply within 2 hours