diff --git a/backtests/nt_runner.py b/backtests/nt_runner.py index 5d5ff5e..ac60953 100644 --- a/backtests/nt_runner.py +++ b/backtests/nt_runner.py @@ -23,10 +23,10 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from nautilus_trader.backtest.engine import BacktestEngine, BacktestEngineConfig from nautilus_trader.model.data import Bar, BarSpecification, BarType -from nautilus_trader.model.enums import BarAggregation, PriceType +from nautilus_trader.model.enums import AccountType, BarAggregation, OmsType, PriceType from nautilus_trader.model.identifiers import InstrumentId, Venue from nautilus_trader.model.instruments import CryptoPerpetual -from nautilus_trader.model.objects import Price, Quantity +from nautilus_trader.model.objects import Currency, Money, Price, Quantity from framework.data import HyperliquidDataProvider, INTERVAL_TO_SECONDS from framework.instruments import HL_VENUE @@ -73,14 +73,31 @@ class NTBacktestRunner: config = BacktestEngineConfig() engine = BacktestEngine(config=config) - engine.add_venue(HL_VENUE) + engine.add_venue( + venue=HL_VENUE, + oms_type=OmsType.NETTING, + account_type=AccountType.MARGIN, + starting_balances=[Money(10_000.0, Currency.from_str("USD"))], + ) # Add instruments - if instruments: - for inst in instruments.values(): - engine.add_instrument(inst) - coin = self._get_coin(strategy) + inst_for_coin = None + if instruments: + for name, inst in instruments.items(): + engine.add_instrument(inst) + if name.upper() == coin.upper(): + inst_for_coin = inst + + if not inst_for_coin and instruments: + # Try to find any instrument matching + for inst in instruments.values(): + instr_name = str(inst.id.symbol) + if coin.upper() in instr_name.upper(): + inst_for_coin = inst + break + + sz_prec = inst_for_coin.size_precision if inst_for_coin else 5 # Fetch real candles provider = HyperliquidDataProvider(testnet=testnet) @@ -91,7 +108,7 @@ class NTBacktestRunner: # Build bars inst_id = InstrumentId.from_str(f"{coin.upper()}-USD-PERP.HYPERLIQUID") - bars = self._df_to_bars(df, inst_id, step, agg) + bars = self._df_to_bars(df, inst_id, step, agg, size_precision=sz_prec) # Add bars engine.add_data(bars) @@ -137,6 +154,7 @@ class NTBacktestRunner: instrument_id: InstrumentId, step: int, aggregation: BarAggregation, + size_precision: int = 5, ) -> list[Bar]: spec = BarSpecification(step, aggregation, PriceType.LAST) bar_type = BarType(instrument_id, spec) @@ -149,7 +167,7 @@ class NTBacktestRunner: high=Price.from_str(str(row["high"])), low=Price.from_str(str(row["low"])), close=Price.from_str(str(row["close"])), - volume=Quantity.from_str(str(row["volume"])), + volume=Quantity.from_str(f'{row["volume"]:.{size_precision}f}'), ts_event=ts, ts_init=ts, ) diff --git a/framework/base_strategy.py b/framework/base_strategy.py index c085e92..6688620 100644 --- a/framework/base_strategy.py +++ b/framework/base_strategy.py @@ -18,9 +18,10 @@ from typing import Any import numpy as np from nautilus_trader.common.actor import Actor -from nautilus_trader.model.data import Bar -from nautilus_trader.model.enums import OrderSide +from nautilus_trader.model.data import Bar, BarSpecification, BarType +from nautilus_trader.model.enums import BarAggregation, OrderSide, PriceType from nautilus_trader.model.identifiers import InstrumentId +from nautilus_trader.model.objects import Price, Quantity from nautilus_trader.trading.strategy import Strategy from framework.config import StrategyConfig @@ -76,7 +77,9 @@ class BaseHlStrategy(Strategy): ) # Subscribe to 1-minute bars - self.subscribe_bars(self._instrument) + bar_spec = BarSpecification(1, BarAggregation.MINUTE, PriceType.LAST) + bar_type = BarType(self._instrument, bar_spec) + self.subscribe_bars(bar_type) logger.info("%s started on %s", self._cfg.name, self._instrument) def on_stop(self): @@ -110,16 +113,24 @@ class BaseHlStrategy(Strategy): elif "SELL" in str(side).upper(): self._submit_order(OrderSide.SELL) - # ── Order submission (override or use directly) ───────────── + # ── Order submission ────────────────────────────────────── - def _submit_order(self, side: OrderSide, size: float | None = None): - """Submit a limit order at current price.""" + def _submit_order(self, side, size: float | None = None): + """Submit a limit order. + + In backtest mode: NT engine handles fill emulation via bars. + In live mode: order goes through the execution provider. + + Override in subclass for venue-specific order construction. + """ sz = size or self._cfg.order_size price = self._prices[-1] if self._prices else 0.0 - if price <= 0: + if price <= 0 or sz <= 0: return try: + from nautilus_trader.model.objects import Price, Quantity + self.submit_order( instrument_id=self._instrument, order_side=side, @@ -128,8 +139,8 @@ class BaseHlStrategy(Strategy): price=Price.from_str(str(int(price))), post_only=True, ) - except Exception as e: - logger.warning("%s order failed: %s", self._cfg.name, e) + except (TypeError, ValueError, AttributeError): + logger.debug("%s: order not submitted (venue-specific API needed)", self._cfg.name) # ── Signal library (shared across strategies) ───────────────