Skip to content

Commit ba9220d

Browse files
committed
Add option for immediate execution with quotes inside the bid ask
# Conflicts: # nautilus_trader/backtest/engine.pyx # nautilus_trader/execution/engine.pyx
1 parent 8de442f commit ba9220d

File tree

10 files changed

+690
-162
lines changed

10 files changed

+690
-162
lines changed

examples/backtest/notebooks/databento_option_greeks.py

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@
5353
from nautilus_trader.model.data import DataType
5454
from nautilus_trader.model.data import QuoteTick
5555
from nautilus_trader.model.enums import OrderSide
56+
from nautilus_trader.model.enums import PriceType
5657
from nautilus_trader.model.greeks_data import GreeksData
5758
from nautilus_trader.model.identifiers import InstrumentId
5859
from nautilus_trader.model.identifiers import Venue
5960
from nautilus_trader.model.identifiers import new_generic_spread_id
6061
from nautilus_trader.model.instruments import FuturesContract
61-
from nautilus_trader.model.objects import Price
6262
from nautilus_trader.model.objects import Quantity
6363
from nautilus_trader.model.tick_scheme import TieredTickScheme
6464
from nautilus_trader.model.tick_scheme import register_tick_scheme
@@ -156,6 +156,15 @@
156156
],
157157
)
158158

159+
160+
def get_mid_price_rounded(tick, instrument, round_up=True, num_ticks=0):
161+
mid_price = tick.extract_price(PriceType.MID)
162+
if round_up:
163+
return instrument.next_ask_price(mid_price.as_double(), num_ticks=num_ticks)
164+
else:
165+
return instrument.next_bid_price(mid_price.as_double(), num_ticks=num_ticks)
166+
167+
159168
# %% [markdown]
160169
# ## strategy
161170

@@ -303,13 +312,31 @@ def on_quote_tick(self, tick):
303312
)
304313

305314
# Submit spread order when we have spread quotes available
306-
if tick.instrument_id == self.config.spread_id and not self.spread_order_submitted:
315+
if (
316+
tick.instrument_id == self.config.spread_id
317+
and not self.spread_order_submitted
318+
and tick.ts_init == 1715248980000000000
319+
):
307320
# Try submitting order immediately - the exchange should have processed the quote by now
308-
self.submit_market_order(instrument_id=self.config.spread_id, quantity=5)
321+
instrument = self.cache.instrument(tick.instrument_id)
322+
mid_price_rounded = get_mid_price_rounded(
323+
tick=tick,
324+
instrument=instrument,
325+
round_up=True, # Round up for buy orders (more aggressive)
326+
)
327+
self.submit_limit_order(
328+
instrument_id=self.config.spread_id,
329+
price=mid_price_rounded,
330+
quantity=5,
331+
)
309332
self.spread_order_submitted = True
310333

311334
if tick.instrument_id == self.config.spread_id2 and not self.spread_order_submitted2:
312-
self.submit_market_order(instrument_id=self.config.spread_id2, quantity=5)
335+
self.submit_limit_order(
336+
instrument_id=self.config.spread_id2,
337+
price=tick.ask_price,
338+
quantity=5,
339+
)
313340
self.spread_order_submitted2 = True
314341

315342
def on_order_filled(self, event):
@@ -367,18 +394,18 @@ def submit_market_order(self, instrument_id, quantity):
367394
order_side=(OrderSide.BUY if quantity > 0 else OrderSide.SELL),
368395
quantity=Quantity.from_int(abs(quantity)),
369396
)
397+
self.user_log(f"Submitting order: {order}")
370398
self.submit_order(order)
371-
self.user_log(f"Order submitted: {order}")
372399

373400
def submit_limit_order(self, instrument_id, price, quantity):
374401
order = self.order_factory.limit(
375402
instrument_id=instrument_id,
376403
order_side=(OrderSide.BUY if quantity > 0 else OrderSide.SELL),
377404
quantity=Quantity.from_int(abs(quantity)),
378-
price=Price.from_str(f"{price:.2f}"),
405+
price=price,
379406
)
407+
self.user_log(f"Submitting order: {order}")
380408
self.submit_order(order)
381-
self.user_log(f"Order submitted: {order}")
382409

383410
def user_log(self, msg, color=LogColor.GREEN):
384411
self.log.warning(f"{msg}", color=color)
@@ -463,6 +490,7 @@ def on_stop(self):
463490
strategies=strategies,
464491
streaming=(streaming if not load_greeks else None),
465492
catalogs=catalogs,
493+
# allow_immediate_quote_execution=True,
466494
)
467495

468496
# BacktestRunConfig
@@ -498,12 +526,9 @@ def on_stop(self):
498526
*data,
499527
]
500528

501-
# Configure venue with enhanced SizeAwareFillModel for realistic option execution
502-
# This fill model provides different execution behavior based on order size:
503-
# - Small orders (<=10 contracts): Good liquidity at best prices
504-
# - Large orders: Experience price impact with partial fills at worse prices
529+
# Configure venue with enhanced BestPriceFillModel for being able to execute limit orders anywhere between a bid ask
505530
fill_model = ImportableFillModelConfig(
506-
fill_model_path="nautilus_trader.backtest.models:SizeAwareFillModel",
531+
fill_model_path="nautilus_trader.backtest.models:BestPriceFillModel",
507532
config_path="nautilus_trader.backtest.config:FillModelConfig",
508533
config={},
509534
)

nautilus_trader/backtest/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,10 @@ class BacktestEngineConfig(NautilusKernelConfig, frozen=True):
334334
If logging should be bypassed.
335335
run_analysis : bool, default True
336336
If post backtest performance analysis should be run.
337+
allow_immediate_quote_execution : bool, default False
338+
Allows a strategy to receive quotes and pass orders before the data is
339+
processed by the execution engine. Only relevant in backtesting.
340+
Useful for backtests on discrete times, like every minute, hour or day.
337341
338342
"""
339343

@@ -344,6 +348,7 @@ class BacktestEngineConfig(NautilusKernelConfig, frozen=True):
344348
risk_engine: RiskEngineConfig | None = RiskEngineConfig()
345349
exec_engine: ExecEngineConfig | None = ExecEngineConfig()
346350
run_analysis: bool = True
351+
allow_immediate_quote_execution: bool = False
347352

348353
def __post_init__(self):
349354
if isinstance(self.trader_id, str):

0 commit comments

Comments
 (0)