Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Trading Strategy Backtester

A local, browser-based application for backtesting trading strategies on US market data. It ships with two complete strategies — an ICT-style intraday strategy and a Trend-following (time-series momentum) strategy — and a set of analysis tools: a shared-capital fund model, a parameter-optimization grid with out-of-sample validation, an S&P 500 benchmark, multiple free data providers with automatic fallback, and a persistent archive of past simulations.

The interface is built with Streamlit: you set the parameters in the left sidebar and the results render on the right. No coding is required to use it.

⚠️ Educational tool. Not financial advice. Results depend entirely on the quality and depth of the free data from yfinance, on transaction-cost assumptions, and on the (often small) sample of trades. Backtested performance does not predict future results.


Table of contents

  1. What this app does
  2. Quick start
  3. The interface at a glance
  4. Shared inputs (top of the sidebar)
  5. Strategy 1 — ICT intraday
  6. Strategy 2 — Trend following
  7. The fund model and position sizing
  8. Parameter optimization (with out-of-sample validation)
  9. S&P 500 benchmark
  10. Saved simulations and persistence
  11. Data providers, fallback, and download limits
  12. Metrics glossary
  13. Project structure
  14. Testing
  15. Honest notes on edge, costs, and overfitting
  16. Limitations

What this app does

Given a set of tickers (ETFs or stocks), a time period, and a strategy with its parameters, the app:

  1. Downloads historical price bars (intraday or daily) from a free data provider, normalizes them to the US Regular Trading Hours session in Eastern Time, and handles provider limits/failures gracefully.
  2. Runs the chosen strategy bar by bar with no look-ahead, producing a list of trades (ICT) or a daily position series (Trend).
  3. Translates results into money through a single shared fund: every trade of every instrument draws on one common capital, in chronological order, and the capital compounds over time.
  4. Reports performance: equity curves, per-trade tables (downloadable as CSV), risk/return metrics, exit-reason breakdowns, and a comparison against a buy-and-hold of the S&P 500.
  5. Lets you optimize parameters across many combinations and verify the best ones on data they were not tuned on (out-of-sample), and save any simulation to a local archive that survives app restarts.

Everything that matters numerically lives in engine.py, which is a pure, network-free, fully tested Python module. The Streamlit file app.py only collects inputs and displays results.


Quick start

Requires Python 3.10+.

# 1. (recommended) create and activate a virtual environment
python3 -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate

# 2. install dependencies
pip install -r requirements.txt

# 3. run the app
streamlit run app.py

Your browser opens at http://localhost:8501. The defaults are ready to go (SPY + QQQ, 60-minute bars, last 12 months, overnight off, fixed 2:1 target). Press Run test in the sidebar.

To run the automated tests:

pytest

The interface at a glance

  • Left sidebar = all controls. At the top are the inputs shared by both strategies. Below them are two tabs, one per strategy, each containing that strategy's own parameters and its run button.
  • Right area = results only. It always shows a sticky "Last simulation" summary plus a "Saved simulations" archive, and — after you run something — the full charts and tables for the strategy you launched.

Because Streamlit re-runs the whole script on every interaction, the detailed charts are (re)drawn when you press a run button. The Last simulation panel and the Saved simulations archive, however, persist across reruns, so a result summary never disappears just because you adjusted a parameter. Sidebar input values are also retained across reruns.

Every input has a ? help icon next to its label. Clicking it opens an info box structured as What it does / Refers to / Examples.


Shared inputs (top of the sidebar)

These apply to whichever strategy you run.

Instruments

A custom picker where the symbol is the dominant element (bold), with the official product name and issuer shown beside it in a smaller, muted font, and a short tooltip describing exactly what each instrument is.

  • The catalog is grouped by category: US indices / stocks, Gold, Silver, Commodities, Other highly liquid ETFs, and Futures.
  • It distinguishes, for example, GLD/IAU (physical gold) from GDX/GDXJ (gold mining stocks — more volatile than the metal), and flags that USO/UNG track via futures (subject to rollover effects).
  • Add your own ticker: type a symbol and press Enter; it appears in the category you pick from the dropdown next to the field, in the same format as the presets. Futures symbols (ending in =F, e.g. GC=F) are automatically filed under Futures. You can add several at once separated by commas or spaces.
  • Remove any ticker — preset or custom — with the small ✕ next to it. A "Restore removed presets" button appears if you hid any.

