The Learning Library
Contents

Module 22 — Transaction Costs, Slippage & Execution

Part V · Production · Priority ⭐ Recommended · Status: Draft v0.1 Prerequisites: Module 1, Module 4, Module 5


Overview

Every backtest makes an implicit promise: the live account will trade something like this. Whether that promise is kept is decided less by your signals than by your cost model — cost-modeling honesty determines whether backtest-to-live degradation is measured (a number you predicted and sized for) or suffered (a slow bleed you discover in the statement). This module builds the measurement machinery and borrows the institutional execution vocabulary — TWAP, VWAP, participation caps, implementation shortfall — scaled honestly to retail MT5 size.

Who is on the other side of your fills? Liquidity providers who widen quotes into your impatience, pattern detectors who profit from predictable order flow, and a broker whose markup breathes with the session clock (Module 1’s four tolls). Institutional desks answer these counterparties with execution algorithms; you will learn the same ideas at the scale where a “large order” is 50 gold lots.

The payoff artifact is a transaction-cost analysis (TCA) loop: every parent order gets scored against the price at the instant of decision, decomposed into named causes, reviewed weekly, and fed back into the cost assumptions your research stack trades on. After this module, “what did execution cost me?” is a lookup, not a shrug.


How It Works — The Execution Cost Machine

Figure: the life of one parent order, from decision to weekly report. Every arrow is a telemetry capture point; every capture point feeds the decomposition that ends in recalibrated research assumptions.

flowchart TD
    decide[Decision made<br/>signal fires at mid]
    send[Order sent<br/>intent logged]
    quote[(Quote snapshot<br/>mid, spread, latency)]
    fill[Fill received<br/>deal price and lots]
    decomp{Shortfall<br/>decomposed}
    spread[Spread share<br/>crossing the toll]
    timing[Timing share<br/>drift while working]
    impact[Impact share<br/>own footprint]
    tca[(Weekly TCA report)]
    recal[Cost-model<br/>recalibration]
    research[(Research cost inputs<br/>Modules 4-8)]

    decide --> send
    send -->|"capture quote at request"| quote
    quote -->|"clock starts"| fill
    fill --> decomp
    decomp -->|"paid to cross"| spread
    decomp -->|"market moved away"| timing
    decomp -->|"I moved the market"| impact
    spread --> tca
    timing --> tca
    impact --> tca
    tca -->|"weekly review"| recal
    recal -.->|"refresh curves"| research
    research -.-> decide

    classDef data fill:#e8f0fe,stroke:#4a86e8
    classDef process fill:#f3f3f3,stroke:#888
    classDef decision fill:#fff4d6,stroke:#d6a300
    classDef ok fill:#e6f4ea,stroke:#2e7d32

    class quote,tca,research data
    class decide,recal,spread,timing,impact process
    class send,fill ok
    class decomp decision

How to read this:

  • The amber diamond is the module in miniature: a raw shortfall number is useless until it is split into causes, because each cause has a different remedy.
  • The dashed return arrows are the point of the whole exercise — measured costs flow back until they drive the assumptions your backtests trade on (Modules 48).
  • Without the quote snapshot at request time, decomposition is impossible after the fact; log it before you need it.

The cost taxonomy, extended

Module 1 introduced four tolls. Execution work extends the list to six, and — more importantly — assigns each one a measurement source. A cost you cannot measure, you cannot negotiate with.

Toll What you pay Where you measure it
Spread crossing Half the bid-ask width, implicitly, on entry and exit Tick-archive bid/ask by hour (Module 4 curves)
Commission Per-lot, per-side fee on raw accounts Account schedule, reconciled against deal history
Swap / financing Overnight carry, triple-charged Wednesdays symbol_info swap fields + accrued amounts in your journal
Market impact Your own flow pushing price away from the decision level Arrival-price shortfall decomposition (this module)
Borrow-like frictions Short-side swap asymmetry, unhittable symbols, margin hikes Broker spec page + rejected-order log
Taxes / levies Turnover or profit taxes by jurisdiction Broker tax statements; your accountant, not your EA

Two clarifications keep the table honest. First, “slippage” is not a seventh toll — it is the timing and impact shares of your shortfall showing up in the decomposition. Second, borrow-like frictions are the CFD-world echo of equity stock lending: when a broker makes shorting expensive or impossible via asymmetric swaps, that scarcity is priced exactly like a borrow fee, and it belongs in the same ledger.

