# 🧮 Building the Backtest: Fills, Slippage, and Fees

By Anh Le

In the last lesson your idea became explicit rules and a `signal` column: +1 for long, 0 for flat, -1 for short, decided from each bar's data. That column is intent, not a strategy. A strategy also has to answer two questions the signal ignores: **when** does the trade actually happen, and **what does it cost** to do it? Everything in this lesson protects your future results from a flattering lie.

By the end you should be able to:

- Explain why filling at the same bar's close is lookahead bias.
- Apply next-bar fills inside a simple loop.
- Turn slippage and commission into numbers you subtract at each fill.
- Point to the exact line in code where each cost lands.
- Estimate cost drag from turnover before you run anything.

## The Signal Is Not the Trade

Between "I want to be long" and "I am long" sit three frictions: timing, price, and money.

- **Timing:** which bar's price you get.
- **Price:** the gap between the quote on the chart and your actual fill (slippage).
- **Money:** the commission you pay for the privilege (fees).

Start with timing, because it is the one that silently doubles your returns. A rule that reads the close of bar *t* can only be acted on after that close exists. So **the signal is decided on bar t, the fill happens on bar t+1**. If you fill at the same close that generated the signal, you are buying at the exact price that told you to buy. That is lookahead bias, and it is the most common reason a first backtest looks like it prints money.

| Bar | What you can observe | What you can do |
| --- | --- | --- |
| t | close of t | compute the target position |
| t+1 | open of t+1 | fill the trade here |
| t+1 | close of t+1 | mark the position to market |

The practical convention: **signal from bar t, fill at bar t+1's open.** The open exists before you trade, it is easy to justify, and it forces you to wait like a real trader would.

```mermaid
flowchart TD
    A["📊<br/>Close of bar t"] --> B["🧮<br/>Compute signal"]
    B --> C["⏭️<br/>Wait for next open"]
    C --> D["💸<br/>Fill with slippage and fee"]
    D --> E["📈<br/>Mark to close of t+1"]
    E --> A
```

**Try it.** Your rule is "go long when the close is above the 20-day moving average." You compute `signal[t]` from `close[t]` and fill at `close[t]`.

**Click to reveal the answer**

That is a same-bar fill, a lookahead bias: the close is only known once the bar is finished, so trading at it means trading on information you did not yet have. The fix is one shift: `signal[t-1]` decides the trade, `open[t]` is the fill price.

## Slippage: The Price You Do Not See on the Chart

The chart shows trades that already happened. When you send an order, you take whatever the book offers, and that is usually slightly worse than the last printed price. That gap is **slippage**.

The beginner-friendly model is a fixed cost per fill, expressed in **basis points** (bps). One basis point is 0.01%, or 0.0001 in decimal form. A buy fill gets worse upward, a sell fill gets worse downward:

```python
fill = price * (1 + slip)   # buying: you pay a little more
fill = price * (1 - slip)   # selling: you receive a little less
```

Direction matters. Slippage always works against you, never for you. If you apply it in only one direction, or apply the same sign to both sides, you have built a small subsidy into the model.

How large? A widely traded ETF might cost a couple of basis points; a thinly traded small cap can cost far more, and more again on a volatile day. Pick a value you can defend for your instrument, write it at the top of your script as a constant, and move it up and down later to see how fragile the result is.

## Commissions: The Cost You Pay Twice

A commission is charged per fill, not per idea. Enter and exit, and you have paid twice. That is why we talk about **round trips**: one complete open-and-close cycle, two fills, two charges.

Fees are usually modelled as a fraction of traded notional:

```python
cost = abs(shares * fill) * fee
```

Put the pieces together and you get a compact drag formula for a round trip:

$$
\text{drag} \approx \text{turnover} \times 2 \times (\text{slip} + \text{fee})
$$

**Try it.** Your strategy does 250 round trips a year. Slippage is 5 bps per fill and commission is 5 bps per fill. Roughly how much does execution cost you per year?

**Click to reveal the answer**

Each fill costs 10 bps, a round trip costs 20 bps (0.20%), and 250 round trips cost about 50% of your capital per year. Costs are not a rounding error. A strategy that trades every day has to beat a very large headwind, which is exactly why turnover is a design choice, not an afterthought.

## Wiring It Into a Loop

Vectorized math is fast, but for a first backtest a loop is better: every timing decision and every cost is visible on its own line. Assume `df` has `open`, `close`, and a `signal` column already computed from past data.

```python
import pandas as pd

SLIP = 0.0005   # 5 bps per fill
FEE  = 0.0005   # 5 bps of traded notional per fill

def backtest(df, slip=SLIP, fee=FEE):
    cash, shares, pos = 1.0, 0.0, 0
    curve = []
    for t in range(1, len(df)):
        target   = df["signal"].iloc[t - 1]   # decided on bar t-1
        open_px  = df["open"].iloc[t]         # filled on bar t
        close_px = df["close"].iloc[t]

        if target != pos:
            if shares != 0.0:                 # close what we hold
                fill = open_px * (1 - slip) if shares > 0 else open_px * (1 + slip)
                cash += shares * fill - abs(shares * fill) * fee
                shares = 0.0
            if target != 0:                   # open the new side
                fill = open_px * (1 + slip) if target > 0 else open_px * (1 - slip)
                shares = target * cash / (fill * (1 + fee))  # size so the fee fits
                cash -= shares * fill + abs(shares * fill) * fee
            pos = target

        curve.append(cash + shares * close_px)  # mark to market
    return pd.Series(curve, index=df.index[1:])
```

