Appearance

Personalize your experience

Mode
Accent Color
Layout
Direction
All articles

A Python Algo Trading Stack That Survives Production

Five processes, one source of truth for positions, and the specific design decisions that stop a working script from becoming an unexplained loss.

QuaTick Engineering11 min read2,001 words
PostLinkedIn
On this page

Almost every Indian algo trading system starts as one Python file that connects, subscribes, computes a signal and places an order. That file works, which is the problem — it works well enough that it goes live, and then it accumulates failure modes that are all consequences of the same decision: everything shares one process and one in-memory idea of the truth.

Five processes

System architecture

Production boundaries follow failure boundaries

Data, decisions, execution, controls and observation are separated so one crash does not erase every capability at once.

The broker websocket feeds a feed handler that normalises, publishes and persists ticks. Strategies consume published state and emit target positions. The order manager owns idempotent broker orders and the durable position record. A separate risk gate approves every send. A monitor receives heartbeats and broker reconciliation data and can trigger the external kill path. The broker is queried as an independent source of truth.
This is process isolation, not microservice theatre. All five processes can run on one machine initially while retaining separate state and restart behaviour.

The split below is not about scale — a retail system does not need horizontal scaling. It is about isolation: each boundary exists because something on one side of it should survive a crash on the other.

ProcessResponsibilityMust survive
Feed handlerWebsocket, normalise ticks, publish, persistA strategy crash
StrategySignal logic only. Emits intents, never ordersA feed reconnect
Order managerIntents → broker orders, idempotency, retries, position of recordA strategy crash and a feed outage
Risk gatePre-trade limits, loss limits, kill switchEverything else
MonitorHeartbeats, reconciliation, alertingAll of the above
Process boundaries and the failure each one contains

Target position, not orders

Process flow

Declarative intent converges after retries and restarts

The order manager computes only the difference between desired and broker-confirmed position.

The strategy declares a target such as long two lots. The order manager reads the durable current position, subtracts it from the target to compute a signed delta, passes that delta through the risk gate, sends only the required order and records fills. On the next evaluation the same calculation converges to zero, even after a restart, duplicate signal or partial fill.
An imperative “buy two more” message has no natural convergence point. A target position does.

This single change removes a whole category of bug. When the strategy declares a target position rather than an action, a missed fill, a duplicate signal or a restart mid-sequence all self-correct on the next evaluation — the order manager simply computes a smaller or larger delta.

intent.pypython
from dataclasses import dataclass @dataclass(frozen=True)class TargetPosition:    """What the strategy wants to hold, not what it wants to do."""    symbol: str    lots: int          # signed: negative is short    reason: str        # for the audit trail — always populated  class OrderManager:    def reconcile_to(self, target: TargetPosition) -> None:        current = self.positions.get(target.symbol, 0)        delta = target.lots - current         if delta == 0:            return         # A restart, a missed fill or a duplicated signal all converge here:        # the delta is computed from the position of record, so the system        # self-corrects instead of compounding. An imperative "buy 2 lots"        # API has no equivalent recovery path — it just buys 2 more.        self.send(            symbol=target.symbol,            side="BUY" if delta > 0 else "SELL",            lots=abs(delta),            idempotency_key=self._key(target, current),        )
Declarative intent. The strategy never needs to know what it did last time.

Idempotency

Decision tree

A timeout is unknown, not failed

Recovery queries broker state before deciding whether any resend is safe.

Persist a client idempotency key before sending. If an acknowledgement arrives, record the broker order. If the request times out, query the broker order book by client key. If found, adopt the existing order and do not resend. If not found and the broker confirms absence, retry with the same key. If state cannot be determined, stop and alert rather than risking a duplicate.
Automatic HTTP retries are unsafe for order placement unless the endpoint or client key makes the operation idempotent.

The scenario: you send an order, the request times out, and you do not know whether the exchange received it. Retrying risks a double position. Not retrying risks no position. Neither is acceptable, and the resolution is that the decision must be made before the ambiguity arises.

Generate a unique key per intended order, before sending, and persist it. On a retry, send the same key. If the broker API supports client order IDs, use it — the broker deduplicates. If it does not, query the order book by that key before retrying.

Data: store ticks, derive candles

Storing minute candles is tempting because it is what the strategy consumes. It is also irreversible: the first time you need to know what the book looked like at 09:15:43, the information is gone.

A tick store that does not become a liability

  • Append-only, with the broker’s timestamp and your receive timestamp

    The difference between them is your feed latency, and you cannot reconstruct it later.

  • Partition by date and instrument

    Query patterns are almost always "one instrument, one day". Partitioning that way keeps reads cheap without an index.

  • Persist in the feed handler, not the strategy

    So a strategy crash does not create a hole in the historical record.

  • Derive candles at read time and cache them

    Candle boundary conventions change more often than you expect. Deriving lets you fix a convention retroactively.

  • Record the gaps explicitly

    A reconnect leaves a hole. A gap you have marked is data; a gap you have not is a silent lie in every backtest that reads across it.

Parquet files partitioned by date on local disk handles a retail-scale tick archive without any database at all. Reach for a time-series database when queries become the bottleneck, not before.


The event loop

One decision here has outsized consequences: whether strategy evaluation runs on the websocket callback thread or on a separate loop.

