Testing Bollinger Band and RSI Mean Reversion on Proxy FX Data
Summary
The tutorial explains how to backtest a mean-reversion strategy on EUR/USD perpetual futures using TrueFX spot ticks as proxy data. It builds one-minute mid-price bars, then combines a 20-period Bollinger Band with a 14-period RSI: a lower-band touch with RSI below 30 triggers a long, and an upper-band touch with RSI above 70 triggers a short. Positions close when price crosses the Bollinger middle line, with opposite positions flattened before a new entry.
The December 2025 replay produced 44,591 bars and closed 1,089 positions across 2,178 fills, ending with realized P&L of -1,287 USD. The tutorial attributes the decline to spread costs and repeated short trades during a pronounced EUR/USD uptrend. It presents this as a deliberately simple strategy without an edge and suggests testing regime filters, stricter thresholds, and stops. Results depend on proxy data, assumed fees and margin, and one month of observations; they do not establish how the strategy performs on AX Exchange or in other market conditions.
Key ideas
- The strategy enters when a Bollinger Band extreme coincides with an RSI threshold.
- Positions exit when price crosses back through the Bollinger middle line.
- TrueFX EUR/USD spot ticks serve as proxy inputs for EUR/USD perpetual futures.
- The documented December replay lost money amid spread costs and a sustained uptrend.
- A single month of proxy-data results cannot establish performance across venues or regimes.
Tags
Full text
# Mean Reversion with Proxy FX Data (AX Exchange)
# Mean Reversion with Proxy FX Data (AX Exchange)
This tutorial backtests a Bollinger-band mean-reversion strategy on
**EURUSD-PERP** at [AX Exchange](https://architect.exchange) using
[TrueFX](https://www.truefx.com) EUR/USD spot ticks as a proxy.
## Introduction
The strategy combines two indicators on 1-minute mid bars:
- **Bollinger Bands** (`BBMeanReversion`'s `BB(20, 2.0sd)`): a rolling
20-bar mean and a +/-2sd envelope. The bands flag price as overextended
relative to recent volatility.
- **Relative Strength Index** (`RSI(14)`): a 14-bar momentum oscillator on
`[0, 100]`, used with the conventional 30/70 thresholds.
Entry needs both signals at once: a touch of the lower band with `RSI < 30`
opens a long; a touch of the upper band with `RSI > 70` opens a short.
Exit is one-sided: any open position closes when the close crosses back
through the BB middle. Existing positions on the opposite side are flattened
before a new entry.
The shipped `BBMeanReversion` strategy is intentionally simple and has no
edge.
```mermaid
flowchart LR
subgraph Inputs ["Data"]
Q["TrueFX bid/ask ticks"]
end
subgraph Engine ["BacktestEngine"]
W["QuoteTick construction"]
AGG["1-minute MID INTERNAL aggregator"]
BAR["Bar close"]
end
subgraph Indicators
BB(("BB(20, 2.0sd)"))
RSI(("RSI(14)"))
end
subgraph Decision ["Decision"]
EX{{"Net long AND close >= mid<br/>OR<br/>net short AND close <= mid"}}
ENL{{"close <= lower<br/>AND RSI < 30"}}
ENS{{"close >= upper<br/>AND RSI > 70"}}
end
subgraph Orders
CL["Close all positions"]
BUY["BUY market"]
SELL["SELL market"]
end
Q --> W --> AGG --> BAR
BAR --> BB
BAR --> RSI
BB --> ENL
BB --> ENS
RSI --> ENL
RSI --> ENS
BB --> EX
EX -->|yes| CL
ENL -->|yes| BUY
ENS -->|yes| SELL
CL --> BUY
CL --> SELL
```
### Why proxy data
AX Exchange is a new venue not yet covered by historical data vendors.
[TrueFX](https://www.truefx.com) publishes free, institutional-grade EUR/USD
spot tick archives (Integral and Jefferies pools) that stand in cleanly
for AX EURUSD-PERP backtests.
## Prerequisites
- Python 3.13+
- [NautilusTrader installed](../getting_started/installation.md) with the
[`visualization` extra](../getting_started/installation.md#extras), which
provides pandas.
- A source checkout of the repository. The backtest imports
`BBMeanReversion` from `examples/live/architect_ax/strategies.py`, which
the installed package does not include.
- A free TrueFX account, used to download a monthly tick archive.
## Data preparation
### Download TrueFX EUR/USD ticks
1. Go to the [TrueFX historical downloads page](https://www.truefx.com/truefx-historical-downloads/).
2. Pick **EUR/USD** and a month, for example **December 2025**.
3. Extract the ZIP. The CSV is headerless with columns
`pair, timestamp, bid, ask`.
### Load into Nautilus quote ticks
Define `EURUSD_PERP` and `instrument_id` in the next section before running this snippet.
```python
from pathlib import Path
import pandas as pd
from nautilus_trader.model import Quantity
from nautilus_trader.model import QuoteTick
df = pd.read_csv(
Path("EURUSD-2025-12.csv"),
header=None,
names=["pair", "timestamp", "bid", "ask"],
)
df["timestamp"] = pd.to_datetime(
df["timestamp"],
format="%Y%m%d %H:%M:%S.%f",
utc=True,
)
df = df.set_index("timestamp")[["bid", "ask"]].sort_index()
ticks = []
for timestamp, row in df.iterrows():
ts_ns = pd.Timestamp(str(timestamp)).value
ticks.append(
QuoteTick(
instrument_id=instrument_id,
bid_price=EURUSD_PERP.make_price(float(row.bid)),
ask_price=EURUSD_PERP.make_price(float(row.ask)),
bid_size=Quantity.from_int(1),
ask_size=Quantity.from_int(1),
ts_event=ts_ns,
ts_init=ts_ns,
),
)
```
Each quote carries the proxy instrument ID and one unit of bid and ask size.
The strategy declares
`1-MINUTE-MID-INTERNAL`, so the engine builds 1-minute MID bars from the
tick stream internally.
## Instrument definition
Proxy data needs a manual instrument definition. The multiplier of `1000`
gives one contract a notional of 1,000 EUR.
```python
from decimal import Decimal
from nautilus_trader.model import AssetClass
from nautilus_trader.model import Currency
from nautilus_trader.model import InstrumentId
from nautilus_trader.model import PerpetualContract
from nautilus_trader.model import Price
from nautilus_trader.model import Quantity
from nautilus_trader.model import Symbol
USD = Currency.from_str("USD")
instrument_id = InstrumentId.from_str("EURUSD-PERP.AX")
EURUSD_PERP = PerpetualContract(
instrument_id=instrument_id,
raw_symbol=Symbol("EURUSD-PERP"),
underlying="EUR",
asset_class=AssetClass.FX,
quote_currency=USD,
settlement_currency=USD,
is_inverse=False,
price_precision=5,
size_precision=0,
price_increment=Price.from_str("0.00001"),
size_increment=Quantity.from_int(1),
multiplier=Quantity.from_int(1000),
lot_size=Quantity.from_int(1),
margin_init=Decimal("0.05"),
margin_maint=Decimal("0.025"),
ts_event=0,
ts_init=0,
)
```
Fees and margin are explicit backtest assumptions. Check the
[AX Exchange documentation](https://docs.architect.exchange/) for current
rates.
## Configuration
| Parameter | Value | Description |
| -------------------- | ------ | ---------------------------------------------------------- |
| `bb_period` | `20` | Rolling window for the BB mean and the standard deviation. |
| `bb_std` | `2.0` | Band width in standard deviations. |
| `rsi_period` | `14` | RSI lookback in bars. |
| `rsi_buy_threshold` | `30.0` | Long entry confirmation (RSI is on `[0, 100]`). |
| `rsi_sell_threshold` | `70.0` | Short entry confirmation. |
| `trade_size` | `1` | One contract per trade (1,000 EUR notional). |
## Backtest setup
From the repository root:
```python
import sys
from decimal import Decimal
from pathlib import Path
from nautilus_trader.backtest import BacktestEngine
from nautilus_trader.common import LogLevel
from nautilus_trader.config import BacktestEngineConfig
from nautilus_trader.config import LoggerConfig
from nautilus_trader.execution import MakerTakerFeeModel
from nautilus_trader.model import AccountType
from nautilus_trader.model import BarType
from nautilus_trader.model import Money
from nautilus_trader.model import OmsType
from nautilus_trader.model import TraderId
from nautilus_trader.model import Venue
sys.path.insert(0, str(Path("examples/live/architect_ax")))
from strategies import BBMeanReversion
from strategies import BBMeanReversionConfig
engine = BacktestEngine(
BacktestEngineConfig(
trader_id=TraderId.from_str("BACKTESTER-001"),
logging=LoggerConfig(stdout_level=LogLevel.INFO),
),
)
AX = Venue("AX")
engine.add_venue(
venue=AX,
oms_type=OmsType.NETTING,
account_type=AccountType.MARGIN,
base_currency=USD,
starting_balances=[Money.from_str("100000 USD")],
fee_model=MakerTakerFeeModel(
maker_rate=Decimal("0.0002"),
taker_rate=Decimal("0.0005"),
),
)
engine.add_instrument(EURUSD_PERP)
engine.add_data(ticks)
strategy = BBMeanReversion(
config=BBMeanReversionConfig(
instrument_id=instrument_id,
bar_type=BarType.from_str("EURUSD-PERP.AX-1-MINUTE-MID-INTERNAL"),
trade_size=Decimal("1"),
bb_period=20,
bb_std=2.0,
rsi_period=14,
rsi_buy_threshold=30.0,
rsi_sell_threshold=70.0,
),
)
engine.add_strategy(strategy)
engine.run()
```
Reports come off the engine:
```python
print(engine.generate_account_report(venue=AX))
print(engine.generate_order_fills_report())
print(engine.generate_positions_report())
engine.reset()
engine.dispose()
```
The self-contained runnable example uses the bundled AUD/USD fixture with the
same strategy and setup pattern. It is at
[`architect_ax_mean_reversion.py`](https://github.com/nautechsystems/nautilus_trader/tree/develop/examples/backtest/architect_ax_mean_reversion.py).
## What the run produces
Replaying TrueFX EUR/USD December 2025 through `BBMeanReversion(20, 2sd, RSI 14)`
prints 44,591 1-minute mid bars and closes 1,089 positions across 2,178 fills.
Cumulative realized pnl ends at **-1,287 USD**: the strategy bleeds steadily
through the month with no clear regime-driven recovery. Mean reversion
without a regime filter pays the spread on every cycle, and EUR/USD ran a
pronounced uptrend through the second half of December which the strategy
fought repeatedly.

**Figure 1.** *EUR/USD 1-minute mid bars across December 2025 with the BB
middle and +/-2sd envelope. Long flat patches are weekend gaps in the TrueFX
feed.*

**Figure 2.** *Twelve-hour zoom around the dataset midpoint. Top: mid with
BB envelope, long entries (triangles up), short entries (triangles down),
and closing fills (crosses). Bottom: RSI(14) with the 30 buy / 70 sell
thresholds.*

**Figure 3.** *Per-bar BB z-score against RSI for the whole month. Shaded
regions mark the entry-eligible quadrants: lower-left (long) and upper-right
(short). The diagonal lobe is the natural co-movement of band-relative price
and RSI.*

**Figure 4.** *Cumulative realized USD pnl across closed positions. The
curve declines roughly linearly, dominated by spread and small adverse
moves on each cycle.*
### Regenerate the panels
A self-contained renderer re-runs the backtest, computes BB and RSI on the
captured bars, and writes PNG panels using the shared `nautilus_dark`
tearsheet theme.
After building NautilusTrader from source, run these commands from the repository root:
```bash
make sync
TRUEFX_CSV=test_data/local/truefx/EURUSD-2025-12.csv \
uv run --project python --no-sync \
python docs/tutorials/assets/fx_mean_reversion_ax/render_panels.py
```
Set `TRUEFX_CSV` to wherever you saved the EUR/USD archive.
## Next steps
- **Add a regime filter**. The drawdown is concentrated in trending sessions.
Suppress entries when realized range or a slower trend filter says the
market is directional.
- **Tune thresholds**. A wider band (`bb_std=2.5`) or stricter RSI cutoffs
(`25` / `75`) cut entries but raise the bar for confirmation.
- **Add stops**. Hard stop-loss orders cap downside per cycle and prevent
carrying a losing position to the BB middle reversion.
- **Go live on the AX sandbox**. Connect to the AX sandbox for paper
trading once the backtest behaves. See the
[AX Exchange integration guide](../integrations/architect_ax.md) for
setup.
## Running live
The same `BBMeanReversion` strategy runs live against AX Exchange. The
launch script swaps the `BacktestEngine` for a `LiveNode` with the AX
data and execution clients configured. See the live example:
[`ax_mean_reversion.py`](https://github.com/nautechsystems/nautilus_trader/tree/develop/examples/live/architect_ax/ax_mean_reversion.py).
The script targets the AX sandbox (`AxEnvironment.SANDBOX` on both client
configs) and places live sandbox orders. It also sets
`LiveRiskEngineConfig(bypass=True)`, which skips pre-trade risk checks and
order rate limits.
For connection setup and API key configuration, see the
[AX Exchange integration guide](../integrations/architect_ax.md).
## Further reading
- [`BBMeanReversion` strategy source](https://github.com/nautechsystems/nautilus_trader/blob/develop/examples/live/architect_ax/strategies.py)
- [Gold perpetual book imbalance tutorial](gold_book_imbalance_ax.md)
- [Architect Exchange documentation](https://docs.architect.exchange/)Shown in full with attribution under the source's licence. Licence: LGPL-3.0
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.