Skip to content

Add backtest clock and timers example #2327

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@
engine = BacktestEngine(config=engine_config)

# Step 2: Define exchange and add it to the engine
GLOBEX = Venue("GLBX")
XCME = Venue("XCME")
engine.add_venue(
venue=GLOBEX,
venue=XCME,
oms_type=OmsType.NETTING, # Order Management System type
account_type=AccountType.MARGIN, # Type of trading account
starting_balances=[Money(1_000_000, USD)], # Initial account balance
Expand All @@ -64,7 +64,7 @@
# ------------------------------------------------------------------------------------------

# Step 4a: Load bar data from CSV file -> into pandas DataFrame
csv_file_path = r"./6EH4.GLBX_1min_bars.csv"
csv_file_path = r"6EH4.XCME_1min_bars.csv"
df = pd.read_csv(csv_file_path, sep=";", decimal=".", header=0, index_col=False)

# Step 4b: Restructure DataFrame into required structure, that can be passed `BarDataWrangler`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import datetime as dt

from nautilus_trader.common.enums import LogColor
from nautilus_trader.model.data import Bar
from nautilus_trader.model.data import BarType
from nautilus_trader.trading.strategy import Strategy
Expand All @@ -39,7 +40,7 @@ def on_start(self):

def on_bar(self, bar: Bar):
self.bars_processed += 1
self.log.info(f"Processed bars: {self.bars_processed}")
self.log.info(f"Processed bars: {self.bars_processed}", color=LogColor.YELLOW)

def on_stop(self):
# Remember and log end time of strategy
Expand Down
11 changes: 11 additions & 0 deletions examples/backtest/example_002_use_clock_timer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
This is a simple example of how to use NautilusTrader's **Timer** feature in a strategy.

The strategy works by running action at regular time intervals while also handling market data events.
It shows how **timer events** and **market data** can work separately at the same time.

**What this strategy does:**

- Uses **NautilusTrader’s timer** to trigger events on schedule.
- Handles **market data** and **timer events** independently.

This helps you see how timers work alongside market data processing without interfering with each other. 🚀
114 changes: 114 additions & 0 deletions examples/backtest/example_002_use_clock_timer/run_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/env python3
# -------------------------------------------------------------------------------------------------
# Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
# https://nautechsystems.io
#
# Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
# You may not use this file except in compliance with the License.
# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# -------------------------------------------------------------------------------------------------

from decimal import Decimal

import pandas as pd
from strategy import SimpleTimerStrategy

from nautilus_trader import TEST_DATA_DIR
from nautilus_trader.backtest.engine import BacktestEngine
from nautilus_trader.config import BacktestEngineConfig
from nautilus_trader.config import LoggingConfig
from nautilus_trader.model import TraderId
from nautilus_trader.model.currencies import USD
from nautilus_trader.model.data import Bar
from nautilus_trader.model.data import BarType
from nautilus_trader.model.enums import AccountType
from nautilus_trader.model.enums import OmsType
from nautilus_trader.model.identifiers import Venue
from nautilus_trader.model.objects import Money
from nautilus_trader.persistence.wranglers import BarDataWrangler
from nautilus_trader.test_kit.providers import TestInstrumentProvider


if __name__ == "__main__":
# Step 1: Configure and create backtest engine
engine_config = BacktestEngineConfig(
trader_id=TraderId("BACKTEST_TRADER-001"),
logging=LoggingConfig(
log_level="DEBUG", # set DEBUG log level for console to see loaded bars in logs
),
)
engine = BacktestEngine(config=engine_config)

# Step 2: Define exchange and add it to the engine
XCME = Venue("XCME")
engine.add_venue(
venue=XCME,
oms_type=OmsType.NETTING, # Order Management System type
account_type=AccountType.MARGIN, # Type of trading account
starting_balances=[Money(1_000_000, USD)], # Initial account balance
base_currency=USD, # Base currency for account
default_leverage=Decimal(1), # No leverage used for account
)

# Step 3: Create instrument definition and add it to the engine
EURUSD_FUTURES_INSTRUMENT = TestInstrumentProvider.eurusd_future(2024, 3)
engine.add_instrument(EURUSD_FUTURES_INSTRUMENT)

# ==========================================================================================
# Loading bars from CSV
# ------------------------------------------------------------------------------------------

# Step 4a: Load bar data from CSV file -> into pandas DataFrame
csv_file_path = rf"{TEST_DATA_DIR}/xcme/6EH4.XCME_1min_bars_20240101_20240131.csv.gz"
df = pd.read_csv(
csv_file_path,
header=0,
index_col=False,
)

# Step 4b: Restructure DataFrame into required structure, that can be passed `BarDataWrangler`
# - 5 required columns: 'open', 'high', 'low', 'close', 'volume' (volume is optional)
# - column 'timestamp': should be in index of the DataFrame
df = (
# Change order of columns
df.reindex(columns=["timestamp_utc", "open", "high", "low", "close", "volume"])
# Convert string timestamps into datetime
.assign(
timestamp_utc=lambda dft: pd.to_datetime(
dft["timestamp_utc"],
format="%Y-%m-%d %H:%M:%S",
),
)
# Rename column to required name
.rename(columns={"timestamp_utc": "timestamp"}).set_index("timestamp")
)

# Step 4c: Define type of loaded bars
EURUSD_FUTURES_1MIN_BARTYPE = BarType.from_str(
f"{EURUSD_FUTURES_INSTRUMENT.id}-1-MINUTE-LAST-EXTERNAL",
)

# Step 4d: `BarDataWrangler` converts each row into objects of type `Bar`
wrangler = BarDataWrangler(EURUSD_FUTURES_1MIN_BARTYPE, EURUSD_FUTURES_INSTRUMENT)
eurusd_1min_bars_list: list[Bar] = wrangler.process(df)

# Step 4e: Add loaded data to the engine
engine.add_data(eurusd_1min_bars_list)

# ------------------------------------------------------------------------------------------

# Step 5: Create strategy and add it to the engine
strategy = SimpleTimerStrategy(primary_bar_type=EURUSD_FUTURES_1MIN_BARTYPE)
engine.add_strategy(strategy)

# Step 6: Run engine = Run backtest
engine.run()

# Step 7: Release system resources
engine.dispose()
83 changes: 83 additions & 0 deletions examples/backtest/example_002_use_clock_timer/strategy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# -------------------------------------------------------------------------------------------------
# Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
# https://nautechsystems.io
#
# Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
# You may not use this file except in compliance with the License.
# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# -------------------------------------------------------------------------------------------------

import datetime as dt

import pandas as pd

from nautilus_trader.common.enums import LogColor
from nautilus_trader.common.events import TimeEvent
from nautilus_trader.core.datetime import unix_nanos_to_dt
from nautilus_trader.model.data import Bar
from nautilus_trader.model.data import BarType
from nautilus_trader.trading.strategy import Strategy


class SimpleTimerStrategy(Strategy):
TIMER_NAME = "every_3_minutes"
TIMER_INTERVAL = pd.Timedelta(minutes=3)

def __init__(self, primary_bar_type: BarType):
super().__init__()
self.primary_bar_type = primary_bar_type
self.bars_processed = 0
self.start_time = None
self.end_time = None

def on_start(self):
# Remember and log start time of strategy
self.start_time = dt.datetime.now()
self.log.info(f"Strategy started at: {self.start_time}")

# Subscribe to bars
self.subscribe_bars(self.primary_bar_type)

# ==================================================================
# POINT OF FOCUS: Timer invokes action at regular time intervals
# ------------------------------------------------------------------

# Setup recurring timer
self.clock.set_timer(
name=self.TIMER_NAME, # Custom timer name
interval=self.TIMER_INTERVAL, # Timer interval
callback=self.on_timer, # Custom callback function invoked on timer
)

def on_bar(self, bar: Bar):
# You can implement any action here (like submit order), but for simplicity, we are just counting bars
self.bars_processed += 1
self.log.info(f"Processed bars: {self.bars_processed}")

# ==================================================================
# POINT OF FOCUS: Custom callback function invoked by Timer
# ------------------------------------------------------------------

def on_timer(self, event: TimeEvent):
if event.name == self.TIMER_NAME:
event_time_dt = unix_nanos_to_dt(event.ts_event)
# You can implement any action here (like submit order), which should be executed in regular interval,
# but for simplicity, we just create a log.
self.log.info(
f"TimeEvent received. | Name: {event.name} | Time: {event_time_dt}",
color=LogColor.YELLOW,
)

def on_stop(self):
# Remember and log end time of strategy
self.end_time = dt.datetime.now()
self.log.info(f"Strategy finished at: {self.end_time}")

# Log count of processed bars
self.log.info(f"Total bars processed: {self.bars_processed}")
2 changes: 1 addition & 1 deletion nautilus_trader/test_kit/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,7 @@ def es_future(
def eurusd_future(
expiry_year: int,
expiry_month: int,
venue_name: str = "GLBX",
venue_name: str = "XCME",
) -> FuturesContract:
activation_date = first_friday_two_years_six_months_ago(expiry_year, expiry_month)
expiration_date = third_friday_of_month(expiry_year, expiry_month)
Expand Down
Binary file not shown.
23 changes: 23 additions & 0 deletions tests/test_data/xcme/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# File: `6EH4.XCME_1min_bars_20240101_20240131.csv.gz`

- Instrument: 6E
- Expiration: H4 (March 2024)
- Exchange: XCME (MIC code)
- Period 2024-01-01 --> 2024-01-31 (UTC timestamp, no contract rollover occurs in this period)
- Bar type: 1-minute bars


# Zipped format

We used zipped data, because they are 9x smaller than original CSV file and can be DIRECTLY read by [pandas](https://pandas.pydata.org/)
using code like this:

```python
import pandas as pd

df = pd.read_csv(
"6EH4.XCME_1min_bars_20240101_20240131.csv.gz", # update path as needed
header=0,
index_col=False,
)
```
Loading