nautechsystems / nautilus_trader

A high-performance algorithmic trading platform and event-driven backtester
https://nautilustrader.io
GNU Lesser General Public License v3.0
1.98k stars 453 forks source link

Error when self.subscribe_order_book_snapshots(instrument_id=self.instrument_id,depth=20) and def on_order_book(self, order_book: OrderBook) -> None: #1455

Closed x-zho14 closed 8 months ago

x-zho14 commented 8 months ago
#!/usr/bin/env python3
# -------------------------------------------------------------------------------------------------
#  Copyright (C) 2015-2024 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 nautilus_trader.adapters.binance.common.enums import BinanceAccountType
from nautilus_trader.adapters.binance.config import BinanceDataClientConfig
from nautilus_trader.adapters.binance.config import BinanceExecClientConfig
from nautilus_trader.adapters.binance.factories import BinanceLiveDataClientFactory
from nautilus_trader.adapters.binance.factories import BinanceLiveExecClientFactory
from nautilus_trader.config import InstrumentProviderConfig
from nautilus_trader.config import LiveExecEngineConfig
from nautilus_trader.config import LoggingConfig
from nautilus_trader.config import TradingNodeConfig
from nautilus_trader.config.common import CacheConfig
from nautilus_trader.config.common import DatabaseConfig
from nautilus_trader.live.node import TradingNode
from nautilus_trader.model.data import BarType
from nautilus_trader.model.identifiers import InstrumentId
from nautilus_trader.model.identifiers import TraderId

from nautilus_trader.config import StrategyConfig
from nautilus_trader.core.data import Data
from nautilus_trader.core.message import Event
from nautilus_trader.model.data import Bar
from nautilus_trader.model.data import QuoteTick
from nautilus_trader.model.data import TradeTick
from nautilus_trader.model.instruments import Instrument
from nautilus_trader.trading.strategy import Strategy

from nautilus_trader.model.book import OrderBook
from nautilus_trader.model.data import OrderBookDeltas
from nautilus_trader.model.enums import BookType
from nautilus_trader.model.enums import OrderSide
from nautilus_trader.model.enums import PositionSide
from nautilus_trader.model.events import PositionChanged
from nautilus_trader.model.events import PositionClosed
from nautilus_trader.model.events import PositionOpened
from nautilus_trader.model.objects import Price

class MarketMaker(Strategy):
    """
    Provides a market making strategy for testing.

    Parameters
    ----------
    instrument_id : InstrumentId
        The instrument ID for the strategy.
    trade_size : Decimal
        The position size per trade.
    max_size : Decimal
        The maximum inventory size allowed.

    """

    def __init__(
        self,
        instrument_id: InstrumentId,
        trade_size: Decimal,
        max_size: Decimal,
    ) -> None:
        super().__init__()
        # Configuration
        self.instrument_id = instrument_id
        self.trade_size = trade_size
        self.max_size = max_size
        self.instrument: Instrument | None = None  # Initialized in on_start
        self._book: OrderBook | None = None
        self._mid: Decimal | None = 0
        self._adj = Decimal(0)

    def on_start(self) -> None:
        self.instrument = self.cache.instrument(self.instrument_id)
        self.log.error(f"Could not find instrument for {self.instrument_id}")
        if self.instrument is None:
            self.log.error(f"Could not find instrument for {self.instrument_id}")
            self.stop()
            return

        # Create orderbook
        self._book = OrderBook(
            instrument_id=self.instrument.id,
            book_type=BookType.L2_MBP,
        )

        self.subscribe_order_book_snapshots(instrument_id=self.instrument_id,depth=20)

        # Subscribe to live data
        # self.subscribe_order_book_deltas(self.instrument_id)

        self.subscribe_quote_ticks(self.instrument_id)

    def on_order_book(self, order_book: OrderBook) -> None:
        pass
        # print(order_book)

    def on_order_book_deltas(self, deltas: OrderBookDeltas) -> None:
        if not self._book:
            self.log.error("No book being maintained.")
            return
        # for d in deltas.deltas:
            # print((d.ts_init-d.ts_event)/1e6)

    def on_quote_tick(self, tick: QuoteTick) -> None:
        print(tick.ask_price, tick.bid_price, tick.ts_event, tick.ts_init, (tick.ts_event-tick.ts_init)/1e6)
        # bid_price = self._book.best_bid_price()
        # ask_price = self._book.best_ask_price()

    def on_event(self, event: Event) -> None:
        if isinstance(event, PositionOpened | PositionChanged):
            signed_qty = event.quantity.as_decimal()
            if event.side == PositionSide.SHORT:
                signed_qty = -signed_qty
            self._adj = (signed_qty / self.max_size) * Decimal(0.01)
        elif isinstance(event, PositionClosed):
            self._adj = Decimal(0)

    def buy(self, price: Decimal) -> None:
        """
        Users simple buy method (example).
        """
        if not self.instrument:
            self.log.error("No instrument loaded.")
            return

        order = self.order_factory.limit(
            instrument_id=self.instrument_id,
            order_side=OrderSide.BUY,
            price=Price(price, precision=self.instrument.price_precision),
            quantity=self.instrument.make_qty(self.trade_size),
        )

        self.submit_order(order)

    def sell(self, price: Decimal) -> None:
        """
        Users simple sell method (example).
        """
        if not self.instrument:
            self.log.error("No instrument loaded.")
            return

        order = self.order_factory.limit(
            instrument_id=self.instrument_id,
            order_side=OrderSide.SELL,
            price=Price(price, precision=self.instrument.price_precision),
            quantity=self.instrument.make_qty(self.trade_size),
        )

        self.submit_order(order)

    def on_stop(self) -> None:
        """
        Actions to be performed when the strategy is stopped.
        """
        self.cancel_all_orders(self.instrument_id)
        self.close_all_positions(self.instrument_id)