Market impact and the square-root law

The one piece of execution folklore worth memorizing: price impact grows roughly with the square root of your order size relative to daily volume. Double your size and impact grows by about 41 percent, not 100 percent. This is the empirical regularity behind institutional capacity math, and it applies at retail scale too — just with smaller numbers.

# Square-root impact law: impact_bps grows with SQRT(size / ADV).
# Doubling size adds only ~41% more impact, not 100% - sublinear pain.
DAILY_VOL_BPS = 100             # XAUUSD routinely moves ~1% per day
ORDER_LOTS    = 50
ADV_LOTS      = 26_000          # Module 4 median daily volume, lots

impact_bps    = DAILY_VOL_BPS * (ORDER_LOTS / ADV_LOTS) ** 0.5
impact_usd_oz = 2400 * impact_bps / 10_000
# -> ~4.4 bps ~ $1.05/oz upper-scale estimate for one 50-lot shot

Meaning: the square-root law is a scale check, not a quote. The worked example later in this module realizes $0.36/oz on the same order — smaller than the estimate because the order was worked patiently over 50 minutes rather than dumped. Calibration, not faith, is what turns this formula into a planning tool: fit the constant to your own TCA ledger, then use it to answer capacity questions before they become P&L questions.


Measuring Your Costs

Arrival price is the metric

Of all the ways to grade an execution, one has won the institutional argument: arrival-price accounting. The benchmark is the mid-quote at the instant the order was requested, and the score is:

shortfall = (executed_price - decision_mid) x signed_quantity, in money

A positive shortfall means the world charged you for transacting; a negative one means the world paid you. Everything else in this module — the algorithms, the schedules, the dashboards — exists to shrink this number without taking unmeasured risk elsewhere.

📌 Convention: In this module, “shortfall” is measured against the decision-time mid-quote, so crossing the spread lands inside the number and gets decomposed out. Some desks benchmark against the arrival ask for buys, which hides the spread share — pick one convention, write it down, and never switch mid-study.

💡 Idea: Arrival price is to execution what the audit trial balance is to bookkeeping: a single control total that every component must reconcile into. If spread share + timing share + impact share does not add up to total shortfall, your telemetry has a hole — find it before trusting any conclusion.

Demo-versus-live fill studies

Demo fills are necessary but flattering: many brokers route demo orders through idealized books with tighter, faster fills. The honest protocol pairs orders:

  1. Run the identical EA on demo and live accounts, same hours, same symbols, same logic.
  2. Log the quote snapshot at request time for both (mid, spread, server timestamp).
  3. Score fill − quote_at_request per order, per account.
  4. The live-minus-demo gap is your realism premium — the correction your backtests owe reality.

Expect the gap to concentrate exactly where you can least afford it: fast markets and thin sessions. A demo study that shows 0.05/oz slippage while live shows 0.20/oz has told you something valuable — that your cost model must be calibrated on live fills or deliberately padded.

Time-of-day slippage distributions

Slippage is a distribution with a clock, not a constant. Bucket your scored fills by session hour and summarize each bucket with median and 90th percentile — the same shape as Module 4’s spread curves, because they share a cause: liquidity arriving and leaving. These hourly shortfall curves become sampling distributions for backtest fill models (Module 5’s low-timeframe requirements) and gates for scheduling decisions later in this module.

The snippet below is the per-trade scorer that everything else consumes. It takes a fill ledger — one row per child fill — and produces the decomposition:

# Arrival-price shortfall: cost of execution versus the decision-instant mid.
# Benchmark convention: mid-quote captured when the order was REQUESTED.
import polars as pl
fills = pl.DataFrame({
    "fill_price":   [2400.20, 2400.35, 2400.53],
    "fill_oz":      [500.0, 500.0, 500.0],        # three 5-lot slices
    "decision_mid": [2400.00, 2400.10, 2400.25],  # mid when order was sent
    "half_spread":  [0.20, 0.21, 0.25],           # half-spread at request
})
qty = pl.col("fill_oz")
scored = fills.with_columns(
    ((pl.col("fill_price") - pl.col("decision_mid")) * qty)
        .alias("shortfall_usd"),                        # paid over mid
    (pl.col("half_spread") * qty).alias("spread_usd"),  # crossing toll
).with_columns(
    (pl.col("shortfall_usd") - pl.col("spread_usd")).alias("timing_usd")
)
print(scored)  # weekly TCA rolls these columns up across all orders
Column sums across the parent order:
  shortfall_usd = 365   spread_usd = 330   timing_usd = 35

