KhizarImran d54c3827b8 chore: fix clippy warnings and enforce clippy in CI
Replace four map_or(false, ..) calls in check_sl_tp with is_some_and, which
reads closer to the intent ("there is a stop and price hit it").

CI now runs `cargo clippy --all-targets -- -D warnings` in place of
`cargo check` — clippy is a superset, so this is more coverage in one
fewer step, and -D warnings stops the lint debt building back up.
2026-08-02 22:28:59 +01:00
2026-07-17 14:54:59 +00:00
2026-07-05 16:37:12 +01:00
2026-06-03 23:38:12 +01:00

backtestingfx

PyPI

A Rust-powered FX backtesting library for Python. Write your strategy in Python, let Rust handle the heavy lifting.

Inspired by backtesting.py but built specifically for forex — lot sizes, pip-based PnL, stop loss, take profit, and realistic account currency conversion.

pip install backtestingfx

Quick Start

import pandas as pd
from backtestingfx import Backtest, Strategy

class MyCrossStrategy(Strategy):
    def next(self):
        self.close_all()
        self.buy(lot_size=0.1)

df = pd.read_csv("EURUSD_1H.csv")

bt = Backtest(df, MyCrossStrategy, cash=10000.0, spread=0.0001)
stats = bt.run()

print(stats)
--- Backtest Results ---
Initial Cash:   10000.00
Final Cash:     9823.50
Total Return:   -1.77%
Trades:         248
Win Rate:       52.4%
Avg PnL:        -0.71380
Best Trade:     84.20000
Worst Trade:    -61.30000
Profit Factor:  0.94
Max Drawdown:   3.21%

Interactive HTML report

Install the optional report dependency and generate a self-contained HTML file:

pip install "backtestingfx[report]"
bt = Backtest(df, MyCrossStrategy, cash=10000.0, spread=0.0001)
stats = bt.run()
bt.plot("strategy-report.html")

The report includes candlesticks, trade entries and exits, equity, drawdown, trade diagnostics, and a complete trade ledger. Plotly is embedded in the file, so the report works offline without a server.

Installation

pip install backtestingfx

Requires Python 3.9+.

Writing a Strategy

Inherit from Strategy and implement next(). It is called once per bar.

from backtestingfx import Backtest, Strategy

class MyStrategy(Strategy):
    def init(self):
        # called once before the loop starts
        # self._bars contains all Bar objects if you need to pre-compute
        pass

    def next(self):
        # self._bar  — current bar (open, high, low, close, volume, timestamp)
        # self._broker — the broker instance (advanced use)

        if self._bar.close > 1.1000:
            self.buy(lot_size=0.1, stop_loss=1.0950, take_profit=1.1100)
        else:
            self.close_all()

Strategy methods

Method Description
self.buy(lot_size, stop_loss=None, take_profit=None) Open a long position
self.sell(lot_size, stop_loss=None, take_profit=None) Open a short position
self.close_all() Close all open positions
self.close_position(id) Close a specific position by ID

Bar fields

self._bar.open
self._bar.high
self._bar.low
self._bar.close
self._bar.volume
self._bar.timestamp  # unix timestamp (int)

Optimization

Grid-search parameters with the simulations running in parallel Rust threads:

import numpy as np
from backtestingfx import Backtest

def sma_cross(df, fast, slow):
    fast_sma = df["close"].rolling(fast).mean()
    slow_sma = df["close"].rolling(slow).mean()
    return np.where(fast_sma > slow_sma, 0.1, 0.0)   # target lots per bar

bt = Backtest(df, cash=10_000, commission=3.5)
results = bt.optimize(sma_cross, maximize="total_return_pct",
                      fast=range(5, 26), slow=range(30, 101, 5))

best_params, best_stats = results[0]

optimize() returns [(params, stats), ...] sorted best-first by the named Stats field. Re-sort it yourself to minimise something instead.

Why a signal function instead of next()

next() runs in Python, so every bar needs the GIL and threads can't help. A signal function is called once per parameter combination, not once per bar — it returns the target lot size for each bar (positive long, negative short, 0.0 flat), and Rust runs every simulation natively with the GIL released. On a 315-combination grid that is 157x faster than looping Backtest.run() over the same grid.

The trade-off: a signal function can't see the broker, so path-dependent logic (trailing stops, pyramiding, "exit after N bars") still needs next() and a plain loop. Indicator warmup must come out as 0.0, not NaN — a NaN signal is rejected rather than silently treated as "hold".

Backtest Parameters

Backtest(
    df,                       # pandas DataFrame with OHLCV columns
    StrategyClass,
    cash=10000.0,             # starting account balance in USD
    commission=0.0,           # commission per lot (e.g. 7.0 = $7/lot)
    spread=0.0,               # spread in price units (e.g. 0.0001 = 1 pip)
    contract_size=100000.0,   # standard FX lot size, don't change this
    quote_to_account=1.0,     # conversion rate from quote currency to USD
)

Trading non-USD pairs

By default quote_to_account=1.0 which is correct for USD-quoted pairs (EURUSD, GBPUSD).

For other pairs, pass the rate that converts the quote currency to USD:

Pair quote_to_account
EURUSD, GBPUSD 1.0 (default)
EURGBP GBPUSD rate (e.g. 1.27)
USDCAD, GBPCAD CADUSD rate (e.g. 0.74)
USDJPY JPYUSD rate (e.g. 0.0067)
bt = Backtest(df, MyStrategy, cash=10000.0, spread=0.00015, quote_to_account=1.27)

Stats

Field Description
initial_cash Starting balance
final_cash Ending balance
total_return_pct Total return as a percentage
num_trades Number of completed trades
num_wins Number of winning trades
win_rate_pct Win rate as a percentage
avg_pnl Average PnL per trade in USD
best_trade Best single trade PnL in USD
worst_trade Worst single trade PnL in USD
profit_factor Gross profit / gross loss
max_drawdown_pct Maximum drawdown as a percentage
sharpe_ratio Unannualized Sharpe ratio
equity_curve Account equity from initial cash through final liquidation
trades Completed trades with entry, exit, size, direction, and net PnL

Data Format

Pass a pandas DataFrame with these columns:

open, high, low, close, volume

The index should be a DatetimeIndex, or include a timestamp column. Volume is optional (defaults to 0).

Why Rust?

The backtesting engine is written in Rust and compiled as a native Python extension via PyO3. This means the event loop, broker simulation, and stats computation run at native speed while your strategy stays in plain Python.

License

MIT — see LICENSE

S
Description
No description provided
Readme MIT 3.1 MiB
Languages
Rust 51.4%
Python 48.6%