Closed rsmb7z closed 1 year ago
Hi @cjdsellers
Just FYI, test_internal_bars
reproduces above error. Maybe adding similar test in tests to make sure Strategy receives the expected data as in input.
Also I found that in version 1.159 (#868) Bars were returning -1 for OHLC whereas now the values of previous bar.
from typing import Optional
from nautilus_trader.backtest.data.providers import TestDataProvider
from nautilus_trader.backtest.data.providers import TestInstrumentProvider
from nautilus_trader.backtest.data.wranglers import BarDataWrangler
from nautilus_trader.backtest.data.wranglers import QuoteTickDataWrangler
from nautilus_trader.backtest.engine import BacktestEngine
from nautilus_trader.backtest.engine import BacktestEngineConfig
from nautilus_trader.config.common import RiskEngineConfig
from nautilus_trader.examples.strategies.subscribe import SubscribeStrategyConfig
from nautilus_trader.model.currencies import USD
from nautilus_trader.model.data.bar import Bar
from nautilus_trader.model.data.bar import BarType
from nautilus_trader.model.data.tick import QuoteTick
from nautilus_trader.model.data.tick import TradeTick
from nautilus_trader.model.enums import AccountType
from nautilus_trader.model.enums import OMSType
from nautilus_trader.model.identifiers import InstrumentId
from nautilus_trader.model.identifiers import Venue
from nautilus_trader.model.objects import Money
from nautilus_trader.model.orderbook.book import OrderBook
from nautilus_trader.model.orderbook.data import OrderBookData
from nautilus_trader.trading.strategy import Strategy
RESULTS = {}
class TestDataEvents:
class SubscribeStrategy(Strategy):
def __init__(self, config: SubscribeStrategyConfig):
super().__init__(config)
self.instrument_id = InstrumentId.from_str(self.config.instrument_id)
self.book: Optional[OrderBook] = None
def on_start(self):
"""Actions to be performed on strategy start."""
self.instrument = self.cache.instrument(self.instrument_id)
if self.instrument is None:
self.log.error(f"Could not find instrument for {self.instrument_id}")
self.stop()
return
if self.config.book_type:
self.book = OrderBook.create(
instrument=self.instrument,
book_type=self.config.book_type,
)
if self.config.snapshots:
self.subscribe_order_book_snapshots(
instrument_id=self.instrument_id,
book_type=self.config.book_type,
)
else:
self.subscribe_order_book_deltas(
instrument_id=self.instrument_id,
book_type=self.config.book_type,
)
if self.config.trade_ticks:
self.subscribe_trade_ticks(instrument_id=self.instrument_id)
RESULTS[f"{self.instrument_id}-TradeTick"] = []
if self.config.quote_ticks:
self.subscribe_quote_ticks(instrument_id=self.instrument_id)
RESULTS[f"{self.instrument_id}-QuoteTick"] = []
if self.config.bars:
for kind in ("MID-INTERNAL", "BID-EXTERNAL", "ASK-EXTERNAL"):
bar_type: BarType = BarType.from_str(f"{self.instrument_id}-1-MINUTE-{kind}")
self.subscribe_bars(bar_type)
RESULTS[str(bar_type)] = []
def on_order_book_delta(self, data: OrderBookData):
if not self.book:
self.log.error("No book being maintained.")
return
self.book.apply(data)
self.log.info(str(self.book))
def on_order_book(self, order_book: OrderBook):
self.book = order_book
self.log.info(str(self.book))
def on_trade_tick(self, tick: TradeTick):
self.log.info(str(tick))
RESULTS[f"{tick.instrument_id}-TradeTick"].append(tick)
def on_quote_tick(self, tick: QuoteTick):
self.log.info(str(tick))
RESULTS[f"{tick.instrument_id}-QuoteTick"].append(tick)
def on_bar(self, bar: Bar):
self.log.info(str(bar))
RESULTS[str(bar.bar_type)].append(bar)
def on_stop(self):
pass
def on_reset(self):
pass
def setup(self):
self.input_bars_count = 5000
# Configure backtest engine
config = BacktestEngineConfig(
trader_id="BACKTESTER-001",
log_level="INFO",
risk_engine=RiskEngineConfig(
bypass=True, # Example of bypassing pre-trade risk checks for backtests
),
)
# Build backtest engine
engine = BacktestEngine(config=config)
# Optional plug in module to simulate rollover interest,
# the data is coming from packaged test data.
provider = TestDataProvider()
# Add a trading venue (multiple venues possible)
SIM = Venue("SIM")
engine.add_venue(
venue=SIM,
oms_type=OMSType.HEDGING, # Venue will generate position IDs
account_type=AccountType.MARGIN,
base_currency=USD, # Standard single-currency account
starting_balances=[Money(1_000_000, USD)], # Single-currency or multi-currency accounts
)
# Add instruments
GBPUSD_SIM = TestInstrumentProvider.default_fx_ccy("GBP/USD", SIM)
engine.add_instrument(GBPUSD_SIM)
# Add data
wrangler = QuoteTickDataWrangler(instrument=GBPUSD_SIM)
ticks = wrangler.process_bar_data(
bid_data=provider.read_csv_bars("fxcm-gbpusd-m1-bid-2012.csv")[: self.input_bars_count],
ask_data=provider.read_csv_bars("fxcm-gbpusd-m1-ask-2012.csv")[: self.input_bars_count],
)
engine.add_data(ticks)
# Setup wranglers
bid_wrangler = BarDataWrangler(
bar_type=BarType.from_str("GBP/USD.SIM-1-MINUTE-BID-EXTERNAL"),
instrument=GBPUSD_SIM,
)
ask_wrangler = BarDataWrangler(
bar_type=BarType.from_str("GBP/USD.SIM-1-MINUTE-ASK-EXTERNAL"),
instrument=GBPUSD_SIM,
)
# Add data
bid_bars = bid_wrangler.process(
data=provider.read_csv_bars("fxcm-gbpusd-m1-bid-2012.csv")[: self.input_bars_count],
)
ask_bars = ask_wrangler.process(
data=provider.read_csv_bars("fxcm-gbpusd-m1-ask-2012.csv")[: self.input_bars_count],
)
engine.add_data(bid_bars)
engine.add_data(ask_bars)
# Configure your strategy
config = SubscribeStrategyConfig(
instrument_id=str(GBPUSD_SIM.id),
quote_ticks=True,
bars=True,
)
# Instantiate and add your strategy
strategy = self.SubscribeStrategy(config=config)
engine.add_strategy(strategy=strategy)
# Run the engine (from start to end of data)
engine.run()
# For repeated backtest runs make sure to reset the engine
engine.reset()
def test_qoute_ticks(self):
assert len(RESULTS["GBP/USD.SIM-QuoteTick"]) == self.input_bars_count * 4
def test_external_bars(self):
assert len(RESULTS["GBP/USD.SIM-1-MINUTE-BID-EXTERNAL"]) == self.input_bars_count
assert len(RESULTS["GBP/USD.SIM-1-MINUTE-ASK-EXTERNAL"]) == self.input_bars_count
def test_internal_bars(self):
assert len(RESULTS["GBP/USD.SIM-1-MINUTE-MID-INTERNAL"]) == self.input_bars_count
So an initial step to providing the functionality you need is available on latest develop
.
Simply setup your backtest to include the following config:
config = BacktestEngineConfig(
trader_id="BACKTESTER-001",
data_engine=DataEngineConfig(build_time_bars_with_no_updates=False),
)
Bars will then only be built and emitted if the relevant TimeBarAggregator
s internal builder has received a market update since the last bar build interval.
What we eventually want to get to is being able to provide a time schedule to TimeBarAggregator
so that bars are only built and emitted during the instruments market hours, this at least addresses your immediate issue here.
I think I finally understand the use case for updating indications with regards to a bars is_revision
property.
True
: Either ignore the bar, or calculate and update the current indicator value? (please confirm)False
: Calculate and append a new value
True
: Either ignore the bar, or calculate and update the current indicator value? (please confirm)
If the Bar with is_revision
passes this stage, yes it shall update the current Indicator values.
https://github.com/nautechsystems/nautilus_trader/blob/master/nautilus_trader/data/engine.pyx#L1196
False
: Calculate and append a new value
Correct - default current behavior.
Bug Report
Expected Behavior
Internal aggregator shall not trigger new Bars when there is no new data. Maybe need to make it Tick timestamp aware.
Actual Behavior
When there are no new ticks available during non-trading hours, internal aggregator will keep triggering new Bars with new timestamp while old OHLCV.
Steps to Reproduce the Problem
Specifications
nautilus_trader
version: 1.164.0