nautechsystems / nautilus_trader

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

Binance min_notional, max_notional returning None or 0.0 #1238

Closed AnthonyVince closed 1 year ago

AnthonyVince commented 1 year ago

Bug Report

Expected Behaviour

The following code should return values in the strategy. Binance pre-trade checks verify orders against the notional. Nautilus Risk engine checs against max_notional (not checked yet)

self.cache.instrument(self.instrument_id).min_notional self.cache.instrument(self.instrument_id).max_notional

Actual Behaviour

Currently when connecting to Binance testnet for USDt margined perpetuals I get Zero or None.

Screenshot 2023-09-11 at 16 10 00

Steps to Reproduce the Problem


import os
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 CacheDatabaseConfig
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.core.rust.common import LogColor
from nautilus_trader.live.node import TradingNode
from nautilus_trader.model.data import OrderBookDeltas
from nautilus_trader.model.identifiers import InstrumentId
from nautilus_trader.model.orderbook import OrderBook
from nautilus_trader.trading.strategy import Strategy

class SubscribeStrategy(Strategy):
    def __init__(self) -> None:
        super().__init__()
        self.instrument_id = InstrumentId.from_str("ETHUSDT-PERP.BINANCE",)
        self.depth = 50
        self.interval_ms = 10

    def on_start(self) -> None:
        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
        self.subscribe_order_book_deltas(self.instrument_id, depth=50)
        self.subscribe_order_book_snapshots(
            self.instrument_id,
            depth=20,
            interval_ms=1000,
        )
        self.log.info(f"Instrument price increment: {self.instrument.price_increment}")
        self.log.info(f"Instrument min price: {self.instrument.min_price}")
        self.log.info(f"Instrument min notional: {self.instrument.min_notional}")
        self.log.info(f"Instrument max_notional: {self.instrument.max_notional}")

    def on_order_book_deltas(self, deltas: OrderBookDeltas) -> None:
        book = self.cache.order_book(self.instrument_id)
        self.log.info("on_order_book_deltas - book\n" + book.pprint(20), LogColor.CYAN)

    def on_order_book(self, order_book: OrderBook) -> None:
        self.log.info("on_order_book:\n" + order_book.pprint(20), LogColor.CYAN)

config_node = TradingNodeConfig(
    trader_id="TESTER-001",
    logging=LoggingConfig(
        log_level="INFO",
    ),
    exec_engine=LiveExecEngineConfig(
        reconciliation=False,
        reconciliation_lookback_mins=1440,
    ),
    cache_database=CacheDatabaseConfig(type="in-memory"),
    data_clients={
        "BINANCE": BinanceDataClientConfig(
            api_key=os.getenv("BINANCE_FUTURES_TESTNET_API_KEY"),  # "YOUR_BINANCE_TESTNET_API_KEY"
            api_secret=os.getenv("BINANCE_FUTURES_TESTNET_API_SECRET"),  # "YOUR_BINANCE_TESTNET_API_SECRET"
            account_type=BinanceAccountType.USDT_FUTURE,
            base_url_http=None,  # Override with custom endpoint
            base_url_ws=None,  # Override with custom endpoint
            us=False,  # If client is for Binance US
            testnet=True,  # If client uses the testnet
            instrument_provider=InstrumentProviderConfig(load_all=False, load_ids=frozenset(["ETHUSDT-PERP.BINANCE"])),
        ),
    },
    exec_clients={
        "BINANCE": BinanceExecClientConfig(
            api_key=os.getenv("BINANCE_FUTURES_TESTNET_API_KEY"),  # "YOUR_BINANCE_TESTNET_API_KEY"
            api_secret=os.getenv("BINANCE_FUTURES_TESTNET_API_SECRET"),  # "YOUR_BINANCE_TESTNET_API_SECRET"
            account_type=BinanceAccountType.USDT_FUTURE,
            base_url_http=None,  # Override with custom endpoint
            base_url_ws=None,  # Override with custom endpoint
            us=False,  # If client is for Binance US
            testnet=True,  # If client uses the testnet
            instrument_provider=InstrumentProviderConfig(load_all=False, load_ids=frozenset(["ETHUSDT-PERP.BINANCE"])),
        ),
    },
    timeout_connection=20.0,
    timeout_reconciliation=10.0,
    timeout_portfolio=10.0,
    timeout_disconnection=10.0,
    timeout_post_stop=1.0,
)
node = TradingNode(config=config_node)
strategy = SubscribeStrategy()
node.trader.add_strategy(strategy)
node.add_data_client_factory("BINANCE", BinanceLiveDataClientFactory)
node.add_exec_client_factory("BINANCE", BinanceLiveExecClientFactory)
node.build()

