Deterministic tick-level FX backtesting for reproducible research.
Tick Backtest is a configuration-first Python 3.12 package for FX strategy research. You provide Parquet tick shards and YAML configs; the package validates the configuration, runs deterministic backtests, and writes manifests, logs, reports, and analysis artefacts to disk.
- Performance: ~130k ticks/second/core on AMD 5950X (Parquet -> metrics -> signals -> trades)
- Deterministic runs: config snapshots, git hash, dependency snapshot, and shard hashes are captured per run
- Resilient pipelines: per-pair failure isolation, tick validation, and structured telemetry
- Declarative research: swap YAML configs instead of editing strategy code
- Report ready: trade tables, Markdown summaries, metric stratification CSV/PNG artefacts
- CLI + API parity: every supported command is exposed both as
tick-backtest ...andtick_backtest.api.*(...)
Documentation is hosted here: Documentation Site. Release process details live in docs/releasing.md.
Installed-package usage is the primary workflow.
python3.12 -m venv .venv
source .venv/bin/activate
pip install tick-backtestThe package uses compiled extensions for core runtime components. Normal installed usage assumes those extensions are available through the package build/install process.
- Generate a runnable demo project with bundled fixture data:
tick-backtest example-config --output ./demo --include-demo-data
- Run the demo backtest:
tick-backtest run ./demo/backtest.yaml
- Generate report artefacts for one pair:
tick-backtest report ./demo/output/<RUN_ID>/output/EURUSD/trades.parquet
- Run multivariate trade analysis:
tick-backtest analyze ./demo/output/<RUN_ID>/output/EURUSD/trades.parquet
The same surface is available from Python:
from tick_backtest import api
api.example_config("./demo", include_demo_data=True)
api.run("./demo/backtest.yaml")
api.report("./demo/output/<RUN_ID>/output/EURUSD/trades.parquet")
api.analyze("./demo/output/<RUN_ID>/output/EURUSD/trades.parquet")The generated demo project contains:
backtest.yaml,metrics.yaml, andstrategy.yamldemo_data/with bundled EURUSD and GBPUSD Parquet shardsoutput/as the configured run destination
To start from generic packaged templates instead of the demo project:
tick-backtest example-config --output ./tick-backtest-configEdit the generated backtest.yaml:
schema_version: "1.0"
pairs: [EURUSD]
start: 2024-01
end: 2024-01
pip_size: 0.0001
warmup_seconds: 1800
data_base_path: "/abs/path/to/tick_data/"
output_base_path: "/abs/path/to/backtest_outputs/"
metrics_config_path: "./metrics.yaml"
strategy_config_path: "./strategy.yaml"The companion metrics.yaml and strategy.yaml emitted by the shipped minimal template currently look like:
# metrics.yaml
schema_version: "1.0"
metrics:
- name: z30m
type: zscore
enabled: true
params:
lookback_seconds: 1800
- name: tick_rate_30s
type: tick_rate
enabled: true
params:
window_seconds: 30# strategy.yaml
schema_version: "1.0"
strategy:
name: threshold_reversion_strategy
entry:
name: threshold_reversion_entry
engine: threshold_reversion
params:
lookback_seconds: 1800
threshold_pips: 10
tp_pips: 10
sl_pips: 20
trade_timeout_seconds: 7200
predicates:
- metric: tick_rate_30s.tick_rate_per_min
operator: "<"
value: 200
exit:
name: default_exit
predicates: []These examples are copied from the shipped minimal template surface under src/tick_backtest/config/templates/minimal/, not maintained as separate pseudo-examples.
Expected data layout:
- Tick shards are organised as
{data_root}/{PAIR}/{PAIR}_YYYY-MM.parquet - Required Parquet columns are
timestamp,bid, andask startandendare inclusive year-month boundariesdata_base_path,output_base_path,metrics_config_path, andstrategy_config_pathare resolved relative to the directory containingbacktest.yaml
Tick Backtest does not download market data itself. If you need a source-to-parquet workflow, dukascopy-python is a suitable external option.
If you source data from Dukascopy, treat Tick Backtest's Parquet layout as a separate ingestion target. Tick Backtest does not read Dukascopy raw exports directly. Convert the downloaded data into monthly Parquet shards, keep one directory per pair, and ensure each shard exposes timestamp, bid, and ask columns before pointing data_base_path at the archive.
Tick Backtest does not impose a portfolio- or experiment-level directory scheme beyond writing each run to output_base_path/<RUN_ID>/. For repeatable research, it is often useful to group related runs under an experiment directory and point output_base_path at an experiment-specific runs/ folder, for example:
research/
configs/
experiments/
mean_reversion_q2_2026/
runs/
<RUN_ID>/
notes/
summaries/
This keeps the package flexible while still giving you a clean place to organise sweeps, comparisons, and follow-up analysis.
Starter strategy guidance:
- the
minimaltemplate starts withthreshold_reversion_strategy - the runnable demo template uses
ewma_crossover
These are packaged starters for validation and experimentation, not production recommendations. For a first run on your own archive, keep the emitted strategy unchanged until the data layout and run outputs look correct.
Execution model limits:
- no commissions or fees
- no slippage model
- no order book depth, queue position, or market impact model
- no exchange-specific matching or partial-fill simulation
The engine is intended for signal and strategy research with simplified fills, not full execution-cost simulation.
After tick-backtest run, inspect outputs under the resolved output_base_path/<RUN_ID>/:
| Path | Purpose |
|---|---|
manifest.json |
Immutable run snapshot containing configs, git hash, shard hashes, status, and output metadata |
environment.txt |
Dependency snapshot from pip freeze |
output/logs/<RUN_ID>.log |
Structured NDJSON log with validation summaries and runtime errors |
output/<PAIR>/trades.parquet |
Trade-level dataset including entry metadata, metrics, and PnL |
configs/*.yaml |
Copies of backtest, metrics, and strategy configs with SHA256 digests |
After tick-backtest report <trades.parquet>, additional artefacts are written beside the trade file:
| Path | Purpose |
|---|---|
trades_report.md |
Markdown performance summary for the selected trade file |
trades_equity_curve.png |
Equity curve plot referenced by the report |
metric_stratification/ |
Stratification CSV, graph, and Markdown report bundles |
After tick-backtest analyze <trades.parquet>, multivariate artefacts are written beside the trade file under multivariate_analysis/.
This bundle includes summary.md, coefficients.csv, correlations.csv, and dropped_predictors.csv.
To move from a completed run into post-processing, locate a concrete trade file first:
find ./demo/output -path '*/output/*/trades.parquet' | sortThen pass one of those files to report or analyze.
| Command | Input | Output location |
|---|---|---|
tick-backtest run <backtest.yaml> |
Backtest config | Writes a run directory under the configured output_base_path/<RUN_ID>/ |
tick-backtest report <trades.parquet> |
Trade database | Writes trade report artefacts and metric stratification beside the parquet file |
tick-backtest analyze <trades.parquet> |
Trade database | Writes multivariate_analysis/ beside the parquet file |
tick-backtest example-config [--output DIR] [--include-demo-data] |
Optional destination dir | Prints starter YAML or writes a template set or runnable demo project |
| Function | Purpose |
|---|---|
tick_backtest.api.run(config_path, *, output_root=None) |
Run the backtest engine and write run artefacts only |
tick_backtest.api.report(trades_path) |
Generate trade report artefacts and metric stratification outputs |
tick_backtest.api.analyze(trades_path) |
Generate multivariate regression-style analysis outputs |
tick_backtest.api.example_config(dest=None, *, template="minimal", include_demo_data=False) |
Print or write starter YAML templates, optionally with bundled demo data |
The API is intentionally filesystem-oriented. It writes artefacts to disk and does not aim to return in-memory result objects.
- Public packaged starter assets live under
src/tick_backtest/config/templates/andsrc/tick_backtest/demo_data/, and are exposed throughtick-backtest example-config - Checkout-only development fixtures live under
src/tick_backtest/config/and support repo smoke/golden workflows rather than the installed-package surface - Backtest configs are parsed into validated dataclasses before runtime
- Tick data is streamed from Parquet month by month and wrapped in a validator that skips invalid ticks
- Per-pair execution remains sequential to avoid lookahead bias
- Metrics, signals, and position handling run inside the backtest loop and completed trades persist to Parquet
- Reporting and regression-style analysis are post-run workflows invoked separately from the backtest itself
Data flow
- Parse and validate backtest, metrics, and strategy YAML.
- Stream Parquet ticks through the validating feed.
- Update metrics, evaluate signals, and manage positions one tick at a time.
- Persist trades, logs, config snapshots, manifest, and dependency snapshot to disk.
- Run
reportoranalyzelater against a chosentrades.parquet.
Dive deeper in the Developer Notes.
If you are working from a repository checkout rather than an installed package:
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pip install -e .
ruff check src tests scripts setup.py
mypy
pytestThis editable install step is required for a clean local test run because the package builds compiled extensions used by the runtime and test suite.
If you also want to build the docs locally:
pip install -r requirements-docs.txtIf you want to build distribution artefacts locally:
python -m buildInstalled usage should go through tick-backtest or tick_backtest.api. Repository helper scripts under scripts/ are secondary development and CI utilities.
| Symptom | Likely Cause | Fix |
|---|---|---|
ConfigError: unknown field ... |
Extra keys in YAML | Remove or rename; see the Configuration Guide |
pyarrow import error |
Wheel missing | Install the package dependencies and rerun |
ModuleNotFoundError for compiled tick_backtest modules in a repo checkout |
Editable install/build step missing | Run pip install -e . inside the active virtualenv |
| Run finishes but no trades | Warmup consumed data or predicates blocked | Check output/logs/<RUN_ID>.log and entry predicates |
Manifest shows missing_file |
data_base_path does not match the expected shard layout |
Adjust the path or supply the expected Parquet shards |
Percentile metrics return NaN |
Histogram warming up | Feed more ticks; this is expected during the early part of a run |
- Python 3.12
numpy >= 1.26, < 3.0pandas >= 1.5, < 2.3pyarrow >= 10.0, < 16.0matplotlib >= 3.7, < 3.9pyyaml >= 6.0, < 6.1
Running offline? Pre-install these wheels in your environment. Backtests require pip freeze to succeed so the dependency snapshot can be captured in environment.txt.
For a clean local test run from a checkout:
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pip install -e .
pytestCoverage highlights:
tests/config_parsers- YAML schema governance and regression checkstests/data_feed- tick validation and resiliencetests/metrics- primitives plus indicator mathematics with reference helperstests/integration/test_backtest_run.py- end-to-end pipeline regression
GitHub Actions builds wheels and sdists, smoke-tests installed wheel and sdist paths, runs tests, validates distribution metadata, and publishes docs via .github/workflows/.
- Generate a starter config with
tick-backtest example-config. - Point it at your own Parquet tick data.
- Run
tick-backtest runand inspect the generated manifest and pair-level artefacts. - Explore the documentation for advanced configuration and internals.
Author: Edward Clewer
License: Apache License 2.0
Docs: Docs