diff --git a/examples/backtest/example_08_cascaded_indicator/README.md b/examples/backtest/example_08_cascaded_indicator/README.md new file mode 100644 index 00000000000..726e2221d2b --- /dev/null +++ b/examples/backtest/example_08_cascaded_indicator/README.md @@ -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 diff --git a/examples/backtest/example_08_cascaded_indicator/run_example.py b/examples/backtest/example_08_cascaded_indicator/run_example.py new file mode 100644 index 00000000000..705ca6c2595 --- /dev/null +++ b/examples/backtest/example_08_cascaded_indicator/run_example.py @@ -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() diff --git a/examples/backtest/example_08_cascaded_indicator/strategy.py b/examples/backtest/example_08_cascaded_indicator/strategy.py new file mode 100644 index 00000000000..e0c086364ed --- /dev/null +++ b/examples/backtest/example_08_cascaded_indicator/strategy.py @@ -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.")