Hello! I'm new to quantconnect. I've had some limited experience coding for MetaTrader4 and coding in Python for the OANDA Web API. After taking a look at Greg Bland's QuantConnect Guide and the corresponding code at GitHub, I thought I'd try working out a similar example for a Bollinger Bands strategy with FX data. I understand that this may be a fairly limited strategy. I was hoping mainly that it would help towards getting familiar with the API for quantconnect.

I've tried a backtest with the following Python code as my main.py

# region imports
from datetime import timedelta
from enum import StrEnum
import pandas as pd
from typing import TYPE_CHECKING, Any

from AlgorithmImports import (
    QCAlgorithm, Resolution, Slice, Market, TimeSpan,
    MovingAverageType, TradeBarConsolidator, SubscriptionManager,
    TradeBar, BollingerBands, BaseDataConsolidator
)
import numpy as np
# endregion

# after
# https://algotrading101.com/learn/quantconnect-guide/
# https://github.com/GregBland/QuantConnect_article/blob/main/leanHogsAlgo.py

SYMBOL: str = "NZDUSD"

TEST_ORDER_SIZE: int = 100

BB_PERIOD: int = 15
BB_K = 1.5
BB_MA = MovingAverageType.WILDERS
BB_RESOL = Resolution.MINUTE


class Const(StrEnum):
    SYMBOL = "symbol"
    WARMUP = "warmup"
    INDICATOR = "indicator"


class LeanTest00(QCAlgorithm):
    initialized: bool = False

    if TYPE_CHECKING:
        params: dict[str, Any]

    def __init__(self) -> None:
        super().__init__()
        self.params = dict()

    def initialize(self):
        global SYMBOL, WARMUP_DELTA

        self.set_start_date(2024, 6, 1)  # Set Start Date
        self.set_end_date(2024, 8, 31)  # Set End Date
        self.set_cash(2000)  # Set Strategy Cash

        fx = self.add_forex(SYMBOL, resolution=Resolution.MINUTE,
                            market=Market.OANDA, leverage=50)
        symbol = fx.symbol
        self.log("-- symbol %r" % symbol)

        consol_15m = self.create_consolidator(np.timedelta64(
            15, "m").astype("O"), TradeBarConsolidator)  # tick_type ??
        consol_15m.data_consolidated += self.on_15m  # ??
        mng: SubscriptionManager = self.subscription_manager
        mng.add_consolidator(symbol, consol_15m)
        self.log("-- Added consolidator %r" % consol_15m)

        bb = self.bb(symbol, BB_PERIOD, BB_K, BB_MA, BB_RESOL)
        self.params[Const.INDICATOR] = bb
        self.params[Const.SYMBOL] = symbol

    def on_data(self, data: Slice):
        symbol = self.params[Const.SYMBOL]
        bar: TradeBar = data.bars.get(symbol)
        bb: BollingerBands = self.params[Const.INDICATOR]
        if bar:
            bb.update(bar.EndTime, np.mean(
                [bar.high, bar.low, bar.close, bar.close]))
        if bb.is_ready:
            self.plot("BollingerBands", "bb", bb.current.value)
            self.plot("BollingerBands", "standard_deviation",
                      bb.standard_deviation.current.value)
            self.plot("BollingerBands", "middle_band",
                      bb.middle_band.current.value)
            self.plot("BollingerBands", "upper_band",
                      bb.upper_band.current.value)
            self.plot("BollingerBands", "lower_band",
                      bb.lower_band.current.value)
            self.plot("BollingerBands", "band_width",
                      bb.band_width.current.value)
            self.plot("BollingerBands", "percent_b",
                      bb.percent_b.current.value)
            self.plot("BollingerBands", "price", bb.price.current.value)

    def on_15m(self, sender: BaseDataConsolidator, bar: TradeBar):
        # event handler for M15 consolidator
        if self.is_warming_up:
            self.log("Consolidator [warmup] %r " % (bar.time,))
            return

        self.log("Consolidator %r" % (bar.time,))

        bb: BollingerBands = self.params[Const.INDICATOR]
        if bb.is_ready:
            price = np.mean([bar.high, bar.low, bar.close, bar.close])
            if price < bb.lower_band.current.value:
                symbol = self.params[Const.SYMBOL]
                self.sell(symbol, TEST_ORDER_SIZE)
            elif price > bb.upper_band.current.value:
                symbol = self.params[Const.SYMBOL]
                self.liquidate(symbol)

 

I'm certain that there must be a number of issues in this implementation. In trying to debug this, I was running this under local tests using the quantconnect Python tooling, the `'lean' command and a local docker image from quantconnect.

With this code, I've been seeing the following under the backtest log for a local backtest:

--

2024-09-28T06:06:50.5156592Z DEBUG:: DataMonitor.OnNewDataRequest(): Data from /forex/oanda/minute/nzdusd/20240619_quote.zip could not be fetched, error: Could not find file '/Lean/Data/forex/oanda/minute/nzdusd/20240619_quote.zip'.
2024-09-28T06:06:50.5157188Z ERROR:: SubscriptionDataSourceReader.InvalidSource(): File not found: /Lean/Data/forex/oanda/minute/nzdusd/20240619_quote.zip
2024-09-28T06:06:50.5158510Z DEBUG:: DataMonitor.OnNewDataRequest(): Data from /forex/oanda/minute/nzdusd/20240620_quote.zip could not be fetched, error: Could not find file '/Lean/Data/forex/oanda/minute/nzdusd/20240620_quote.zip'.
--

 

It seems there's some missing data for the backtest. I wonder if anyone would have any idea as to how to fix this for the local backtest?

When I've run the same code under backtest with the quantconnect cloud, I'm seeing only one trade, a loss of net 9 cents USD. From what I'm seeing for the BolllingerBands indicator there, I'm certain that there may be some issues in how I've implemented the indicator.

If I could figure out how to get the local backtest running, I'm hoping I'd be able to debug the main code then.

Trying to diagnose the log messages about the backtest's missing data, could it be related to the start date and the end date that I'd set under initialize()?

Thanks for any help!