Evaluate on the tick callbackEvaluate on a separate loop
LatencyLowestOne loop interval higher
Risk of blocking the feedHigh — slow logic drops ticksNone
DeterminismPoor; depends on tick arrivalGood; fixed cadence
Suitable forLatency-critical onlyAlmost everything retail

Unless you are doing something genuinely latency-sensitive, evaluate on a fixed cadence and let the feed handler do nothing but receive, normalise and publish. A blocked websocket callback silently drops ticks, and a strategy running on a partial tape produces decisions you cannot reproduce afterward.

Monitoring: alert on silence

System architecture

Presence signals flow into an absence detector

Components only publish heartbeats and state; the monitor owns clocks, deadlines and escalation.

The feed handler emits a tick heartbeat, the strategy emits an evaluation heartbeat, the order manager emits order and position state, and the broker supplies an independent position snapshot. The monitor checks each against a monotonic deadline, compares positions, groups rejection rates and sends alerts. Missing heartbeat is an incident even when no exception occurred.
“No signal” is a valid strategy result. “No evaluation completed” is an operational failure. Monitor them as different events.

The failure mode that costs the most is not an exception — exceptions are loud. It is a component that stops doing anything while everything around it looks normal. A dead feed handler and a quiet market look identical from the outside.

The four alerts worth having before any others

  • Feed heartbeat: no tick on a liquid instrument for N seconds during market hours.
  • Strategy heartbeat: no evaluation completed for N intervals. Not "no signal" — no evaluation.
  • Reconciliation mismatch: broker position differs from position of record. Page immediately.
  • Order rejection rate: rejections above a threshold, which usually means margin, a bad symbol, or a session expiry.
heartbeat.pypython
import time class Heartbeat:    """Components call beat(). The monitor asks stale(). Nothing else."""     def __init__(self, name: str, max_silence_s: float):        self.name = name        self.max_silence_s = max_silence_s        self._last = time.monotonic()     def beat(self) -> None:        self._last = time.monotonic()     def stale(self) -> bool:        # monotonic(), not time(): a system clock adjustment during the        # session would otherwise either fire a false alert or, worse,        # suppress a real one.        return time.monotonic() - self._last > self.max_silence_s
Absence detection. The monitor asks the question; components only ever report presence.

What to build first

Building all five processes before placing a single order is its own failure mode. The useful order is to get one instrument trading end-to-end with the non-negotiable parts in place, then split.

  1. Feed handler with persistence

    Before any strategy. You want the tick archive accumulating from day one, because you cannot retroactively collect it.

  2. Order manager with idempotency and a durable position record

    Even for a single strategy. This is the component that is painful to retrofit, because everything else assumes its interface.

  3. Risk gate with position limits and an external kill switch

    Before the first live order, without exception. See risk controls for an automated book.

  4. Strategy, as the thinnest possible component

    It should read state and emit a target position. If it is doing anything else, that thing belongs somewhere else.

  5. Monitor, with the four alerts

    Before you stop watching the screen manually — which is the point of all of this.

Frequently asked questions

Should a strategy emit orders or a target position?

A target position. When the strategy declares what it wants to hold and the order manager computes the delta from the position of record, a missed fill, a duplicated signal or a restart mid-sequence all self-correct on the next evaluation. A strategy that emits orders directly has to track its own fills, and a strategy that is wrong about its position places wrong orders confidently.

How do I avoid double orders when a broker request times out?

Generate a unique idempotency key for each intended order before sending it, and persist it. On retry, send the same key so the broker can deduplicate, or query the order book by that key before retrying. A timeout is an unknown state, not a failure, and the correct response is to find out what happened rather than to resend blindly.

Should I store ticks or candles?

Ticks, append-only, with both the broker timestamp and your receive timestamp. Storing candles is irreversible — the first time you need to know what the market looked like at a specific second, the information is gone. Derive candles at read time and cache them, which also lets you correct a candle-boundary convention retroactively.

Should strategy logic run on the websocket callback?

Generally no. A slow callback silently drops ticks, and a strategy running on a partial tape produces decisions you cannot reproduce afterward. Unless the strategy is genuinely latency-critical, evaluate on a fixed cadence and keep the feed handler doing nothing but receive, normalise and publish.

What monitoring does an algo trading system actually need?

Alerts on absence rather than on errors, because a dead component and a quiet market look identical from outside. The four that matter first are a feed heartbeat, a strategy evaluation heartbeat, a position reconciliation mismatch against the broker, and an order rejection rate threshold. Use a monotonic clock for staleness checks so a system time adjustment cannot suppress a real alert.

QuaTick Engineering

Platform & execution

The engineering team builds QuaTick’s order routing, market-data pipeline and broker integrations. These pieces cover the parts of automation that only show up in production.

Choosing a Broker API for Algo Trading in India

What to evaluate in an Indian broker API before you build against it: rate limits, websocket subscription caps, order-type coverage, historical data access and failure behaviour.

QuaTick Engineering9 min read

Risk Controls for an Automated Book

The risk architecture an automated trading system needs: position limits, loss limits, rate limits, a kill switch outside the strategy process, and daily reconciliation.

QuaTick Risk Desk9 min read

From Paper Trading to Live: A Go-Live Checklist

A go-live checklist for automated strategies: what paper trading cannot test, the failure modes that only appear with real fills, and how to structure a first month live.

QuaTick Risk Desk9 min read