Lesson 5 · 20 min
Order types & execution
Market, limit, and stop orders — and how the PortfolioEngine simulates fills.
An order is an instruction to your broker. Beginners drown in jargon — market, limit, stop, MOC — but in systematic trading you usually express *intent* through `set_target_weights` and let the infrastructure translate it into orders.
Definition: Execute now at the best available price.
Certainty of fill (if liquidity exists).
Price uncertainty — you pay the spread + slippage.
Quant Buffet: PortfolioEngine rebalances at the daily close with slippage applied — similar to a market-on-close intent.
The four orders that matter
| Order | Guarantees | Gives up | When quants use it |
|---|---|---|---|
| Market | Execution | Price | Small orders in liquid ETFs |
| Limit | Price | Execution | Anything thin, or large relative to volume |
| Stop (market) | Exit is attempted | Price entirely | Rarely — see the flash-crash lesson |
| Market-on-close | The official closing price | Intraday choice | Daily strategies whose signal *is* the close |
Market-on-close deserves special attention because it matches what the lab simulates. The closing auction is typically the single most liquid moment of the US trading day — often around a tenth of total volume — so a daily-close strategy that submits MOC orders is trading when depth is best. That is a rare case where the realistic implementation is also the convenient one.
How Quant Buffet executes in code
def on_day(engine, dt):
# Target 60% SPY, 40% TLT — engine sells and buys to reach it
engine.set_target_weights(dt, {"SPY": 0.6, "TLT": 0.4})- Rebalance uses the daily close as the reference price.
- Slippage of 2 bps makes buys slightly more expensive and sells slightly cheaper.
- Commission of 5 bps is charged on the notional of every fill.
- Sells execute before buys, so cash is available for the purchases.
- Weights are clamped to zero or below — negatives become 0 (long-only).
- Weights summing above 1.0 are scaled down proportionally, so you cannot accidentally use leverage.
- Unchanged targets are skipped entirely, which is why a daily-called strategy does not pay daily costs.
A fully worked rebalance
Start with $100,000 in cash. SPY closes at $500, TLT at $90. Your target is 60/40.
| Step | SPY | TLT |
|---|---|---|
| Target notional | $60,000 | $40,000 |
| Execution price (+2 bps slippage) | $500.10 | $90.018 |
| Shares bought | ≈ 119.98 | ≈ 444.35 |
| Commission at 5 bps | ≈ $30 | ≈ $20 |
Total friction on this rebalance is roughly $62 on $100,000, or about 6.2 bps — a one-off cost of about 0.06%. That is negligible. Now suppose your signal flips completely every single day.
Real example: how turnover kills a good signal
| Strategy style | Round trips per year | Approx. annual cost drag |
|---|---|---|
| 200-day SMA trend on SPY | 2–6 | ≈ 0.03–0.09% |
| Monthly momentum rotation | ≈ 12 | ≈ 0.2% |
| Weekly rotation | ≈ 50 | ≈ 0.7% |
| Daily mean reversion, full flip | ≈ 250 | ≈ 3.5% or more |
The arithmetic is brutally simple: each full round trip costs roughly 14 bps in the lab's model (5 bps commission plus 2 bps slippage, on both the sell and the buy). Two hundred and fifty of those is about 3.5% per year, before taxes. Many published mean-reversion edges are worth 2–4% per year gross — which means execution cost alone can consume the entire signal. Whenever a daily strategy looks weak, check turnover before you blame the idea.
# Cheap way to cut turnover: only trade when the change is material
state = {"weights": {}}
def on_day(engine, dt):
target = compute_weights(dt) # your signal
current = state["weights"] # what you asked for last time
keys = set(target) | set(current)
drift = sum(abs(target.get(s, 0.0) - current.get(s, 0.0)) for s in keys)
if drift < 0.05: # less than 5% total drift
return # skip the rebalance entirely
state["weights"] = dict(target)
engine.set_target_weights(dt, target)Long-only weight math
| Weights | Interpretation |
|---|---|
{"SPY": 1.0} | 100% in SPY, 0% cash |
{"SPY": 0.5, "TLT": 0.5} | Fully invested, equal split |
{"SPY": 0.6, "TLT": 0.3} | 90% invested, 10% cash drag |
{"SPY": 0.8, "TLT": 0.8} | Scaled to 50/50 — no leverage allowed |
{"SPY": -0.5} | Clamped to 0 — shorting is not supported |
{} or all zeros | Liquidate to cash |
Before Lesson 6 — you should be able to
- Explain what each of the four order types trades away.
- Compute the cost of a rebalance from weights, price, slippage, and commission.
- Estimate annual cost drag from round trips per year.
- Reduce turnover with a drift threshold instead of rebalancing blindly.