Three lines carry the whole lesson. `signal.iloc[t - 1]` enforces the wait. `open.iloc[t]` is the honest fill price. Every `fee` and `slip` subtraction happens at a fill, which means the cost is charged twice on a round trip whether you remember it or not.

**Try it.** Change `df["open"].iloc[t]` to `df["close"].iloc[t]` and rerun. What happens to the equity curve?

**Click to reveal the answer**

Ending value rises, often a lot. You are now filling at the very price that produced the signal, which is a lookahead bias plus zero slippage on timing. Any strategy that suddenly improves when you "simplify" the fill rule is telling you the improvement came from the bias, not the logic.

## Traps That Quietly Inflate Returns

- **Same-bar fills.** Filling at the close that generated the signal. Always shift the signal by one bar.
- **Charging the entry only.** A round trip pays twice. If your exits are free, your results are inflated by exactly the size of one fee per trade.
- **Slippage in one direction.** It hurts buys and sells. Two signs, or it is a gift.
- **Unadjusted prices.** Splits and dividends create fake overnight moves. Clean data first, then backtest.
- **Skipping the cost sensitivity check.** Run the same backtest with double the slippage. If the edge disappears, you found a cost, not an edge.

## What to Carry Into the Results

- A signal is a plan; a fill is an event. Decide on bar t, trade at bar t+1's open.
- Slippage moves buys up and sells down, measured in basis points.
- Commission is charged per fill, so a round trip pays it twice.
- Cost drag scales with turnover: `turnover x 2 x (slip + fee)` is a one-line sanity check before you write any code.
- The loop is the audit trail. If you cannot point to the line where a cost is subtracted, it is probably not being subtracted.

Next, in **Reading Results and Avoiding Overfitting**, we take this honest equity curve and ask the harder question: is this a real edge, or did we tune the parameters until the past looked good?

---

## Contents

Part of [📊 Quantitative Trading for Beginners: Build and Backtest a Strategy](https://everwise.study/courses/quantitative-trading-for-beginners-build-and-backtest-a-strategy.md)

- **📈 Market Data and Return Statistics**
  - [🧭 What a Quant Strategy Actually Needs](https://everwise.study/courses/quantitative-trading-for-beginners-build-and-backtest-a-strategy/what-a-quant-strategy-needs.md)
  - [🗂️ Getting and Cleaning Price Data](https://everwise.study/courses/quantitative-trading-for-beginners-build-and-backtest-a-strategy/getting-and-cleaning-price-data.md) (sign-in required)
  - [📉 Returns, Volatility, and Time Series Basics](https://everwise.study/courses/quantitative-trading-for-beginners-build-and-backtest-a-strategy/returns-volatility-and-time-series-basics.md)
  - [✅ Checkpoint: Market Data and Statistics](https://everwise.study/courses/quantitative-trading-for-beginners-build-and-backtest-a-strategy/checkpoint-market-data-and-statistics.md)
- **🧪 Strategy Design and Backtesting**
  - [📝 From Idea to Testable Trading Rules](https://everwise.study/courses/quantitative-trading-for-beginners-build-and-backtest-a-strategy/from-idea-to-testable-trading-rules.md)
  - [🧮 Building the Backtest: Fills, Slippage, and Fees](https://everwise.study/courses/quantitative-trading-for-beginners-build-and-backtest-a-strategy/building-backtest-fills-slippage-and-fees.md) — this lesson
  - [🔍 Reading Results and Avoiding Overfitting](https://everwise.study/courses/quantitative-trading-for-beginners-build-and-backtest-a-strategy/reading-results-and-avoiding-overfitting.md)
  - [✅ Checkpoint: Strategy Design and Backtesting](https://everwise.study/courses/quantitative-trading-for-beginners-build-and-backtest-a-strategy/checkpoint-strategy-design-and-backtesting.md)
- **🛡️ Risk Control and Capstone Build**
  - [⚖️ Position Sizing and Core Risk Metrics](https://everwise.study/courses/quantitative-trading-for-beginners-build-and-backtest-a-strategy/position-sizing-and-core-risk-metrics.md)
  - [🏗️ Capstone: Build and Backtest Your Own Strategy](https://everwise.study/courses/quantitative-trading-for-beginners-build-and-backtest-a-strategy/capstone-build-and-backtest-your-strategy.md) (premium content)
  - [✅ Checkpoint: Risk and Capstone](https://everwise.study/courses/quantitative-trading-for-beginners-build-and-backtest-a-strategy/checkpoint-risk-and-capstone.md) (premium content)
- [Course cheatsheet](https://everwise.study/courses/quantitative-trading-for-beginners-build-and-backtest-a-strategy/cheatsheet.md) (premium content)