Data providers (order = priority)

Choose one or more free data sources; the order of selection sets the priority. The app tries the first source and, if it errors or returns no data, falls through to the next. See Data providers.

Period

Last year, Last 2 years, or Custom (with explicit start/end date pickers). Longer periods give a larger, more reliable sample, but intraday history is limited by the provider (see download limits).

Fund capital

  • Fund budget — the starting capital, shared across all operations of all tickers. Entered with thousands separators (e.g. 10,000), reformatted automatically after you press Enter.

Strategy 1 — ICT intraday

A mechanical intraday strategy in the ICT style: after price sweeps liquidity beyond the previous day's extreme and then reverses on a Fair Value Gap, the strategy enters in the opposite direction toward the opposing liquidity.

Definitions

  • PDH / PDL — Previous Day High / Low, the previous RTH session's high/low.
  • Sweep — a bar trades above PDH (buyside sweep → short bias) or below PDL (sellside sweep → long bias).
  • Fair Value Gap (FVG) — a 3-bar imbalance (bars i-1, i, i+1):
    • bullish if low[i+1] > high[i-1] (zone [high[i-1], low[i+1]])
    • bearish if high[i+1] < low[i-1] (zone [high[i+1], low[i-1]])
  • Inversion (entry trigger):
    • SHORT: after a buyside sweep, when a bar closes below the low of a bullish FVG zone.
    • LONG: after a sellside sweep, when a bar closes above the high of a bearish FVG zone.
  • Swing — a local high/low over a ±SWING_WIN-bar window; the list of swing highs/lows is the liquidity pool used for the "liquidity" target.

Operating rules

  1. Setups are evaluated only on bars after the open; the last entry is allowed up to LAST_ENTRY (default 14:30 ET); at most MAX_TRADES_DAY trades/day (default 1).
  2. Entry at the close of the bar that confirms the inversion.
  3. Stop beyond the swept extreme ± BUFFER (default 0.05%), scaled by the stop-width multiplier (see below). R = |stop − entry|.
  4. Target — selectable:
    • rr_fisso (fixed RR): target at RR × R (default RR = 2.0).
    • liquidita (liquidity): target at the nearest liquidity pool in the trade's direction (nearest swing low below entry for shorts, nearest swing high above entry for longs; falls back to PDL/PDH).
    • trailing: a percentage trailing stop (no fixed target).
  5. Exit / overnight — a key parameter:
    • overnight OFF: time-stop at 16:00 ET (flat at the session close).
    • overnight ON: the position is held until target or stop, for up to MAX_HOLD_DAYS trading days (default 5).
  6. Costs: COST per side (default 0.02%), applied to entry and exit, expressed in R.
  7. Conservative assumption: if a single bar touches both stop and target, the stop is counted.

Lesson the engine confirms: with overnight OFF, intraday targets are rarely reached — most trades exit by time-stop. The UI therefore always shows the exit-reason breakdown (target / stop / time-stop) for every configuration.

ICT parameters

Each has a ? tooltip in the app. Defaults in parentheses.

Parameter Default Meaning
Bar interval 60m Candle size the strategy looks for setups on (60m/30m/15m/1d)
Overnight off Hold past 16:00 ET, or flatten at the close
Max position size (% of budget) 10% Notional committed per trade, as a % of the current fund budget
Exit / target method rr_fisso rr_fisso / liquidita / trailing
Win: target RR 2.0 How much a winning target earns, in multiples of R (the win lever)
Loss: stop width (×) 1.0 Multiplies the base stop distance (the loss/win-rate lever)
Trailing % 0.50% Trailing-stop width (only for trailing)
Max holding days 5 Max trading days held (only when overnight is on)
SWING_WIN 2 Half-window (in bars) defining a local swing
EQ_TOL 0.10% Tolerance for "equal" highs/lows
LAST_ENTRY 14:30 ET No new entries after this time
MAX_TRADES_DAY 1 Max trades per instrument per day
BUFFER 0.05% Extra margin beyond the swept extreme for the stop
FVG_LOOKBACK 6 Bars within which a formed FVG remains a valid trigger
COST 0.02% per side Commissions + slippage, subtracted from the R result

