Backtesting a Strategy That Can't Fill at Once
Almost every backtest contains the same quiet fiction: a decision is made on one bar and the entire position appears at one price. That is a fair approximation for a small position in a deep market and nonsense for a position large enough that acquiring it takes time. Between them sits a size at which your backtest stops describing your strategy — and finding that size is research work, not an operational detail to discover later with real money.
Educational material, not trading advice. Algorithmic crypto trading is high-risk and most retail algo traders lose money.
The assumption you inherited
A single-fill backtest asserts that your whole order transacts at one observed price with no consequence. Both halves fail as size grows: the order stops fitting at one price, and the act of buying it changes the price you pay for the rest.
The useful reframing is that “position size” is not one number in your config file but a claim about the market you traded in. A rule wanting a modest fraction of a liquid pair simulates as a single print without much distortion. The same rule scaled up, or applied to a thin pair, must be worked — spread over minutes or hours — and a simulation that ignores that is measuring a strategy nobody can run.
What happens to each child order after it leaves your process is not this post’s subject. This is about the part you write: the schedule, and how a backtest represents it.
A parent order is a schedule, not a trade
Once a decision is too large to transact at once, your code stops emitting orders and starts emitting a plan. The decision becomes a parent — a target quantity and a window to reach it in — which your execution layer turns into a sequence of child orders. Three shapes cover most of what people build:
- Clock-paced. Divide the quantity evenly across equal slices of the window. Trivial to implement and trivially predictable, which is its main weakness — a fixed cadence is a pattern.
- Activity-paced. Size each slice as a fraction of how much trading is happening, participating more when there is more to participate in. This adapts to conditions but makes completion time uncertain: a quiet window means an unfinished parent.
- Opportunistic. Hold a target rate, but accelerate when conditions look favourable against your decision price and slow when they don’t. The most complex, and the easiest to accidentally turn into a second, undeclared strategy on top of the first.
None of these is a signal. They do not decide whether to hold the position — that was already decided. They decide how the decision is realized, which makes them a source of cost and variance rather than of edge.
What a schedule does to a backtest
Replacing a single fill with a schedule changes three things in your result, and only the first is obvious.
The fill price becomes an average. Your realized entry is no longer a price on your chart; it is a weighted mean across the window, with weights given by your schedule. Any edge the signal has must survive being earned at that average rather than at the price that triggered it.
You acquire timing risk in exchange for reducing impact. Working an order slowly makes each slice smaller and less disruptive, and exposes you to the price moving away from your decision for longer. Working it quickly does the opposite. That trade-off has no free direction, and where you sit on it is a parameter — one more axis a search can overfit.
And your turnover gets more expensive per unit, non-linearly. Size-scaling cost is the mechanism covered in modelling transaction costs in a backtest; the point here is that once you are slicing, that size term is no longer a small correction you can leave at zero.
Modelling it without a tick simulator
You do not need a full order-level simulator to stop lying to yourself. You need the fill price to be a window average and the cost to depend on size.
For illustration only: a decision on bar t is worked over the next k bars under a clock-paced schedule. Instead of filling at bar t‘s price, fill the whole quantity at the mean of the next k bars’ prices, and charge a cost term growing with quantity relative to the activity in that window.
entry = price.shift(-1).rolling(k).mean().shift(-(k - 1)) # schedule-weighted fill
size_cost = impact_coef * (qty / activity.rolling(k).sum()) ** 0.5
Two cautions. The shifting is exactly the alignment that produces silent lookahead if a sign is wrong, so it belongs in the audit described in auditing your code for lookahead bias. And the exponent is a modelling choice: a concave relationship between size and cost is the conventional shape, but the coefficient is yours to calibrate, and until you have, the model is a stated assumption rather than a fact.
If behaviour changes qualitatively under slicing — the rule wants to reverse before a parent completes, or issues a new decision while the old one is still being worked — a vectorized approximation has run out. That is where the event-driven form earns its cost, as in event-driven vs. vectorized backtests: an engine that can hold an in-flight parent, part acquired and the rest outstanding, represents something a column of positions cannot.
Capacity is a research output
Run the whole backtest at several sizes and report the metric as a function of size. That curve — not a single number at one convenient size — is the honest result, and it usually has a plateau, a knee, and a decline. The knee is the strategy’s capacity: the point past which adding capital adds nothing, because your own trading is eating the edge.
Three consequences:
- A performance figure without a size is uninterpretable, exactly as a Sharpe ratio without a turnover figure is.
- Sweeps run at one small size favour high-turnover configurations, because those are the ones capacity punishes first. Sweep at your intended size, and validate there too — a walk-forward stitched together at a size you will never trade is validating a different strategy. See how walk-forward validation works.
- Capacity is shared, not per-strategy. Two rules wanting the same exposure at the same moment compete for the same liquidity, so each one’s measured capacity overstates what the pair can hold.
The takeaway
Order slicing looks like plumbing and behaves like a modelling assumption. Once a strategy is large enough to need a schedule, the schedule is inside the hypothesis: the claim is no longer “this relationship exceeds costs” but “this relationship exceeds the cost of acquiring the position gradually, at this size, with this much timing risk.” A single-fill backtest cannot express that — which is why the first thing to do with a promising result is not to scale it, but to find the size at which it stops being promising. Calibrate with the smallest real size you can, because your own fills are the only data that isn’t a guess — see what paper trading catches, and what it misses.