# Configure the trading node
config_node = TradingNodeConfig(
    trader_id=TraderId("TESTER-001"),
    logging=LoggingConfig(log_level="INFO"),
    exec_engine=LiveExecEngineConfig(
        reconciliation=True,
        reconciliation_lookback_mins=1440,
    ),
    cache=CacheConfig(
        database=DatabaseConfig(),  # default redis
        buffer_interval_ms=100,
    ),
    data_clients={
        "BINANCE": BinanceDataClientConfig(
            api_key="",
            api_secret="",
            account_type=BinanceAccountType.USDT_FUTURE,
            instrument_provider=InstrumentProviderConfig(load_all=True),
        ),
    },
    exec_clients={
        "BINANCE": BinanceExecClientConfig(
            api_key="",
            api_secret="",
            account_type=BinanceAccountType.USDT_FUTURE,
            instrument_provider=InstrumentProviderConfig(load_all=True),
        ),
    }
)

# Instantiate the node with a configuration
node = TradingNode(config=config_node)

strategy = MarketMaker(instrument_id=InstrumentId.from_str("TRBUSDT-PERP.BINANCE"), trade_size=Decimal("1"), max_size=Decimal("1"))
# Add your strategies and modules
node.trader.add_strategy(strategy)

# Register your client factories with the node (can take user defined factories)
node.add_data_client_factory("BINANCE", BinanceLiveDataClientFactory)
node.add_exec_client_factory("BINANCE", BinanceLiveExecClientFactory)
node.build()

# Stop and dispose of the node with SIGINT/CTRL+C
if __name__ == "__main__":
    try:
        node.run()
    finally:
        node.dispose()

It has such error:

Exception in callback <bound method LiveClock._raise_time_event of <nautilus_trader.common.clock.LiveClock object at 0x7f0457e4f600>> handle: Traceback (most recent call last): File "uvloop/cbhandles.pyx", line 251, in uvloop.loop.TimerHandle._run File "nautilus_trader/common/clock.pyx", line 805, in nautilus_trader.common.clock.LiveClock._raise_time_event File "nautilus_trader/common/clock.pyx", line 817, in nautilus_trader.common.clock.LiveClock._raise_time_event File "nautilus_trader/common/clock.pyx", line 828, in nautilus_trader.common.clock.LiveClock._handle_time_event File "nautilus_trader/data/engine.pyx", line 1557, in nautilus_trader.data.engine.DataEngine._snapshot_order_book File "nautilus_trader/data/engine.pyx", line 1559, in nautilus_trader.data.engine.DataEngine._snapshot_order_book File "nautilus_trader/model/identifiers.pyx", line 280, in nautilus_trader.model.identifiers.InstrumentId.from_str_c ValueError: Error parsing InstrumentId from 'TRBUSDT': Missing '.' separator between symbol and venue components