The two win/loss levers

  • Win — target RR sets how much a winning trade earns (RR × R). Higher RR = bigger wins but a lower hit rate (the target is reached less often).
  • Loss — stop width (×) sets how far the stop sits beyond the swept extreme. A wider stop gets stopped out less often (higher win rate) but pushes the price target further away; a tighter stop does the opposite. In R terms a full loss is always −1; what changes is how frequently it happens.

These two levers let you explore the whole expectancy space yourself (expectancy = win_rate × RR − (1 − win_rate) × 1 − cost).


Strategy 2 — Trend following

A classic time-series momentum strategy on daily bars:

  • Long when the closing price is above its moving average (e.g. the 200-day SMA); otherwise flat (cash), or short if you enable shorting.
  • The signal computed on a given day is applied to the next day's return (shift(1)), so there is no look-ahead.
  • With several tickers it builds an equal-weighted portfolio.

Trend parameters

Parameter Default Meaning
Moving average (days) 200 The trend filter window. Longer = fewer trades, follows the broad trend
Allow short off Go short below the average instead of staying flat

The trend tab reuses the shared tickers, period, providers, and budget, and it always uses daily (1d) data regardless of the bar interval chosen for ICT.

Trend output

  • Budget growth chart: the strategy vs Buy & Hold of the basket vs the S&P 500 (dotted line).
  • KPIs: final budget, CAGR, Sharpe ratio, maximum drawdown, average market exposure, and number of position switches.
  • A comparison table of return / CAGR / volatility / Sharpe / max drawdown for the strategy vs buy-and-hold.

Trend following is included as an example of a documented edge (a risk premium / momentum anomaly). It typically does not beat the S&P 500 on absolute return in a bull market, but it tends to improve risk-adjusted results — higher Sharpe and much smaller drawdowns, because it steps aside when price falls below the average.


The fund model and position sizing

Both the ICT backtest and the trend strategy run on a single shared fund.

  • For ICT, all trades of all selected tickers are merged into one chronological sequence and executed on the common budget, which compounds over time.

  • Position sizing: each ICT trade commits a notional equal to max_size_pct × current_budget. Given the notional, the number of shares is notional / entry, and the money P&L of a trade is:

    risk_per_share = |stop − entry|
    shares         = notional / entry
    pnl            = R × shares × risk_per_share      (R is already net of costs)
    
  • The Fund section reports starting and final budget, total P&L and return %, volume generated (sum of the notional values traded), maximum drawdown %, the fund budget curve, and the full list of operations in chronological order (with a ticker column), downloadable as CSV.

  • Each ticker's panel shows its contribution to the fund (P&L in currency) plus its strategy metrics in R. The sum of contributions equals the fund's total P&L.


Parameter optimization (with out-of-sample validation)

Under the ICT tab, the Optimization section lets you sweep many parameter combinations and — crucially — verify the best ones on data they were not tuned on. This is the defense against overfitting.

How it works:

  1. You choose lists of values for RR, stop width ×, overnight, and method, and an in-sample fraction.
  2. The app downloads each ticker once and pools the trades of all instruments for a larger sample.
  3. It evaluates every combination (Cartesian product, capped at 300).
  4. It splits the history into in-sample (where you "optimize") and out-of-sample (the unseen verification period), based on the in-sample fraction.
  5. It shows, per combination, the expectancy in R for both IS and OOS, total OOS R, OOS profit factor, and trade counts.