In production the frame comes from your journal export rather than literals, and an optional third share (own impact) is estimated against a no-you counterfactual tick path — but the accounting identity stays exactly this simple.


Worked Example — Decomposing a 50-Lot Parent Order

Hand-checkable, no library required. Setup:

  • Parent order: buy 50 lots XAUUSD (contract 100 oz/lot → 5,000 oz total).
  • Schedule: sliced 10 × 5 lots, worked over 50 minutes.
  • Decision mid (at request): 2400.00. Average fill: 2400.36.
  • Average quoted half-spread during the window: 0.20 USD/oz.

Total implementation shortfall, in the trader’s unit of account:

IS = (avg_fill - decision_mid) x filled_oz
   = (2400.36 - 2400.00) x 5,000 oz
   = 0.36 USD/oz x 5,000 oz  =  1,800 USD

Decompose by cause:

Component Arithmetic USD
Total shortfall (2400.36 − 2400.00) × 5,000 oz 1,800
Spread-crossing share 0.20 × 5,000 oz 1,000
Drift / timing share (0.36 − 0.20) × 5,000 oz 800

Context anchors, because naked numbers mislead:

  • Against the order’s 12,000,000 USD notional, 1,800 USD is 15 bps — an order of magnitude above the 1–5 bps institutional desks report on deep books, because you crossed a marked-up retail feed ten times. The gap is the honest price of the rails, and it belongs in your cost model.
  • Raw-account commission adds $3.50 × 2 sides × 50 lots = 350 USD, lifting all-in execution to 2,150 USD ≈ 0.43 USD/oz. A strategy with a 2.50 USD/oz gross expectancy has already spent 17 percent of it walking through the door.
  • The 800 USD drift share is the only negotiable part: better scheduling attacks it, while the 1,000 USD spread share is a fixed toll of crossing — unless you switch to resting-limit entries and accept the fill-certainty tradeoff discussed below.

Execution Benchmark Algorithms

Institutional execution is a mature discipline with four canonical scheduling algorithms. Their plumbing does not transfer to MT5 — there is no order book to hide in — but their vocabulary and tradeoffs transfer completely, because the tradeoffs are between the same two quantities: certainty of price and certainty of completion.

Algorithm How it trades Typical slippage Primary risks
TWAP Fixed-interval equal slices; minimizes deviation from the time-average price 2–10 bps Predictability — pattern detectors see the metronome and front-run it; best on stable, illiquid venues
VWAP Slices follow historical intraday volume profile (U-shape) 1–5 bps Volume-shape mismatch on unusual days; forced end-of-day concentration
POV Constant percentage of real-time volume (e.g., 10%) 2–8 bps Signaling when the participation rate is too high; non-completion when volume dries up
Implementation Shortfall (Almgren–Chriss) Optimized, front-loaded decay curve trading impact against timing risk; benchmark is the decision/arrival price 3–15 bps Parameter misestimation; front-loading reverse-engineered by observers (modern fixes inject randomness and adaptivity)

Three readings of the table worth internalizing:

TWAP is honesty through boredom. Equal slices on a fixed clock minimize tracking of the interval’s average price, which is why it survives on stable or illiquid venues where nothing else does. Its flaw is legibility: a metronome is the easiest pattern in the world to detect, and anyone who detects it can trade ahead of every remaining slice. Randomized jitter (below) is the retail-sized antidote.

VWAP outsources the clock to the crowd. By spending your size where the market habitually spends its size — the U-shaped intraday volume profile, heavy at open and close — you hide inside aggregate flow. It wins when the day’s volume resembles its history and bleeds when it does not, and the end-of-day catch-up clause punishes any day where early volume disappoints.

Implementation Shortfall is the optimizer the others approximate. Almgren and Chriss formalized the tension every scheduler feels: trade slowly and market impact is cheap but timing risk (drift while you work) is expensive; trade fast and the reverse holds. The result is an optimal trajectory that is deliberately front-loaded — most size early, decaying after — because finishing early kills timing risk. The catch cuts both ways: the schedule is only as good as its volatility and impact parameters, and a front-loaded curve is itself a detectable signature. Modern practice randomizes the trajectory and adapts it to observed conditions.

🧪 Evidence: The slippage bands above (TWAP 2–10 bps, VWAP 1–5 bps, POV 2–8 bps, IS 3–15 bps) are the practitioner-consensus ranges reported in execution-algorithm surveys and primers — see QuantEngines’ guide and Ryan O’Connell’s implementation-shortfall walkthrough in References. Treat them as sanity scales for institutional depth, not as promises for a retail CFD feed, where spread crossing alone can exceed the entire band.


Retail-Scale Approximations on MT5

You will not run VWAP tracking against consolidated tape on a CFD account. You can run every core idea in miniature, and for M5-scale systems the miniatures capture most of the value.

Mini-TWAP slicing with EA timers

The workhorse approximation: an EA timer fires N child orders of equal size across a window, with randomized jitter so the schedule resists forecasting. This is snippet-grade engineering:

# Mini-TWAP builder: equal slices on a fixed clock, then randomized jitter
# so pattern-detectors cannot forecast (and front-run) the next child order.
import numpy as np

rng         = np.random.default_rng(42)
PARENT_LOTS = 50
N_SLICES    = 10
SLOT_SEC    = 50 * 60 // N_SLICES   # 50-minute window -> 300-second slots
JITTER_SEC  = 90                    # +/- unpredictability band

schedule = [
    {"slice": i + 1, "lots": PARENT_LOTS // N_SLICES,
     "fire_sec": int(i * SLOT_SEC
                     + rng.integers(-JITTER_SEC, JITTER_SEC + 1))}
    for i in range(N_SLICES)
]
print(schedule[:3])   # fire times sit near 0/300/600 s but never predictable

Operational notes: clamp negative fire times to immediate execution; recompute the schedule if a slice is rejected rather than retrying blindly; and never slice through scheduled news or the rollover hour — splitting is for liquidity, not for surviving spread blowouts. True iceberg concealment (exchange-hidden reserve size) is rarely supported on retail CFD accounts; a jittered slice stream is your substitute.

Stop-versus-limit entry: the certainty tradeoff

Every entry order answers one question wrongly for somebody: do you want certainty of fill or certainty of price? You cannot have both.

Order type You are certain of You surrender
Market / stop entry The fill happening now The price — slippage tail belongs to you
Limit entry The worst price you will accept The fill — adverse selection picks off your queue exactly when you are wrong

The execution-literate rule: use stops where being in the trade matters more than a few tenths (breakouts, where missing the move is the loss), limits where price matters more than presence (mean-reversion fades, where patience is the edge). And measure which one is quietly costing you more — the TCA ledger answers this per setup, not per opinion.

Participation caps by hour

POV thinking shrinks to a lookup against Module 4’s hourly curves: never let your scheduled flow exceed a fraction of the hour’s typical volume, and refuse hours whose tail spreads are toxic.

# Hourly participation cap: child flow stays below a fraction of typical
# hourly volume (Module 4 curves); a tail-spread gate blocks toxic hours.
import polars as pl
curves = pl.DataFrame({                # built once from the tick archive
    "hour_utc":           [7, 12, 21],
    "median_volume_lots": [9_500, 26_000, 1_200],
    "p90_spread_pts":     [28, 15, 65],  # tail spread in points
})
POV_CAP, DESIRED_LOTS_HR, SPREAD_GATE_PT = 0.10, 600.0, 40
plan = curves.with_columns(
    pl.min_horizontal(
        pl.col("median_volume_lots") * POV_CAP,   # 10% of the hour's flow
        pl.lit(DESIRED_LOTS_HR),                  # what the strategy wants
    )
    .mul((pl.col("p90_spread_pts") <= SPREAD_GATE_PT).cast(pl.Float64))
    .alias("max_lots_this_hour")
)
print(plan)
# -> hour 21 collapses to 0.0: rollover tail spread fails the gate

Meaning: scheduling becomes a constrained optimization over a table you already maintain. The cap protects you from being the market in thin hours; the gate keeps slices out of the rollover and other spread blowout windows that Module 1 flagged as hazards.

When your alpha is an execution algorithm

Some strategies have no directional view at all — the edge is the spread itself, harvested by quoting both sides and managing inventory (market making). On MT5’s CFD rails that business is structurally unavailable: no central book to rest quotes in, no queue position to defend. The adjacent skills — inventory management, quote skewing, adverse-selection defense — live on exchange-connected venues and are mapped in Appendix B of the curriculum master (Avellaneda–Stoikov territory). Know the boundary: if your backtest P&L appears mainly because fills were assumed friendly, you have built an execution strategy wearing a signal strategy’s clothes — and the TCA loop below is how you catch it. Reinforcement-learning approaches to scheduling (Module 21) inherit exactly this caveat: the environment must charge honest costs or the learned policy is fiction.


Pre- and Post-Trade Analytics

Measurement only pays when it is wired into the desk routine. Two artifacts do the work: a fill-quality dashboard for looking, and a weekly TCA review for acting.

The fill-quality dashboard

Five fields per child order earn their columns; everything else is decoration until these exist.

Field Meaning Why it earns its column
Requested price Mid-quote captured at send Anchors the arrival-price benchmark
Quoted spread (points) Bid-ask width at request Splits the crossing toll from drift
Filled price Actual deal price The outcome being judged
Latency (ms) Send-to-fill elapsed time Separates slow-pipe costs from market drift
Session bucket Hour / session tag Groups fills into comparable regimes before judging

The weekly TCA ritual

Same time every week, thirty minutes, no exceptions:

  1. Export intents and deals from the journal; join child fills to parent orders.
  2. Score arrival-price shortfall per parent order; decompose into spread / timing / impact shares.
  3. Aggregate by symbol × session bucket; flag buckets whose median or p90 shortfall jumped versus trailing weeks.
  4. Compare realized curves against the calibrated cost model; investigate outliers before explaining them away.
  5. Recalibrate the session-hour cost matrices; push them into the research stack’s cost inputs.

Step 5 is the loop-closer: refreshed curves flow back into Module 4’s data layer assumptions, Module 5’s fill models, Module 7’s cost budgets and Module 8’s spread ceilings — and, for this repository’s owner, directly into Phase-5 replays and Module 19 CPCV evaluations.

🔧 For your pipeline: your live supervisor already logs every intent and deal in the SQLite journal plus debug traces — that is a complete arrival-price TCA ledger waiting for a reader. Proposed job: a nightly script joins intents to deals, captures mid/spread at intent time, and writes per-parent-order shortfall rows using the decomposition above. Then retire the flat cost constants (meta_label_cost 0.00004 on XAUUSD, 0.00030 on BTCUSD, in return units) in favor of session-hour cost curves calibrated from that ledger, and feed the recalibrated cost matrix back into Phase-5 replays and the Module 19 CPCV path evaluations — so validated Sharpe ratios are computed against measured, current execution reality rather than launch-day guesses.


Testing It Honestly

Execution analytics can lie in every direction a backtest can, plus a few of its own. Two tests belong in CI, not in your head.

The synthetic-fill fixture

Plant a decomposition with known ground truth, then require the scorer to recover it exactly. This catches silent regressions — a renamed column, a sign flip, a unit confusion — before they corrupt a quarter of TCA history:

# Synthetic-fill fixture: plant a KNOWN decomposition, demand exact recovery.
import numpy as np

OZ_PER_SLICE  = 500.0
HALF_SPREAD   = 0.20                      # planted toll per oz
DRIFT_STEP    = 0.02                      # planted drift per slice
mids          = 2400.00 + DRIFT_STEP * np.arange(10)
fill_prices   = mids + HALF_SPREAD        # every slice pays the toll

expected_spread = HALF_SPREAD * OZ_PER_SLICE * 10          # -> 1,000
expected_timing = DRIFT_STEP * np.arange(10).sum() \
                  * OZ_PER_SLICE                           # -> 450
actual_total    = ((fill_prices - 2400.00) * OZ_PER_SLICE).sum()

assert abs(expected_spread + expected_timing - actual_total) < 1e-9
print("fixture decomposition reconciles:", actual_total, "USD")
# -> fixture decomposition reconciles: 1450.0 USD

