Skip to content

Commit

Permalink
Add cascaded indicators example (#2398)
Browse files Browse the repository at this point in the history
  • Loading branch information
stefansimik authored Mar 3, 2025
1 parent 6530125 commit dcbea70
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 0 deletions.
16 changes: 16 additions & 0 deletions examples/backtest/example_08_cascaded_indicator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Example: Using cascaded technical indicators

This example demonstrates how to use cascaded technical indicators in a **NautilusTrader** strategy.

The example shows how to set up and use two Exponential Moving Average (EMA) indicators in a cascaded manner,
where the second indicator (EMA-20) is calculated using values from the first indicator (EMA-10),
demonstrating proper initialization, updating, and accessing indicator values in a cascaded setup.

**What this example demonstrates:**

- Creating and configuring multiple technical indicators (EMAs)
- Setting up a cascaded indicator relationship
- Registering the primary indicator to receive bar data
- Manually updating the cascaded indicator
- Storing and accessing historical values for both indicators
- Proper handling of indicator initialization in a cascaded setup
100 changes: 100 additions & 0 deletions examples/backtest/example_08_cascaded_indicator/run_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/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

from strategy import DemoStrategy
from strategy import DemoStrategyConfig

from examples.utils.data_provider import prepare_demo_data_eurusd_futures_1min
from nautilus_trader.backtest.engine import BacktestEngine
from nautilus_trader.config import BacktestEngineConfig
from nautilus_trader.config import LoggingConfig
from nautilus_trader.model import Bar
from nautilus_trader.model import TraderId
from nautilus_trader.model.currencies import USD
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.instruments.base import Instrument
from nautilus_trader.model.objects import Money


if __name__ == "__main__":

# ----------------------------------------------------------------------------------
# 1. Configure and create backtest engine
# ----------------------------------------------------------------------------------

engine_config = BacktestEngineConfig(
trader_id=TraderId("BACKTEST-CASCADED-IND-001"), # Unique identifier for this backtest
logging=LoggingConfig(
log_level="INFO", # Set to INFO to see indicator values
),
)
engine = BacktestEngine(config=engine_config)

# ----------------------------------------------------------------------------------
# 2. Prepare market data
# ----------------------------------------------------------------------------------

prepared_data: dict = prepare_demo_data_eurusd_futures_1min()
venue_name: str = prepared_data["venue_name"]
eurusd_instrument: Instrument = prepared_data["instrument"]
eurusd_1min_bartype = prepared_data["bar_type"]
eurusd_1min_bars: list[Bar] = prepared_data["bars_list"]

# ----------------------------------------------------------------------------------
# 3. Configure trading environment
# ----------------------------------------------------------------------------------

# Set up the trading venue with a margin account
engine.add_venue(
venue=Venue(venue_name),
oms_type=OmsType.NETTING, # Use a netting order management system
account_type=AccountType.MARGIN, # Use a margin trading account
starting_balances=[Money(1_000_000, USD)], # Set initial capital
base_currency=USD, # Account currency
default_leverage=Decimal(1), # No leverage (1:1)
)

# Register the trading instrument
engine.add_instrument(eurusd_instrument)

# Load historical market data
engine.add_data(eurusd_1min_bars)

# ----------------------------------------------------------------------------------
# 4. Configure and run strategy
# ----------------------------------------------------------------------------------

# Create strategy configuration
strategy_config = DemoStrategyConfig(
instrument=eurusd_instrument,
primary_bar_type=eurusd_1min_bartype,
primary_ema_period=10, # Period for primary EMA indicator
secondary_ema_period=20, # Period for secondary cascaded EMA indicator
)

# Create and register the strategy
strategy = DemoStrategy(config=strategy_config)
engine.add_strategy(strategy)

# Execute the backtest
engine.run()

# Clean up resources
engine.dispose()
116 changes: 116 additions & 0 deletions examples/backtest/example_08_cascaded_indicator/strategy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from collections import deque

from nautilus_trader.common.enums import LogColor
from nautilus_trader.config import StrategyConfig
from nautilus_trader.core.datetime import unix_nanos_to_dt
from nautilus_trader.indicators.average.ma_factory import MovingAverageFactory
from nautilus_trader.indicators.average.ma_factory import MovingAverageType
from nautilus_trader.model.data import Bar
from nautilus_trader.model.data import BarType
from nautilus_trader.model.instruments import Instrument
from nautilus_trader.trading.strategy import Strategy


class DemoStrategyConfig(StrategyConfig, frozen=True):
"""
Configuration for the demo strategy.
"""

instrument: Instrument
primary_bar_type: BarType
primary_ema_period: int = 10 # Period for primary EMA indicator
secondary_ema_period: int = 20 # Period for secondary cascaded EMA indicator


class DemoStrategy(Strategy):
"""
A simple strategy demonstrating the use of cascaded indicators.
"""

def __init__(self, config: DemoStrategyConfig):
super().__init__(config)

# Count processed bars
self.bars_processed = 0

# Store bar type from config
self.bar_type = config.primary_bar_type

# Primary indicator: EMA calculated on 1-min bars
self.primary_ema = MovingAverageFactory.create(
config.primary_ema_period, # Period for primary EMA indicator
MovingAverageType.EXPONENTIAL, # Type of moving average
)
self.primary_ema_history: deque[float] = deque() # Store historical values here

# Cascaded indicator: EMA calculated on primary EMA values
self.secondary_ema = MovingAverageFactory.create(
config.secondary_ema_period, # Period for secondary cascaded EMA indicator
MovingAverageType.EXPONENTIAL, # Type of moving average
)
self.secondary_ema_history: deque[float] = deque() # Store historical values here

def on_start(self):
# Subscribe to bars
self.subscribe_bars(self.bar_type)

# Register primary indicator to receive bar data
self.register_indicator_for_bars(self.bar_type, self.primary_ema)

self.log.info("Strategy started.")

def on_bar(self, bar: Bar):
# Count processed bars
self.bars_processed += 1
self.log.info(
f"Bar #{self.bars_processed} | "
f"Bar: {bar} | "
f"Time={unix_nanos_to_dt(bar.ts_event)}",
color=LogColor.YELLOW,
)

# Store latest primary EMA value
# Since primary EMA is registered, it's automatically updated with new bars
primary_ema_value = self.primary_ema.value
self.primary_ema_history.appendleft(primary_ema_value)

# Update cascaded EMA with the latest primary EMA value
# We need to wait until primary EMA is initialized
if self.primary_ema.initialized:
# Manually feed primary EMA value into secondary EMA
self.secondary_ema.update_raw(self.primary_ema.value)
# Store latest secondary EMA value
self.secondary_ema_history.appendleft(self.secondary_ema.value)

# Wait until both indicators are initialized
# - Primary EMA needs first `primary_ema_period` bars to initialize
# - Secondary EMA needs `secondary_ema_period` values from primary EMA to initialize
# So in total we need at least `primary_ema_period + secondary_ema_period` bars before both indicators are ready
if not self.primary_ema.initialized or not self.secondary_ema.initialized:
self.log.info("Waiting for indicators to initialize...", color=LogColor.RED)
return

# Access and log indicator values
primary_ema_latest = self.primary_ema.value
secondary_ema_latest = self.secondary_ema.value

# Log latest indicator values
self.log.info(
f"Latest values. | "
f"Primary EMA({self.config.primary_ema_period}) = {primary_ema_latest:.7f}, "
f"Secondary EMA({self.config.secondary_ema_period}) = {secondary_ema_latest:.7f}",
color=LogColor.BLUE,
)

# Check history and log previous values if available
if len(self.primary_ema_history) > 1 and len(self.secondary_ema_history) > 1:
primary_ema_prev = self.primary_ema_history[1]
secondary_ema_prev = self.secondary_ema_history[1]
self.log.info(
f"Previous values | "
f"Primary EMA({self.config.primary_ema_period}) = {primary_ema_prev:.7f}, "
f"Secondary EMA({self.config.secondary_ema_period}) = {secondary_ema_prev:.7f}",
)

def on_stop(self):
self.log.info(f"Strategy stopped. Processed {self.bars_processed} bars.")

0 comments on commit dcbea70

Please sign in to comment.