Same Code, Everywhere: From on_bar() Backtest to Live Cron in One Engine
Jonny Bravo
-Same Code, Everywhere: From on_bar() Backtest to Live Cron in One Engine
There's a quiet graveyard in every quant shop: strategies that backtested beautifully and then traded like strangers. The research was done in a Jupyter notebook with pandas and shift(-1). The live system was rebuilt by an engineer in a different language, with a different fill model, on a different data path. Somewhere in the translation, the edge died — or worse, a sign flipped and the edge inverted.
This backtest-to-live discrepancy is the most expensive kind of bug because you can't see it until real money is on the line. The two systems were never the same system, so of course they behave differently.
Our answer is structural: there is one engine. The code that runs your backtest is the code that generates your live signals. Not a port. Not a "production rewrite." The same BacktestEngine.run, the same on_bar, the same fill mechanics. This post follows a strategy across the whole lifecycle — author, preflight, backtest, go live — and shows where the discrepancy would have crept in, and why it can't.
One on_bar, written once
You implement a single method. Anything you don't explicitly touch is held — no silent flat:
class Strategy(BaseStrategy): class Params(SingleParams): fast: int = 12 slow: int = 48 def warmup_bars(self): return self.p.slow + 1 def on_bar(self): c = self.closes() # the sole instrument's closes if ema(c, self.p.fast) > ema(c, self.p.slow): self.target(weight=1.0) # go fully long else: self.close()
This is the entire strategy. There is no separate "live version." The helpers — self.closes(), self.target(weight=...), self.close() — resolve against a point-in-time snapshot whether they're called inside a backtest loop or a live cron job, because the thing calling them is identical in both.
The engine's own docstring is blunt about this. It is "the event loop shared by the CLI, the backtest DAG, and the live signal runner." Shared, singular. When you fix a subtle bug in how a position is sized, you fix it everywhere at once, because there's only one place it lives.
Preflight: fail in milliseconds, not minutes
Before you spend a single backtest, the platform runs a preflight — a synchronous, no-execution check that your strategy will actually run over the window you asked for:
"""Preflight — instant, synchronous "will this backtest run?" check. Resolves a strategy's universe + data requirements and probes each required dataset's coverage over the window with a bounded query, so the common failures (wrong symbol, out-of-range window, missing aux data) surface in milliseconds in the editor instead of minutes later as a dead Airflow run."""
Typo'd a symbol? Asked for a window where your funding feed doesn't exist yet? Preflight tells you in the editor, before anything queues. It's the difference between a tight authoring loop and the soul-crushing cycle of "submit, wait five minutes, discover the symbol was wrong, repeat."
Backtest: the fills are the live fills
When you run the backtest, the mechanics are deliberately the ones live trading imposes. From the engine's contract:
for each bar ts on the clock:
1. execute pending fills (targets decided at the PREVIOUS bar) at THIS bar's OPEN
2. accrue funding/carry on open positions for the elapsed bar
3. mark to market at THIS bar's CLOSE -> record equity/drawdown/exposure
4. call strategy.on_bar(snapshot, state) -> new targets, held as pending
Read step 1 carefully: a target you decide at bar t fills at the open of bar t+1, not the close you just observed. With slippage and taker fees applied. This is the single most common place backtests cheat — filling at the price that triggered the signal — and it's designed out. You cannot act on information at the instant you receive it, exactly as in production. The backtest's optimism is capped at reality.
Going live: same code, reconstructed state
To take a strategy live, you schedule it as a signal runner on a cron cadence — say 5 * * * *, every hour at five past. On each tick, the runner doesn't replay history; it reconstructs the current portfolio state from your live exchange account, builds the same point-in-time snapshot, and calls the same run_bar:
def run_bar(self, snapshot, state): # seed current positions as HOLD, let on_bar mutate, collect targets self.snapshot = snapshot self.state = state self.now = snapshot.ts self.equity = float(state.equity_usd or 0.0) self._orders = {iid: TargetPosition(iid, pos.side, pos.size_usd) for iid, pos in state.positions.items() if pos.side != "flat" and pos.size_usd != 0.0} self.on_bar() return list(self._orders.values())
The strategy emits desired positions — TargetPosition objects. It does not place orders. That separation is deliberate and it's what makes "same code" possible: the strategy's job is to decide what to hold, identically in backtest and live. Turning the difference between desired and current into actual orders is a separate concern, handled by the executor.
Mandates: the gap between desired and live, made explicit
A mandate is exactly that gap: a (strategy, instrument, portfolio) target with the current size, the desired size, fees, and tradeability flags. The execution engine diffs them and works the difference into the market — slicing large orders into chunks, respecting per-instrument precision, honoring slippage guards.
And critically, the live executor refuses to retry into a wall. It carries a list of exchange error codes that will never succeed:
# exchange error codes that will never succeed on retry. Retrying these just # burns the run window and pushes transient failures past their budget. PERMANENT_ERROR_MARKERS = ( "-2027", # binance: exceeded the maximum allowable position at current leverage "-2019", # binance: margin is insufficient "-1111", # binance: precision over the maximum / invalid quantity precision "insufficient margin", "insufficient balance", ... )
This is the kind of hard-won operational detail that lives only in code that has actually traded. A research-only backtester never learns it. Our executor did, and it's wired into the same lifecycle your backtest already passed through.
Dry run: the same path, minus the orders
The final safety net before capital: every mandate execution can run with dry_run=True. It walks the entire execution path — reconstructs state, computes the diff, sizes and slices the orders, applies the guards — and stops just short of sending them. You see exactly what the live system would do, against your real account, with your real balances, and no order leaves the building.
When the dry run looks right, you flip one flag. There's no second system to trust, because there was never a second system.
Why "same code" is the whole pitch
Most platforms sell you a backtester and an execution engine and quietly leave the integration — the riskiest part — as your problem. The seam between them is where strategies go to die.
We don't have a seam. Preflight, backtest, live signals, and mandate execution are four views of one engine. The strategy you validated is the strategy that trades. The fills you modeled are the fills you get. The bug you fixed is fixed everywhere.
For a fund manager, that's not a convenience feature. It's the difference between deploying with confidence and deploying with hope.
Author a strategy, preflight it, backtest it, and schedule it live — all without leaving the engine that already proved it works. Open the editor →