Wire the fixture through the same scoring function production uses — a test that reimplements the logic tests only itself.

Journal-versus-statement reconciliation

Once per month, sum the journal’s recorded commissions, swaps, and deal counts and reconcile against the broker statement’s totals. This is ordinary double-entry discipline: two independent records of the same events must agree. Unexplained residuals above a materiality threshold (suggest 0.5 percent of period costs) mean something real — missed fills during an adoption gap, currency-conversion rounding, or a deal the journal never saw — and every one of those also corrupts your TCA denominators.

⚠️ Pitfall: Demo-calibrated cost models are systematically optimistic. Pair demo and live studies before trusting any shortfall distribution — the gap between them is itself a number worth tracking quarterly, because brokers retune routing.


Pitfalls Checklist

Symptom Why it lies Defense
Measuring shortfall only on winners Winners’ fills flatter execution quality; losers carry the fat tails Score every parent order, winners and losers alike
Comparing fills across sessions London-overlap and rollover fills are different populations Always bucket by session-hour before judging
Ignoring swap legs in round-trip cost Held positions look “clean” while financing bleeds invisibly Accrue swap into the same trade ledger
Yesterday’s spread curve applied to news days News blows spreads out 5–20×; the curve is a fair-weather object Event-window overrides; stand down across releases (Module 14)
Over-slicing tiny orders into fee minimums 100 micro-slices multiply per-ticket minimum commissions Floor the slice size against the fee schedule

Hands-On Project

Deliverable: src/execution/ (a TCA harness plus an Almgren–Chriss-style schedule simulator) and docs/research/tca-findings.md reporting what the loop found.

Tasks:

  1. Build the TCA harness: join supervisor-journal intents to deals, capture the quote snapshot at request, score arrival-price shortfall per parent order, and emit the spread / timing decomposition.
  2. Calibrate session-hour cost curves per symbol from at least four weeks of fills (demo-plus-live, labeled); store p50 and p90 per bucket alongside Module 4’s spread/volume curves.
  3. Build the schedule simulator: execute a notional large order (start with 50 lots XAUUSD) against historical ticks from the Module 4 archive, charging impact via a square-root model calibrated to your own curves.
  4. Compare modeled shortfall for the Almgren–Chriss-style front-loaded schedule against a plain TWAP baseline and a single-shot market order; sweep the urgency parameter and sketch the impact-versus-timing-risk frontier.
  5. Feed the recalibrated session-hour cost matrix into one Phase-5 replay and one Module 19 CPCV evaluation; report the net-Sharpe delta against the flat-cost status quo.

Acceptance criteria:

  • Every journal parent order scores with a non-negative spread share and total shortfall reconciling to deal prices within rounding.
  • The simulator reproduces the worked example exactly: 1,800 USD total, split 1,000 spread / 800 drift on the synthetic fixture.
  • Session-hour cost curves cover all 24 hours × both symbols, with p50 and p90 stored per bucket.
  • Modeled-versus-realized shortfall error is reported out of sample, and the calibrated model beats a naive flat-cost assumption.
  • At least one Phase-5 or CPCV conclusion changes under recalibrated costs — and the direction is documented either way.

Key Takeaways

  • Backtest-to-live degradation is either measured or suffered; arrival-price shortfall is the number that measures it, and every fill you log without a decision-time quote snapshot is a number you forfeited.
  • Shortfall decomposes into a spread share (a toll you chose by crossing), a timing share (drift you can engineer down), and an impact share (your own footprint) — the decomposition, not the total, tells you which lever to pull.
  • The square-root law makes impact sublinear: doubling order size adds roughly 41 percent more impact, so capacity planning comes before ambition scaling.
  • Institutional canon compresses cleanly to retail: slice with jitter (anti-predictability), cap participation against hourly volume, respect the spread clock — the tradeoffs survive, only the plumbing differs.
  • Stop-versus-limit is a certainty tradeoff, not a preference: stops buy fill certainty and rent out price certainty; limits do the reverse.
  • Demo fills set the optimistic floor, the live journal sets the truth, and the weekly TCA ritual is the loop that keeps the two reconciled.
  • Cost inputs are curves, not constants: session-hour matrices recalibrated from your own ledger flow back into Modules 4–8 assumptions, Phase-5 replays, and CPCV validations.

References