Backtesting an ETF Strategy Without Fooling Yourself
Backtests are not predictions. They are debugging tools for your assumptions. The fastest way to fool yourself is to let the code see the future, hide costs, test one pleasant window, then tune until the equity curve stops embarrassing you.
Freeze the information set
Start with a simple rule: every data point used by a signal must have been available before the trade. That sounds obvious, but lookahead bias sneaks in through adjusted prices, index membership, financial statement dates, and same-bar execution.
Three common leaks:
- Using today’s close to compute today’s signal and trade at today’s close.
- Using a current ETF universe list on historical dates. ETFs launch and close. A universe that only contains today’s survivors is a survivorship-biased universe.
- Joining dividend or holdings data by calendar date instead of publication date.
If your signal is computed at close, execute at next open or next close after a lag. If you use fundamentals, join on the report date, not the period end. In pandas, make the lag explicit:
import pandas as pd
# prices: date, open, close, volume
# signal_raw: date, value (computed from same-day close)
# Enforce one full bar of delay before execution.
df = prices.merge(signal_raw, on='date', how='left')
df['signal'] = df['value'].shift(1)
df['trade_date'] = df['date']
# If you execute at next open, shift the execution price instead:
df['exec_price'] = df['open'].shift(-1)
# And drop the last row, which has no future open.
df = df.dropna(subset=['exec_price'])
# Point-in-time universe: join with listing dates and only keep
# symbols whose listing_date <= date < delisting_date.
df = df.merge(listing_dates, on='symbol')
df = df[(df['date'] >= df['listing_date']) &
((df['delisting_date'].isna()) | (df['date'] < df['delisting_date']))]
Also beware adjusted close from vendors. It is usually retroactively adjusted for splits and dividends, which is useful for total return, but if you use it for signal levels and execution prices, you may introduce future information. Keep two series: raw prices for execution and total-return series for performance attribution.
Count dividends and frictions honestly
An ETF strategy that ignores dividends is not conservative; it is wrong in a predictable direction. Many equity ETFs distribute income, and those distributions are part of the return. But dividends are not free money. You must model withholding tax, currency conversion, fund fees, and the fact that the ex-dividend price drop is real.
Trading costs matter even for liquid ETFs. The bid-ask spread, commission, and slippage may be small per trade, but they compound with turnover. A daily strategy with 100% turnover can bleed out through frictions that a monthly strategy barely notices.
Make assumptions explicit in a config file, not buried in code:
backtest:
start: '2015-01-01'
end: '2025-12-31'
execution: 'next_open'
universe:
source: 'point_in_time_listings'
min_history_days: 252
return_series:
use_adjusted_close: true
include_dividends: true
dividend_tax_rate: 0.21 # example placeholder; set from your tax situation
costs:
commission_bps: 5
spread_bps: 2
slippage_bps: 3
min_trade_notional: 1000
currency:
base: 'USD'
fx_cost_bps: 10
The table below is not a prescription. It is a checklist of cost components that should either appear in your config or be consciously set to zero with a reason.
| Component | Why it matters | Typical place to set it |
|---|---|---|
| Commission | Direct cash cost per trade | Broker schedule, per-share or bps |
| Bid-ask spread | You cross the spread on entry/exit | Half-spread assumption by liquidity bucket |
| Slippage | Price moves between signal and fill | bps by ADV or volatility |
| Market impact | Large orders move the price | Position size relative to ADV |
| Dividend withholding | Reduces net distributions | Tax rate by investor domicile |
| FX conversion | Cross-border ETF trades need currency | bps per conversion |
| Fund fees | Ongoing drag | Expense ratio, accrued daily |
If you cannot estimate a cost, run the backtest with zero and with a deliberately pessimistic number. The difference tells you how fragile the edge is. Never report only the optimistic run.
Test on the whole timeline, not the window that flatters you
The easiest self-deception is choosing a start date after a drawdown or before a bull run. A strategy tested on 2010-2021 looks different from one tested on 2000-2025. You do not need to predict the future, but you do need to stop selecting the past.
Use a validation scheme that matches how you will trade. For most ETF strategies, that means walk-forward analysis: train or select parameters on an in-sample window, trade them on the next out-of-sample window, then roll forward. The out-of-sample segments are the only ones you should believe.
| Method | What it protects against | Main weakness |
|---|---|---|
| Single train/test split | Obvious overfitting | One test window can be lucky |
| Walk-forward | Regime change and parameter drift | Can be slow; overlapping data needs care |
| Purged K-fold | Leakage from overlapping labels | Complex for simple ETF rules |
| Monte Carlo trade shuffle | Fragile path dependence | Not a substitute for out-of-sample |
| Parameter sensitivity sweep | Fragile peak parameters | Still in-sample unless nested |
A useful sanity check is to run the strategy on the full period with no parameter optimization. If the default parameters fail everywhere except one window, you do not have a strategy. You have a story about that window.
Stop tuning until the curve looks right
Overfitting is not a single mistake. It is a process: you try a parameter, look at the curve, adjust, repeat. By the time the curve looks good, you have trained on the test set without realizing it.
Combat this with pre-registration. Before you run the sweep, write down the parameter ranges, the selection metric, and the maximum number of trials you will allow. Then run a grid and inspect the shape, not just the best cell. A robust parameter should sit on a plateau, where nearby values perform similarly. A single spike surrounded by poor results is a warning.
# Example: run walk-forward with fixed trial budget.
# Do not keep adding trials after seeing the out-of-sample result.
for window in 252 504 756; do
for lookback in 20 60 120; do
python backtest.py \
--start 2005-01-01 \
--end 2025-12-31 \
--train-window "$window" \
--signal-lookback "$lookback" \
--execution next_open \
--costs config/costs.yaml \
--out "runs/wf_${window}_${lookback}.json"
done
done
# Then compare the distribution of out-of-sample returns,
# not the single best run.
python analyze_runs.py runs/wf_*.json --metric cagr --metric max_drawdown
Other checks:
- Does the strategy survive if you lag the signal by one extra bar?
- Does it survive if you double costs?
- Does it survive if you remove the best five days or the best five trades?
- Does it survive if you start in a different decade?
If a strategy only works under one assumption, that assumption is the strategy. You should test the assumption directly.
Automation can help you run these checks repeatedly. You can wire data refresh and walk-forward runs into the same kind of pipeline described in the AI automation guide, and use workflow tools like Zapier vs n8n vs AI Workflow Builder to schedule them. But automation does not validate a backtest. It only makes it easier to repeat the same mistakes faster.
What to do next
- Write a one-page backtest protocol before you run another optimization. Include your universe source, signal lag, execution rule, cost assumptions, validation scheme, and maximum trial count. Date it.
- Re-run your current backtest with a forced one-bar lag and a point-in-time universe. If the results collapse, you had a lookahead or survivorship problem.
- Reconcile dividends and costs separately. Produce two reports: one with gross total return and one with net return after your cost config. Keep both.
- Reserve a final holdout period and do not touch it until you have frozen the strategy. If you look at it more than once, it is no longer a holdout.
- If your current setup is a spreadsheet plus scattered scripts, the ETF Dashboard is a Taiwan-ETF analytics workbench that puts price and dividend data, technical indicators and backtesting in one dashboard for $199 USD one-time. Use it only after you have a protocol; the tool will not choose your assumptions for you.
Get ETF Dashboard
ETF Dashboard โ $199, one-time payment, instant download. See the full breakdown on the review page.
Related reading
- The Ultimate Guide to AI Automation 2026
- How I Built a 10-Product Digital Business
- Building an AI Content Factory
Published by slashman413 โ writing practical, evergreen guides on money, productivity, developer tooling and the web. More about this site โ
Frequently Asked Questions
What is lookahead bias in ETF backtesting?
It is using information that was not available at the time of the trade, such as same-day closing prices, revised index membership, or restated dividends. The fix is to lag signals and join data on the date it was actually published.
Should I use adjusted close or raw prices in a backtest?
Use raw prices for execution assumptions and an adjusted total-return series for performance measurement. Mixing them can create future information in your signals or hide the cash impact of dividends.
How many parameters can I tune before a backtest is overfit?
There is no safe number; the risk grows with every choice you make after seeing the data. Pre-register a small range, prefer parameter plateaus over spikes, and keep a final holdout you never optimize on.
๐ Recommended Tools
๐ Related Articles
- Automated Investment Dashboards โ Track Your Portfolio in Real TimeHow to build and run an automated investment dashboard: data sources, the metrics that matter, drift alerts, โฆ
- ETF Investment Strategy 2026 โ How to Build a Data-Driven PortfolioA practical, data-driven ETF investment framework for 2026: core-satellite allocation, ETF selection criteria, โฆ
- The Complete Guide to ETF Automated Investment โ Build Your Own DashboardA comprehensive guide to building an automated ETF investment dashboard. Covers portfolio tracking, โฆ
- Automated ETF Investing: AI Dashboard Implementation GuideIn this guide, we dive deep into the advantages of automated ETF investing and teach you how to optimize your โฆ
๐ฌ Free Weekly AI Product Guides
New tools, templates and automation walkthroughs โ plus hands-on updates on the Slashman Tools catalogue. One email a week, zero fluff.
Free forever ยท No spam ยท Unsubscribe anytime ยท Sent instantly