cjdsellers commented 8 months ago

Hi @x-zho14

The instrument ID string also needs a venue component, because we need to be able to identify all TRBUSDT instruments which could be trading on other venues.

    @staticmethod
    def from_str(value: str) -> InstrumentId:
        """
        Return an instrument ID parsed from the given string value.
        Must be correctly formatted including characters either side of a single
        period.

        Examples: "AUD/USD.IDEALPRO", "BTCUSDT.BINANCE"

        Parameters
        ----------
        value : str
            The instrument ID string value to parse.

        Returns
        -------
        InstrumentId

        """
        return InstrumentId.from_str_c(value)

So in your case you would need InstrumentId.from_str("TRBUSDT.BINANCE"). I'll make sure to add the potential of the ValueError to the docs.

Hope that helps.

x-zho14 commented 8 months ago

Hi, the self.instrument_id is "InstrumentId.from_str("TRBUSDT-PERP.BINANCE")"! and I get it pass to self.subscribe_order_book_snapshots(instrument_id=self.instrument_id,depth=20). I think I pass the correct "InstrumentId.from_str("TRBUSDT-PERP.BINANCE")". It only fails at snapshots and don't fail at others. You can also replicate this bug on your example, nautilus_trader/nautilus_trader/examples/strategies/volatility_market_maker.py by masking off

self.subscribe_order_book_snapshots(
            self.instrument_id,
            depth=20,
            interval_ms=1000,
        )  # For debugging
dimitar-petrov commented 8 months ago

@cjdsellers

I believe this happens for instrument ids containing -, like BTCUSDT-PERP.

timer_name is using - as delimiter and if the InstrumentId contains a - it is breaking the parsing in _snapshot_order_book

    cpdef void _handle_subscribe_order_book_snapshots(
        self,
        MarketDataClient client,
        InstrumentId instrument_id,
        dict metadata,
    ):
        Condition.not_none(client, "client")
        Condition.not_none(instrument_id, "instrument_id")
        Condition.not_none(metadata, "metadata")

        if instrument_id.is_synthetic():
            self._log.error("Cannot subscribe for synthetic instrument `OrderBook` data.")
            return

        cdef:
            uint64_t interval_ms = metadata["interval_ms"]
            uint64_t interval_ns
            uint64_t timestamp_ns
        key = (instrument_id, interval_ms)
        if key not in self._order_book_intervals:
            self._order_book_intervals[key] = []

            timer_name = f"OrderBook-{instrument_id}-{interval_ms}"
            interval_ns = millis_to_nanos(interval_ms)
            timestamp_ns = self._clock.timestamp_ns()
            start_time_ns = timestamp_ns - (timestamp_ns % interval_ns)
            ...

When the timer event callback is executed InstrumentId.from_str_c is breaking.

    cpdef void _snapshot_order_book(self, TimeEvent snap_event):
        cdef tuple pieces = snap_event.name.partition('-')[2].partition('-')
        cdef InstrumentId instrument_id = InstrumentId.from_str_c(pieces[0])
        cdef int interval_ms = int(pieces[2])

Switching the delimiter to pipe(|) might be a quick fix for it.

            timer_name = f"OrderBook|{instrument_id}|{interval_ms}"
cjdsellers commented 8 months ago

Thanks for the suggestion there @dimitar-petrov

I went with the pipes, but then changed the parsing to make sure an instrument ID containing pipes doesn't break the new version too (Betfair uses instrument IDs with pipes).

Also, thanks again for the report and patience @x-zho14, looks like I was too quick to dismiss the issue.

Fix will be pushed to develop branch soon 🙏