A combination is flagged Robust (✅) only if its expectancy is positive both in-sample and out-of-sample, with at least 30 OOS trades. A combination that shines only on the past and collapses on the unseen period is not an edge — it is an illusion. The table is downloadable as CSV.

The strategy is intraday (FVGs and sweeps happen within a single day), so optimization should be run on intraday intervals (60m/30m/15m); on 1d it generates no trades.


S&P 500 benchmark

Both strategies report performance against a buy-and-hold of the S&P 500 (via the SPY ETF, daily) over the same period:

  • ICT: a line under the Fund metrics — "S&P 500 in the period: +X% · fund above/below by N points".
  • Trend: a dedicated dotted line in the growth chart (alongside the strategy and the basket's buy-and-hold), plus the gap in the caption.

This makes the verdict concrete: a strategy is only interesting if it beats — on return, or at least on risk-adjusted terms — simply buying and holding the index.


Saved simulations and persistence

  • After each run, a Last simulation summary is stored and shown at the top of the results area. It persists across reruns, so it does not vanish when you tweak the inputs.
  • Press 💾 Save to append that simulation to saved_simulations.json, a local archive that survives app restarts.
  • The 📁 Saved simulations expander lists every saved run (strategy, tickers, period, key metrics), lets you download the archive as CSV, and delete individual rows.

The archive stores the summary (parameters + key metrics), not the full interactive charts — exactly what you need to compare past runs at a glance and find them again after a restart.


Data providers, fallback, and download limits

You can pick one or more free, no-API-key data sources, and the order of selection sets the priority. If the first source errors or has no data, the app immediately falls back to the next; the source that actually served each ticker is shown under its header.

Available providers:

  • Yahoo (yfinance) — intraday (60m/30m/15m) and daily (1d). The most complete source. Adding a new provider is easy: register a function (ticker, interval, start, end) -> OHLC DataFrame in engine.PROVIDERS.
  • Stooq — daily (1d) only, via public CSV (it tries both stooq.com and stooq.pl). For intraday it returns nothing and yields to the fallback. Note: from some IPs (e.g. data centers) Stooq applies an anti-bot gate and may not respond — in that case the fallback takes over.

Chunked downloading

Yahoo limits the width of each single intraday request (e.g. ~60 days for 15/30-minute bars). A single over-wide request returns empty. To work around this, the download is automatically split into chunks (download_chunked): requests are made in allowed-size windows, starting from the most recent, and empty or failing chunks are skipped. The system thus always retrieves the available data, even just the recoverable tail, instead of failing entirely.

Availability limits

Beyond the per-request limit, Yahoo simply does not have intraday history older than a certain window:

  • 1m ≈ 7 days
  • 15m / 30m ≈ 60 days
  • hourly ≈ 730 days
  • 1d: effectively unlimited

For long histories use the 1d interval. If no provider returns any data (nonexistent ticker, or a period entirely outside the available window), the app shows a clear message and skips only that ticker. Free data can contain gaps or small inaccuracies; treat results as indicative.


Metrics glossary

Metric Meaning
R Risk unit = |stop − entry|. Every ICT outcome is expressed in multiples of R, net of costs.
Win rate Fraction of trades with a positive R result.
Expectancy (R) Average R per trade. The core edge measure: positive = profitable before sizing.
Total R Sum of R across all trades.
Profit factor Gross profit ÷ gross loss. Above 1 = net positive.
Max drawdown (R / %) Largest peak-to-trough decline of the equity/budget curve.
Exit breakdown Count of trades exited by target / stop / time-stop.
Return % Total percentage return of the fund over the period.
CAGR Compound annual growth rate (trend strategy).
Sharpe Annualized return ÷ annualized volatility (risk-free = 0).
Volume generated Sum of the notional values traded across all trades.
Contribution to fund A single ticker's money P&L within the shared fund.
Exposure Fraction of days the trend strategy is invested (not flat).

If the total number of trades is below 30, a yellow banner warns that the sample is too small for reliable statistical conclusions — you need more instruments and/or more years of data to estimate a real edge.


Project structure

File Role
engine.py Pure, testable backtest logic. No Streamlit, no network inside the strategy logic (downloading is isolated). Contains the ICT engine, the trend strategy, the money/fund model, the optimization grid, and the data providers.
app.py The Streamlit UI: collects inputs, calls the engine, renders results, manages the saved-simulation archive.
test_engine.py The automated test suite (pytest), using synthetic datasets — no network.
requirements.txt Python dependencies.
README.md This file.
.claude/launch.json Dev-server launch configuration (Streamlit on port 8501).
saved_simulations.json Created at runtime when you save a simulation.

Testing

pytest

The suite (39 tests) covers, with hand-built synthetic data and no network:

  • detect_fvgs on known bullish/bearish/no-gap cases, and find_swings.
  • The full backtest runs without errors on synthetic data.
  • No look-ahead: a trade's result does not depend on bars before entry.
  • With overnight OFF, no trade stays open past the end of the day.
  • Coherence of overnight ON vs OFF.
  • The money/fund model (known values, compounding, volume, drawdown, invalid inputs) and the per-trade money table.
  • The stop-width multiplier widens the stop as expected.
  • Chunked downloading splits and concatenates windows, skips empty/erroring chunks, and de-duplicates overlapping timestamps (with an injected fetcher — no real network).
  • Provider fallback: priority order, first-success-wins, all-failed raises, reversed order changes the source.
  • The optimization grid: Cartesian product, date split, in/out-of-sample results, multi-ticker aggregation.
  • The trend strategy: captures an uptrend, stays flat in a downtrend without shorting, profits from a downtrend when shorting is on, costs reduce return, and insufficient data returns None.

Honest notes on edge, costs, and overfitting

  • A simple mechanical rule rarely has a real edge after costs. With the ICT strategy at default parameters, the win rate tends to sit right around the break-even rate for the chosen RR, so the expectancy before costs is near zero, and transaction costs then push it slightly negative. This is a legitimate backtest result, not a bug — the engine's arithmetic is exact (a fixed-RR win pays exactly +RR, a loss exactly −1).
  • Costs loom large with tight intraday stops. A round trip of ~0.04% of notional against a stop of, say, ~0.6% of price is ~0.06 R per trade. On a zero-edge strategy, any positive cost guarantees a loss over time.
  • Small samples are noise. A single ticker on hourly bars over two years may produce only a couple dozen trades — far too few for conclusions. Pool many instruments and use the optimization's out-of-sample check.
  • The best in-sample combination often fails out-of-sample. That is exactly what the Robust (✅) filter is designed to expose.

The most robust, replicable edge for a non-professional is boring: diversified, systematic exposure to risk premia (trend following across many assets, or a low-cost factor portfolio), held with discipline over years — not discretionary intraday patterns.


Data sources & terms of use

This project ships code only — it contains and redistributes no market data. Prices are fetched at runtime from free sources:

  • Yahoo Finance via the open-source yfinance library. Yahoo's terms are intended for personal, non-commercial use; they restrict redistribution and do not formally endorse automated scraping. Use the data for your own research and learning.
  • Stooq free CSV feed — likewise fine for personal/educational use, not for redistribution.

In short: running this app for your own backtesting and learning is the intended, low-risk use. Do not redistribute the downloaded data, and check the providers' current terms before any commercial use. This tool is educational and is not financial advice.

Limitations

  • Free yfinance / Stooq data: gaps, occasional inaccuracies, and the intraday availability limits described above.
  • Futures symbols (=F) have extended trading hours that do not match equity RTH; use the 1d interval for them.
  • The fund model assumes fills at the modeled entry/exit prices and a flat per-side cost; real slippage, partial fills, and borrow costs are not modeled.
  • The app is single-process and local; the saved-simulation archive is a plain local JSON file.

About

Backtest ICT intraday and Trend-following strategies on US market data — Streamlit app with shared-fund model, out-of-sample optimization, S&P 500 benchmark, and free data providers with fallback.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages