Research and Execution Are Not the Same Program
A systematic trading workflow is really two programs sharing one codebase. What breaks when the boundary between them blurs — and the rule that keeps it enforced.
One process, two jobs
Somewhere in a systematic trading codebase there is usually a function that fetches a quote, a function that runs a backtest, and a function that submits an order — and it’s tempting to let all three live in the same running process. They share imports. They share a database connection. Sometimes they share a config object. This is convenient right up until the moment it isn’t, and that moment tends to arrive without an exception, a stack trace, or anything else that would show up in a log.
Research and execution are not variations on the same job. They have opposite reliability profiles, and a codebase that doesn’t encode that difference somewhere will eventually let one borrow the other’s resources at the worst possible time.
What “different jobs” actually means
Research is allowed to be slow. It’s allowed to crash. Its worst failure mode is “the analysis takes longer than expected” or “the notebook died and I reran it.” Correctness, for research, means the conclusion is right — the backtest numbers, the feature ranking, the parameter sweep. Nobody is harmed if a research job takes an extra ten minutes.
Execution has the opposite shape. It has to be available at a specific moment, not eventually. It has to behave the same way on the tenth run as on the first. Correctness, for execution, means the state is right — the account snapshot it just read, the order it’s about to send, the position it believes is open. A slow execution path isn’t an inconvenience; it’s a missed fill, a stale price, or a double-submitted order.
Put those two reliability profiles in the same process, and the slower, more forgiving one will eventually starve the faster, less forgiving one of something it needs.
The shapes this failure takes
None of these need anything specific to a broker, a strategy, or a market — they’re generic enough to show up in any systematic pipeline that grew research and execution together instead of apart.
A shared connection pool. A backtest sweeping years of daily bars issues a handful of large, slow queries. An execution loop needs to write “order submitted” to the same database a few hundred milliseconds later. If they share a connection pool, the backtest’s query can hold a lock — or simply exhaust the pool — right when execution needs a fast write. The backtest doesn’t notice. The order does.
A shared rate-limited client. Market-data APIs are rate-limited, and it’s natural to wrap that limit in one client object and import it everywhere. A research job doing a wide historical pull can spend the entire budget refreshing a universe of symbols. If execution shares that same client and the same budget, its next request for a live quote waits behind requests that have nothing to do with a live decision.
A mutated “global” setting. An exploratory script wants to answer “what if risk were set differently?” so it flips a value on a shared settings object for the duration of the run. That’s a reasonable thing to want to test. It’s not reasonable for that object to be the same instance the execution loop reads from, in the same process, where the mutation outlives the experiment that made it.
A notebook that outlives its own assumption. Someone opens an interactive session to check something, leaves it running, and forgets about it. If that session holds a stale reference to something execution depends on — a cached price, a cached position — it is now quietly disagreeing with reality for as long as it stays open.
A slow dependency treated as a hard one. A feature-engineering step that occasionally hangs on a flaky external call is a research annoyance. The same call, imported into the execution path because “it’s already there,” turns an occasional hang into an occasional missed trade window.
The fix is a boundary, not a try/except
Wrapping each shared call in a try/except patches the specific incident and leaves the shape intact. The next shared resource will fail the same way, because the actual problem was never the missing exception handler — it was that two workloads with incompatible reliability requirements were allowed to touch the same state.
The rule that holds up is about dependency direction, not file layout:
Research and execution may share small, deterministic, read-only domain utilities —
a position-sizing formula, a date-rounding helper, a symbol normalizer.
Execution must never depend on exploratory research jobs, mutable notebooks,
experimental pipelines, or ad hoc analysis state.
That’s narrower than “split it into two repos,” and it’s meant to be. Separation isn’t about duplicating every function or standing up a second codebase — most of the value comes from being explicit about four things:
- State ownership. Execution owns the objects that represent live reality (positions, account state, order status). Research reads a copy, never the original.
- Resource allocation. If a rate limit, a connection pool, or an API budget matters to execution, research gets its own — even if that means a second, deliberately smaller one.
- Configuration access. Research can propose a parameter value. It should never be able to mutate the value execution is currently using.
- Deployment surface. Execution’s process should be able to start, run, and restart without anything research-related successfully importing, or failing to import.
None of this requires exotic infrastructure. A second process, a read-only database replica or periodic snapshot, and a lint rule or import check that fails the build if execution imports anything from a research package will cover most of it.
What it costs
This isn’t free. Enforcing the boundary means some logic — the small, deterministic parts on the allowed, read-only side — gets written once but reviewed with more scrutiny than either side would get alone. Deployment has more moving parts than “one script, one process.” It is genuinely more convenient, in the short term, to let one process do both jobs.
The convenience is exactly what makes it easy to postpone the split until after the first incident, at which point the fix is the same one it always would have been — just later, and with more code depending on the thing you now have to untangle.
What this generalises to
Worth auditing your own system for:
- Does anything execution depends on get created, opened, or mutated by a process whose job is exploration rather than action?
- If your research workload ran flat-out for an hour right now, would execution notice?
- Can a research script import execution internals — not “should it,” but does the language and project structure actually prevent it?
- Is there a single object — a client, a config, a connection — that both sides hold a reference to, where only one side is allowed to change it?
- If execution crashes, does anything about how research runs that day help explain why?
A research process failing should be a boring, contained event. An execution process failing should be rare, loud, and never explained by “something in the analysis job.”