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.
Process
Responsibility
Must survive
Feed handler
Websocket, normalise ticks, publish, persist
A strategy crash
Strategy
Signal logic only. Emits intents, never orders
A feed reconnect
Order manager
Intents → broker orders, idempotency, retries, position of record
A strategy crash and a feed outage
Risk gate
Pre-trade limits, loss limits, kill switch
Everything else
Monitor
Heartbeats, reconciliation, alerting
All 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 populatedclass 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 callback
Evaluate on a separate loop
Latency
Lowest
One loop interval higher
Risk of blocking the feed
High — slow logic drops ticks
None
Determinism
Poor; depends on tick arrival
Good; fixed cadence
Suitable for
Latency-critical only
Almost 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 timeclass 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
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.
QE
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.