Skip to content
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

Add cascaded indicators example #2398

Merged
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
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.")
Loading