if __name__ == "__main__":
    try:
        node.run()
    finally:
        node.dispose()
  1. Run above code.
  2. It's also apparent if you print out the Instrument data dictionary and look inside the filters, all references to minNotional are zero.

Specifications

AnthonyVince commented 1 year ago

Example:

CryptoPerpetual(id=ETCUSDT-PERP.BINANCE, raw_symbol=ETCUSDT, asset_class=CRYPTOCURRENCY, asset_type=SWAP, quote_currency=USDT, is_inverse=False, price_precision=3, price_increment=0.001, size_precision=2, size_increment=0.01, multiplier=1, lot_size=1, margin_init=0.0500, margin_maint=0.0250, maker_fee=0.0200, taker_fee=0.0180, info={'symbol': 'ETCUSDT', 'pair': 'ETCUSDT', 'contractType': 'PERPETUAL', 'deliveryDate': 4133404800000, 'onboardDate': 1569398400000, 'status': 'TRADING', 'maintMarginPercent': '2.5000', 'requiredMarginPercent': '5.0000', 'baseAsset': 'ETC', 'quoteAsset': 'USDT', 'marginAsset': 'USDT', 'pricePrecision': 3, 'quantityPrecision': 2, 'baseAssetPrecision': 8, 'quotePrecision': 8, 'underlyingType': 'COIN', 'underlyingSubType': [], 'settlePlan': 0, 'triggerProtect': '0.0800', 'liquidationFee': '0.015000', 'marketTakeBound': '0.10', 'filters': [{'filterType': 'PRICE_FILTER', 'minPrice': '0.567', 'maxPrice': '946.547', 'tickSize': '0.001', 'multiplierUp': None, 'multiplierDown': None, 'multiplierDecimal': None, 'avgPriceMins': None, 'minQty': None, 'maxQty': None, 'stepSize': None, 'limit': None, 'maxNumOrders': None, 'notional': None, 'minNotional': None, 'maxNumAlgoOrders': None, 'bidMultiplierUp': None, 'bidMultiplierDown': None, 'askMultiplierUp': None, 'askMultiplierDown': None, 'applyMinToMarket': None, 'maxNotional': None, 'applyMaxToMarket': None, 'maxNumIcebergOrders': None, 'maxPosition': None, 'minTrailingAboveDelta': None, 'maxTrailingAboveDelta': None, 'minTrailingBelowDelta': None, 'maxTrailingBelowDelta': None}, {'filterType': 'LOT_SIZE', 'minPrice': None, 'maxPrice': None, 'tickSize': None, 'multiplierUp': None, 'multiplierDown': None, 'multiplierDecimal': None, 'avgPriceMins': None, 'minQty': '0.01', 'maxQty': '100000', 'stepSize': '0.01', 'limit': None, 'maxNumOrders': None, 'notional': None, 'minNotional': None, 'maxNumAlgoOrders': None, 'bidMultiplierUp': None, 'bidMultiplierDown': None, 'askMultiplierUp': None, 'askMultiplierDown': None, 'applyMinToMarket': None, 'maxNotional': None, 'applyMaxToMarket': None, 'maxNumIcebergOrders': None, 'maxPosition': None, 'minTrailingAboveDelta': None, 'maxTrailingAboveDelta': None, 'minTrailingBelowDelta': None, 'maxTrailingBelowDelta': None}, {'filterType': 'MARKET_LOT_SIZE', 'minPrice': None, 'maxPrice': None, 'tickSize': None, 'multiplierUp': None, 'multiplierDown': None, 'multiplierDecimal': None, 'avgPriceMins': None, 'minQty': '0.01', 'maxQty': '100000', 'stepSize': '0.01', 'limit': None, 'maxNumOrders': None, 'notional': None, 'minNotional': None, 'maxNumAlgoOrders': None, 'bidMultiplierUp': None, 'bidMultiplierDown': None, 'askMultiplierUp': None, 'askMultiplierDown': None, 'applyMinToMarket': None, 'maxNotional': None, 'applyMaxToMarket': None, 'maxNumIcebergOrders': None, 'maxPosition': None, 'minTrailingAboveDelta': None, 'maxTrailingAboveDelta': None, 'minTrailingBelowDelta': None, 'maxTrailingBelowDelta': None}, {'filterType': 'MAX_NUM_ORDERS', 'minPrice': None, 'maxPrice': None, 'tickSize': None, 'multiplierUp': None, 'multiplierDown': None, 'multiplierDecimal': None, 'avgPriceMins': None, 'minQty': None, 'maxQty': None, 'stepSize': None, 'limit': 200, 'maxNumOrders': None, 'notional': None, 'minNotional': None, 'maxNumAlgoOrders': None, 'bidMultiplierUp': None, 'bidMultiplierDown': None, 'askMultiplierUp': None, 'askMultiplierDown': None, 'applyMinToMarket': None, 'maxNotional': None, 'applyMaxToMarket': None, 'maxNumIcebergOrders': None, 'maxPosition': None, 'minTrailingAboveDelta': None, 'maxTrailingAboveDelta': None, 'minTrailingBelowDelta': None, 'maxTrailingBelowDelta': None}, {'filterType': 'MAX_NUM_ALGO_ORDERS', 'minPrice': None, 'maxPrice': None, 'tickSize': None, 'multiplierUp': None, 'multiplierDown': None, 'multiplierDecimal': None, 'avgPriceMins': None, 'minQty': None, 'maxQty': None, 'stepSize': None, 'limit': 10, 'maxNumOrders': None, 'notional': None, 'minNotional': None, 'maxNumAlgoOrders': None, 'bidMultiplierUp': None, 'bidMultiplierDown': None, 'askMultiplierUp': None, 'askMultiplierDown': None, 'applyMinToMarket': None, 'maxNotional': None, 'applyMaxToMarket': None, 'maxNumIcebergOrders': None, 'maxPosition': None, 'minTrailingAboveDelta': None, 'maxTrailingAboveDelta': None, 'minTrailingBelowDelta': None, 'maxTrailingBelowDelta': None}, {'filterType': 'MIN_NOTIONAL', 'minPrice': None, 'maxPrice': None, 'tickSize': None, 'multiplierUp': None, 'multiplierDown': None, 'multiplierDecimal': None, 'avgPriceMins': None, 'minQty': None, 'maxQty': None, 'stepSize': None, 'limit': None, 'maxNumOrders': None, 'notional': '5.0', 'minNotional': None, 'maxNumAlgoOrders': None, 'bidMultiplierUp': None, 'bidMultiplierDown': None, 'askMultiplierUp': None, 'askMultiplierDown': None, 'applyMinToMarket': None, 'maxNotional': None, 'applyMaxToMarket': None, 'maxNumIcebergOrders': None, 'maxPosition': None, 'minTrailingAboveDelta': None, 'maxTrailingAboveDelta': None, 'minTrailingBelowDelta': None, 'maxTrailingBelowDelta': None}, {'filterType': 'PERCENT_PRICE', 'minPrice': None, 'maxPrice': None, 'tickSize': None, 'multiplierUp': '1.1000', 'multiplierDown': '0.9000', 'multiplierDecimal': '4', 'avgPriceMins': None, 'minQty': None, 'maxQty': None, 'stepSize': None, 'limit': None, 'maxNumOrders': None, 'notional': None, 'minNotional': None, 'maxNumAlgoOrders': None, 'bidMultiplierUp': None, 'bidMultiplierDown': None, 'askMultiplierUp': None, 'askMultiplierDown': None, 'applyMinToMarket': None, 'maxNotional': None, 'applyMaxToMarket': None, 'maxNumIcebergOrders': None, 'maxPosition': None, 'minTrailingAboveDelta': None, 'maxTrailingAboveDelta': None, 'minTrailingBelowDelta': None, 'maxTrailingBelowDelta': None}], 'orderTypes': ['LIMIT', 'MARKET', 'STOP', 'STOP_MARKET', 'TAKE_PROFIT', 'TAKE_PROFIT_MARKET', 'TRAILING_STOP_MARKET'], 'timeInForce': ['GTC', 'IOC', 'FOK', 'GTX']})
filipmacek commented 1 year ago

The solution was to get maxNotionalValue from the position risk endpoint. So after that we are capped at zero and max notional, and I am ok with that. PR is created for this bug https://github.com/nautechsystems/nautilus_trader/pull/1239 @cjdsellers

cjdsellers commented 1 year ago

Hi @AnthonyVince thanks for the report here!

Thanks for the solution @filipmacek, from what I could see from the Binance API this is the correct way to determine the max notional.

I also went ahead and added instrument min/max notional risk limit checks per order into the RiskEngine 58e80067286c6b69fd1ccf61c93ba6de5be326ef.