Research, Data and Risk Platforms · Technology
11Backtest-Engine Architecture
A quoting strategy looked rich in the bar backtest, flat in the order-book replay, and lost money in the simulator. The firm had four backtesters, written by four teams in four styles, each with its own idea of a strategy, a clock, an order and a result; the strategy had been ported to each by hand. It took a week to be sure that the three answers differed because the markets differed — bars against queues, queues against a market that reacts — and not because the four ports of the strategy did. This chapter builds the engine that makes that question a query: one strategy object, one event model, one data-access layer and one results store over the four fidelity levels of One Quant Book 7 (chapter 16), the fourth being a reactive simulation on the exchange simulator of One Quant Book 10.
11.1 One engine, four levels
Definition 11.1 (Backtest engine)
A backtest engine is the platform component that runs a strategy against historical or simulated markets at a chosen fidelity level through one strategy interface, reads its data through a data-access layer, records what the strategy saw and did in one event model, and stores every run with the identity of its code, data, parameters and seed.
The firm’s backtesters already exist: Book 7’s vectorised backtester (firm.vecbt, level 1), its event-driven backtester on bars (firm.evbt, level 2) and its order-book replay with queue positions (firm.lobreplay, level 3), and Book 10’s exchange simulator (firm.exchsim). Book 7 defined level 4 as live trading at small size or paper trading, and in its chapter 19 let an agent trade inside the synthetic market as the stand-in for live; here the exchange simulator plays that part, with the same session replayed as real orders against which the strategy’s own orders rest and trade. The engine does not rewrite any of them. Each level has an adapter that turns the one strategy object into what that backtester expects, and turns what the backtester reports into the engine’s events (Figure 11.1).
The strategy interface has two forms, because the levels differ in what a strategy is. At level 1 there are no orders: a strategy is a rule that maps closes to a target position, run over all dates at once. At levels 2 to 4 a strategy reacts to the market and to its own fills by sending and cancelling orders. A TargetStrategy states its rule once, target(close, i), and the engine runs it vectorised at level 1 and bar by bar at level 2; a QuoteStrategy has two callbacks, on_market and on_fill, and runs at levels 2 to 4. Asking for a quoting strategy at level 1 is an error, not a silently empty backtest.
class Quoter(B.QuoteStrategy):
"""A lot at the best bid and ask; requote when the touch moves; stop at the limit."""
name = "quoter"
def __init__(self, lots: int = 1, limit: int = 3):
self.qty, self.limit, self.mine = 100 * lots, 100 * lots * limit, {}
def on_market(self, ctx, t, snap):
b, a = snap["bid"], snap["ask"]
if b is None or a is None:
return
live = set(ctx.working())
self.mine = {v: sp for v, sp in self.mine.items() if v in live}
have = set()
for v, (side, px) in list(self.mine.items()):
if px != (b if side == 1 else a): # the touch moved: cancel
ctx.cancel(v)
del self.mine[v]
else:
have.add(side)
for side, px in ((1, b), (-1, a)):
if side not in have and side * ctx.position < self.limit:
self.mine[ctx.send(side, px, self.qty)] = (side, px)
class Engine:
def __init__(self, level: int, strategy, data: DataAccess, config: dict):
if level not in LEVELS:
raise ValueError(f"level {level} not in {LEVELS}")
if level == 1 and not isinstance(strategy, TargetStrategy):
raise TypeError("level 1 runs a TargetStrategy: no orders, no queues")
self.level, self.strategy, self.data = level, strategy, data
self.cfg = dict(config)
def manifest(self, pnl: float, n_fills: int) -> dict:
name = self.cfg["dataset"]
return {"level": self.level, "strategy": type(self.strategy).__name__,
"strategy_code": code_hash(self.strategy), "engine_code": ENGINE_CODE,
"data": {name: self.data.hash(name)},
"params": {k: v for k, v in sorted(self.cfg.items()) if k != "dataset"},
"seed": self.cfg.get("seed", 0), "pnl": round(float(pnl), 6),
"fills": n_fills}
def run(self) -> Run:
level = {1: self._level1, 2: self._level2, 3: self._level3, 4: self._level4}
pnl, pos, fills, log, mids = level[self.level]()
fills = np.array(fills, dtype=float).reshape(-1, 4)
mids = np.array(mids).reshape(-1, 2)
return Run(self.level, pnl, pos, fills, log.ordered(), mids,
self.manifest(pnl, len(fills)))
Run with its manifest. code/firm/btengine/firm_btengine.py11.2 The event model and the clock
Definition 11.2 (Event model)
An event model is the set of event classes a backtest engine records and the total order in which it processes and stores them: here each event carries a time, a class and a sequence number, and events are ordered by time, then by a fixed rank of their class, then by sequence number.
Four classes suffice for the chapter’s strategies, in this rank order: market (the top of the book the strategy saw), fill (what the market did to its orders), order and cancel (what the strategy did in response). The rank settles the question every event-driven engine must answer and most answer by accident: when a quote update and a fill carry the same time, which does the strategy see first? A fixed answer makes two runs of the same inputs identical and two levels comparable; the sequence number breaks the remaining ties in arrival order.
LEVELS = (1, 2, 3, 4)
CLASSES = ("market", "fill", "order", "cancel")
RANK = {c: i for i, c in enumerate(CLASSES)}
# ------------------------------------------------------------ event model
@dataclass(order=True, frozen=True)
class Event:
t: float # seconds from the session start (level 1-2: bar end)
rank: int # RANK[cls]: simultaneous events in a fixed class order
seq: int # arrival order within the run: ties broken the same way
cls: str = field(compare=False)
data: tuple = field(compare=False, default=())
class EventLog:
def __init__(self):
self.events: list[Event] = []
def add(self, t: float, cls: str, *data) -> None:
self.events.append(Event(float(t), RANK[cls], len(self.events), cls, tuple(data)))
def ordered(self) -> list[Event]:
return sorted(self.events)
The clock is the simulated clock of Book 7 (chapter 17): time moves from event to event, never from the machine. Each backtester keeps its own clock internally — bar ends at level 2, message times at level 3, nanoseconds of the simulated day at level 4 — and the adapters convert all of them to seconds from the start of the session, so that events of different levels can be laid side by side.
11.3 Data access
Definition 11.3 (Data-access layer)
A data-access layer is the component through which every backtest reads its data: it resolves a dataset name to stored data and returns, with the data, an identity of their content (a hash) that the run records.
The chapter’s layer stores named datasets as Parquet (chapters 3 and 4) and keeps a content hash in each file’s metadata: a firm.tape session of Book 7 (the order-by-order messages of a synthetic hour), and daily bars. The hash is what makes “the same data” checkable: two runs that read datasets with equal hashes read the same bytes, whatever the files were called, and a run’s manifest names the hash, not the path. In the firm the same interface fronts the tick store of chapter 4 and the reference data of chapter 6; a strategy never opens a file.
11.4 The results store
Definition 11.4 (Results store)
A results store keeps every backtest run with its manifest — level, strategy and engine code hashes, data hashes, parameters and seed — and its outputs (fills, events, P&L), under an identifier derived from the manifest, and answers queries over runs.
A run’s identifier is the hash of its manifest (Listing 11.4): the same strategy code, engine code, data, parameters and seed give the same identifier, so a rerun is recognised rather than stored twice, and a changed line of the strategy gives a new one. The store answers the questions the week of the chapter’s hook was spent on: which runs of this strategy exist, at which levels, on which data, with which code; and, for two runs, where their events first differ.
def save(self, run: Run) -> str:
d = self.root / run.run_id
d.mkdir(exist_ok=True)
(d / "manifest.json").write_text(json.dumps(run.manifest, sort_keys=True, indent=1))
f, ev = run.fills, run.events
fills = {"t": f[:, 0], "side": f[:, 1], "qty": f[:, 2], "price": f[:, 3]}
pq.write_table(pa.table(fills), d / "fills.parquet")
events = {"t": [e.t for e in ev], "cls": [e.cls for e in ev],
"data": [json.dumps(e.data) for e in ev]}
pq.write_table(pa.table(events), d / "events.parquet")
return run.run_id
As of September 2026 — Open-source engines with one strategy for several environments
Open-source trading platforms describe the same design goal. NautilusTrader’s documentation calls it “an open-source, production-grade, Rust-native engine for multi-asset, multi-venue trading systems” in which “the same strategy and execution-algorithm code can run across backtest and live systems”, with a common core used by its backtest, sandbox and live contexts and a message bus between components.
11.5 Adding a level: the reactive simulation
Definition 11.5 (Reactive simulation)
A reactive simulation is a backtest in which the strategy’s orders are real orders in a simulated market: they rest in its queues, trade against the other participants’ orders and change what those orders execute against afterwards, so that the market the strategy meets depends on the strategy’s presence. The firm uses it as fidelity level 4, the exchange simulator standing in for the venue.
At level 3 the strategy’s orders are shadows (Book 7, chapter 18): they take a place in the replayed queue and fill when the recorded executions reach them, but the replay does not change — a market order that filled a shadow also fills, in the recording, the orders it filled that day. At level 4 the same firm.tape session is replayed through Book 10’s simulator as real orders: each recorded addition becomes a limit order, each cancellation a cancel, each trade a market order of its size. The strategy’s orders are among them. A market order that meets the strategy’s order trades with it and not with the order behind it, which then rests longer and trades later with something else; when the orders at the touch leave, the strategy’s order is left alone at the front, visible to everyone, where the replay’s shadow would have vanished with the level it was hiding in.
def _level4(self):
meta = self.data.meta(self.cfg["dataset"])
secs, lat = float(meta["seconds"]), int(round(self.cfg.get("latency", 0.0) * 1e9))
sim = X.Simulator(_venue(secs), seed=self.cfg.get("seed", 1))
tape = TapeConfig(seconds=secs, seed=int(meta["seed"]), news_at=None)
sim.add_background(X.TapeBackground(tape)) # the same session, as real orders
log, fills, side, rctx = EventLog(), [], {}, [None]
class Inner(firm_lobreplay.Strategy): # what ReplayStrategyAdapter drives
def on_market(self, ctx, t, snap):
rctx[0] = ctx
wrap.market(t, snap)
def on_fill(self, ctx, vid, t, qty, price):
rctx[0] = ctx
fills.append((t, side[vid], qty, price))
wrap.fill(vid, t, qty, price, side[vid])
def send(s, price, qty):
vid = rctx[0].send(s, price, qty)
side[vid] = s
return vid
back = _Back(send, lambda v: rctx[0].cancel(v), lambda: rctx[0].working(),
lambda: rctx[0].position)
wrap = _Wrap(self.strategy, _Ctx(back, log))
lm = X.LatencyModel(entry_ns=lat, ack_ns=lat, data_ns=lat)
sim.add_agent(X.ReplayStrategyAdapter(Inner()), X.SessionSpec("BT", latency=lm))
sim.run()
pos = sum(s * q for _, s, q, _ in fills)
cash = -sum(s * q * p for _, s, q, p in fills)
mid = wrap.mids[-1][1] if wrap.mids else 0.0
return cash + pos * mid, pos, fills, log, wrap.mids
ReplayStrategyAdapter, fills and events recorded as at every other level. code/firm/btengine/firm_btengine.pyLevel 4 is the counterfactual impact of Book 7 (chapter 18) made concrete, with its limits: the background flow is a recording, so its limit orders do not requote around the strategy and its informed traders do not change their minds. It measures the mechanical part of the strategy’s presence — queues and matching — and not the strategic part, which needs agents (Book 10’s firm.agentmkt).
11.6 The P&L ladder
The chapter’s experiment runs the quoting strategy of Listing 11.1 on twenty one-hour sessions of firm.tape (seeds 1 to 20) at levels 2, 3 and 4, with 20 s of data and order-entry latency and no fees at any level. At level 2 the strategy sees the touch at the end of each one-second bar that has trades and a quote fills in full when the bar trades at its price (the touch fill of Book 7, chapter 17); at level 3 it takes its place at the back of the queue; at level 4 it is in the queue.
fig_btengine.py.Figure 11.2 is the ladder. At level 2 the strategy makes $228 an hour (, one standard error over the sessions) from 841 fills: a quote that fills whenever the bar trades at its price never waits in a queue. At level 3 it loses $12 an hour (, indistinguishable from zero) from 333 fills: behind the queue, it fills less and mostly when the price is about to move through it. At level 4 it loses $102 an hour () from 434 fills. The last step is not noise: level 4 is below level 3 in every one of the twenty sessions (Figure 11.3), by $90 an hour on average ().
fig_btengine.py.Because the strategy object is one and the same at every level, the difference between two runs is the market’s; the engine’s event logs say where it begins. first_divergence compares the fills of the level-3 and level-4 runs of each session: they part within the first five fills in all twenty sessions, at a median of 11 seconds into the hour. From then on the two runs are different histories of the same session.
11.7 Where the gap comes from
Which fills make the difference? Each level-4 fill either has a level-3 counterpart — a fill of the same side within a millisecond — or it exists only because the strategy’s order was in the book. Symmetrically, some replay fills have no reactive counterpart. The ten-second markout of each fill (the mid ten seconds later against the fill price, signed) measures what the fill earned before the inventory it created was marked; Figure 11.4 splits the markouts of both runs into the three groups, in the manner of the divergence decomposition of Book 7 (chapter 19).
fig_btengine.py.Example 11.6 (Four backtesters, one strategy)
Per session, 193 fills are shared by the two runs; they earn $17.5 of ten-second markout at level 3 and $18.3 at level 4, the same within noise — the check that the classification pairs like with like. The replay has 140 fills of its own, which earn $0.03; the reactive run has 241 of its own, which lose $69.2. The markout gap between the runs is $68.4 an hour, and the reactive-only fills account for 101% of it: the loss is made of trades that happen only because the strategy’s orders were there: an order that absorbs an aggressive order the recording gave to someone else, or that is still at the touch, visible, after the orders around it have left. The remaining $22 of the $90 gap in P&L is the inventory those fills leave, marked after more than ten seconds.
Remark 11.7 (What the engine settled)
With four backtesters and four ports of the strategy, the gap between two levels had three candidate causes: the market, the port and the backtester’s conventions (what the strategy sees, when a fill is reported). The engine removes the second by construction and makes the third explicit in the adapters. What remains is what the levels are for.
11.8 A rule at levels 1 and 2
The same engine runs a daily rule — long when the close is above its level fifty days earlier, short when below — on a thousand days of synthetic bars at level 1 (firm.vecbt, the rule’s weight earned from the next close) and at level 2 (firm.evbt, market orders filled at the next open). Level 1 returns over the period and level 2 ; the five points are the overnight moves on the days the position changes, earned by the old position at level 2 and by the new one at level 1, which trades at a close it has only just seen. The rule is written once; the two runs sit next to each other in the results store under different identifiers.
11.9 Tutorial: one strategy through four backtesters
Goal. Run one strategy object at every level through one engine, store every run, and find where two levels part. End state: Figures 11.2 and 11.4 and a results store holding sixty runs.
- Data:
DataAccess.tape(3600, seed)stores a session with its hash;daily()stores the daily bars. - One session:
pl_btengine.one_sessionruns levels 2, 3 and 4 and saves each run. - The ladder:
ladder()over seeds 1 to 20,summary(rows). - The gap:
classify_fills,markoutsandfirst_divergenceon the level-3 and level-4 runs. - Identity:
rerun_is_identical(): same inputs, same run identifier, same events.
What to change next. Add a one-tick spread condition to the strategy (quote only when the spread is one tick) and see which level’s verdict changes; run level 4 with Book 10’s agent population instead of the recorded session.
11.10 Build: the backtest engine
Purpose. One engine over the firm’s four fidelity levels, so that a strategy is written once, runs anywhere on the ladder, and every run can be found, compared and reproduced. Chapter 12 hosts the same strategy objects in production-shaped environments.
Interface. Engine(level, strategy, data, config).run() -> Run; TargetStrategy, QuoteStrategy; Event, EventLog; DataAccess(root) with put, get, hash, tape, daily; ResultsStore(root) with save, load, query; first_divergence, classify_fills, markouts.
Rules. Book 7’s backtesters and Book 10’s simulator are wrapped, never edited; events in the total order (time, class rank, sequence); a run’s identifier is its manifest’s hash; a strategy never opens a file; no level silently accepts a strategy it cannot run.
Acceptance tests. code/firm/btengine/tests/: the event order on simultaneous events; content hashes; level 1 equal to firm.vecbt called directly, level 3 to firm.lobreplay, level 4 to firm.exchsim with the adapter; equal run identifiers for equal inputs and different ones for a changed parameter; queries; the fill classification and markouts on a worked case.
Stretch. Parallel runs over seeds (chapter 13); an agent-population background at level 4; a level-3 run from the tick store’s recorded feed of chapter 4.
Sources and further reading
- One Quant Book 7, chapters 16–19 (fidelity levels, event-driven backtests, order-book replay, simulation against live); One Quant Book 10, chapter 26 (the exchange simulator).
- NautilusTrader documentation, “Overview” and “Architecture”.
11.11 Exercises
Exercise 11.1 ★
Order these simultaneous events by the engine’s event model: an order sent at , a quote update at , a fill at , a cancel at .
Solution
Solution of Exercise 11.1.
The cancel at first; then, at , the quote update (market), the fill, and the order.
Exercise 11.2 ★
Why does the engine refuse a quoting strategy at level 1 instead of running it with no fills?
Solution
Solution of Exercise 11.2.
A vectorised backtest has no orders, queues or fills, so a quoting strategy run there would trade nothing and return zero: a result that looks like an answer. Refusing makes the mismatch between the strategy and the level visible.
Exercise 11.3 ★
Two runs have the same strategy code, data hashes and seed but different run identifiers. What can differ?
Solution
Solution of Exercise 11.3.
The level, a parameter (latency, queue model, bar width, inventory limit), or the engine’s code hash: the identifier hashes the whole manifest.
Exercise 11.4 ★★
From the ladder, is the level-3 loss of $12 an hour () evidence that the strategy loses in replay? Is the level-3 to level-4 gap evidence of anything?
Solution
Solution of Exercise 11.4.
No: is within one standard error of zero. The gap is: level 4 is below level 3 in all twenty sessions, by $90 on average with a standard error of 7.4, twelve standard errors from zero.
Exercise 11.5 ★★
Explain why the shared fills should earn the same markout at levels 3 and 4, and what it would mean if they did not.
Solution
Solution of Exercise 11.5.
A shared fill is the same trade in both runs: the same side, price and moment, against the same aggressive order of the session; what the price does in the next ten seconds is mostly the session’s, so the markouts should agree. If they did not, the classification would be pairing different trades, or the two runs’ markets would have drifted apart before the fills — either way the decomposition could not be read.
Exercise 11.6 ★★
What does level 4 as built here not capture about the strategy’s effect on the market, and which component would add it?
Solution
Solution of Exercise 11.6.
The strategic response of the other participants: the recorded limit orders do not requote around the strategy’s orders, market makers do not widen or narrow, and informed traders do not change their timing. An agent population (Book 10’s firm.agentmkt) running in the simulator would add it.
Exercise 11.7 ★★★
Coding. Run one_session for seed 3 with the inventory limit raised from three lots to ten. How do the three levels’ P&L change, and does the reactive-only share of the gap move?
Solution
Solution of Exercise 11.7.
For seed 3 with the limit of three lots: $417, and at levels 2, 3 and 4 (1 462, 583 and 690 fills); with ten lots: $350, and (1 580, 670 and 818 fills). More inventory costs a little at levels 2 and 3 and a great deal at level 4. The reactive-only fills’ share of the markout gap rises from 129% to 147% (above 100% because in this session the replay-only fills lose too): with a larger limit the strategy keeps quoting after it has been run over, and at level 4 those quotes are real.
Exercise 11.8 ★★★
Find the flaw. “We compared the strategy in our replay backtester and in the new simulator and the simulator was worse, so the simulator’s matching has a bug. We ported the strategy carefully to both.”
Solution
Solution of Exercise 11.8.
With two hand ports, the difference between the runs has three candidate causes — the market model, the ports, and the backtesters’ conventions — and the claim picks one without evidence. Run one strategy object through one engine at both levels so the ports cannot differ, then compare the event logs: the first differing fill says whether the simulator matched differently or whether the strategy was simply in the book (a reactive-only fill). A simulator that is worse because the strategy’s orders are real is not a bug.
11.12 Problem: Four Backtesters, One Strategy
Problem 11.1
Weekend problem — the ladder and its gap
The quoting strategy of Listing 11.1, twenty one-hour sessions, levels 2 to 4.
Part I — The engine.
- What must a backtest engine own that each backtester does not?
- Why two strategy forms, and which levels run each?
- What does the class rank of the event model settle?
- What does a run’s manifest contain, and why is the identifier its hash?
- Why does the data-access layer return a hash with the data?
Part II — The levels.
- What does the strategy see, and when does a quote fill, at level 2?
- How does a level-3 shadow order differ from a level-4 order?
- What does level 4 replay from the session, and as what?
- What does level 4 not capture?
- How is Book 7’s level 4 represented here?
Part III — The ladder.
- What are the P&L per hour and the fills at each level?
- Which differences are statistically clear?
- In how many sessions is level 4 below level 3, and by how much on average?
- When do the level-3 and level-4 runs first differ?
- Why are the level-2 fills so many?
Part IV — The gap.
- State the named result: the P&L an hour at each level and the share of the replay-to-reactive gap explained by fills that exist only because the strategy was in the book.
- Which fills lose, and why?
- What part of the gap is not in the ten-second markouts?
- What would you conclude about the strategy?
- In one sentence: what does the engine make possible that four backtesters did not?
Solution
Solution of Problem 11.1.
- One strategy interface, one event model and clock, one data-access layer and one results store across the levels.
- A target rule for level 1 (no orders), run bar by bar at level 2; a quoting strategy with market and fill callbacks for levels 2 to 4.
- The order of simultaneous events: market, then fills, then the strategy’s orders and cancels, so that equal inputs give equal runs and levels are comparable.
- Level, strategy and engine code hashes, data hashes, parameters, seed and summary results; hashing it makes equal inputs one identifier and any change a new one.
- So that the run records which data it read, whatever the files are called, and “same data” can be checked.
- The touch at the end of each one-second bar with trades; a quote fills in full when the bar trades at its price.
- A shadow takes a place in the recorded queue and fills when recorded executions reach it, without changing the recording; a level-4 order rests in the simulator’s queue and trades against the replayed orders, changing what they meet afterwards.
- Every recorded addition as a limit order, cancellation as a cancel, trade as a market order of its size.
- The strategic reaction of other participants.
- By the simulator standing in for the venue, as Book 7 used an agent inside the synthetic market as the stand-in for live trading.
- Level 2: $228 () from 841 fills; level 3: () from 333; level 4: () from 434.
- Level 2 against level 3, and level 3 against level 4 (paired over sessions); level 3 is not distinguishable from zero.
- All twenty; by $90 an hour ().
- Within their first five fills in every session, at a median of 11 seconds into the hour.
- A touch fill needs only a trade at the price, never a place in a queue.
- Named result. P&L per hour $228 at level 2, at level 3 and at level 4; the reactive-only fills explain 101% of the $68.4 ten-second-markout gap between replay and reactive simulation, the shared fills none, and the other $22 of the $90 gap is inventory marked later.
- The 241 reactive-only fills per session, which lose $69.2 over ten seconds: trades that happen only because the strategy’s orders are real, in the queue or left at the touch.
- About $22 an hour: the P&L of the inventory beyond ten seconds.
- That it has no edge: it breaks even at best behind the queue and loses where its orders count; the bar backtest’s profit is the touch fill’s.
- It makes the difference between two levels a property of the markets, because the strategy object, event model and data are the same.
11.13 Interview questions
Interview question 11.1 ★ researcher
Your strategy is profitable in the bar backtest and loses in the order-book simulation. Where do you look?
Solution
Solution of Interview question 11.1.
At the fills: how many, where and when, at each level. Bar backtests with touch fills ignore queue position and adverse selection; the order-book simulation adds both. Check that it is the same strategy code, then decompose the difference by fills and markouts.
What the interviewer is looking for: Fill models, queues and adverse selection; a decomposition, not a guess.
Interview question 11.2 ★★ developer
Two events in your event-driven backtester have the same timestamp. How should the engine order them?
Solution
Solution of Interview question 11.2.
By a fixed, documented rule — for example market data before fills before the strategy’s own actions — and then by arrival order, so that runs are deterministic and levels comparable. Never by the order of a hash map.
What the interviewer is looking for: Determinism by a total order.
Interview question 11.3 ★★ developer, researcher
How would you make every backtest run reproducible and findable a year later?
Solution
Solution of Interview question 11.3.
A manifest per run: code hashes of the strategy and engine, data snapshot hashes, parameters, seed and environment; an identifier derived from it; outputs stored under it with a query API; data read only through a layer that returns hashes.
What the interviewer is looking for: Content identity of code and data, not file names or dates.
Interview question 11.4 ★★ researcher
What does a replay of the order book miss for a passive strategy, and how would you measure it?
Solution
Solution of Interview question 11.4.
Its own impact: its orders take fills that went to others, stay visible at the touch, and change what later orders meet. Run it in a reactive simulator on the same session and decompose the difference into shared fills, replay-only fills and reactive-only fills by markout.
What the interviewer is looking for: Shadow orders against real orders; a counterfactual measured on the same data.
Interview question 11.5 ★★★ developer
Design a backtest engine that covers vectorised backtests, event-driven backtests and a market simulator with one strategy API.
Solution
Solution of Interview question 11.5.
Two strategy forms (a target rule and an event-driven strategy) over one context; adapters per backtester, the backtesters unchanged; one event model with a total order; a data-access layer with content hashes; a results store keyed by manifest hashes; tests that each adapter reproduces the backtester called directly.
What the interviewer is looking for: Adapters over existing engines, one context, reproducible runs.