mirror of
https://github.com/manifoldbt/manifoldbt.git
synced 2026-11-02 07:56:11 +00:00
Merge pull request #19 from manifoldbt/bench/hft-nautilus-linux
bench: build the Nautilus data objects at the scale of the installed wheel
This commit is contained in:
@@ -49,7 +49,10 @@ them in its own idiom:
|
||||
risk-adverse queue model, constant order latency.
|
||||
* **NautilusTrader**: a `Strategy` subclass per rule on a `BacktestEngine`:
|
||||
`OrderBookDelta` (L2_MBP / L3_MBO) or `QuoteTick` (L1_MBP) plus `TradeTick`,
|
||||
`queue_position=True`, `trade_execution=True`, a `LatencyModel`.
|
||||
`queue_position=True`, `trade_execution=True`, a `LatencyModel`. The data objects are
|
||||
built from raw integers, scaled to the fixed precision of the installed wheel (9
|
||||
decimals on Windows, 16 on Linux), which the adapter reads from Nautilus rather than
|
||||
assuming; the journals of the six workloads are identical on both.
|
||||
|
||||
## The rule this harness is built around
|
||||
|
||||
|
||||
@@ -16,16 +16,41 @@ import numpy as np
|
||||
import spec
|
||||
|
||||
PX_PREC, SZ_PREC = 1, 6
|
||||
PX_UNIT = 10 ** (9 - PX_PREC) # raw = value x 1e9 at the venue's precision
|
||||
SZ_UNIT = 10 ** (9 - SZ_PREC)
|
||||
CHUNK = 400_000
|
||||
|
||||
# One price tick and one quantity step as a raw integer. They depend on the build of
|
||||
# Nautilus, so _imports() sets them (see raw_unit).
|
||||
PX_UNIT = SZ_UNIT = None
|
||||
|
||||
|
||||
def raw_unit(precision):
|
||||
"""The raw integer of one unit of the last decimal at `precision`, on this build of Nautilus.
|
||||
|
||||
The data objects built with `from_raw` take the value scaled to the build's fixed precision, not
|
||||
to the instrument's: 9 decimals on the standard wheel (Windows), 16 on the high-precision wheel
|
||||
(Linux). A scale written as 10 ** 9 is right on one and 10 ** 7 too small on the other, and
|
||||
nothing complains: prices read as zero, the books look empty, a market order fills a quantity
|
||||
that rounds to nothing and the position opens flat (an AssertionError in PositionOpened)."""
|
||||
from nautilus_trader.model.objects import FIXED_PRECISION
|
||||
return 10 ** (FIXED_PRECISION - precision)
|
||||
|
||||
|
||||
def check_raw_scale(px_prec, sz_prec):
|
||||
"""Refuse to run if a raw integer built with raw_unit is not read back as the value it stands for."""
|
||||
from nautilus_trader.model.objects import Price, Quantity
|
||||
for cls, prec in ((Price, px_prec), (Quantity, sz_prec)):
|
||||
steps = 123_456
|
||||
got = cls.from_raw(steps * raw_unit(prec), prec).as_double()
|
||||
want = steps / 10 ** prec
|
||||
if abs(got - want) > 1e-9 * want:
|
||||
raise RuntimeError(f"raw scale: {cls.__name__} at {prec} decimals reads {got}, expected {want}")
|
||||
|
||||
|
||||
def _imports():
|
||||
global BacktestEngine, BacktestEngineConfig, FillModel, LatencyModel, LoggingConfig, StrategyConfig
|
||||
global BTC, USDT, OrderBookDelta, QuoteTick, TradeTick, AccountType, AggressorSide, BookAction
|
||||
global BookType, OmsType, OrderSide, RecordFlag, TimeInForce, InstrumentId, Symbol, TradeId, Venue
|
||||
global CurrencyPair, Money, Price, Quantity, Strategy
|
||||
global CurrencyPair, Money, Price, Quantity, Strategy, PX_UNIT, SZ_UNIT
|
||||
from nautilus_trader.backtest.engine import BacktestEngine, BacktestEngineConfig
|
||||
from nautilus_trader.backtest.models import FillModel, LatencyModel
|
||||
from nautilus_trader.config import LoggingConfig, StrategyConfig
|
||||
@@ -37,6 +62,8 @@ def _imports():
|
||||
from nautilus_trader.model.instruments import CurrencyPair
|
||||
from nautilus_trader.model.objects import Money, Price, Quantity
|
||||
from nautilus_trader.trading.strategy import Strategy
|
||||
check_raw_scale(PX_PREC, SZ_PREC)
|
||||
PX_UNIT, SZ_UNIT = raw_unit(PX_PREC), raw_unit(SZ_PREC)
|
||||
|
||||
|
||||
def instrument():
|
||||
@@ -66,13 +93,16 @@ def stream(d, tier, iid, feed_ns, conv_clock):
|
||||
conv_clock[0] accumulates the seconds spent converting."""
|
||||
t0 = time.perf_counter()
|
||||
n_t = len(d["tr_ts"])
|
||||
tr_px = (np.round(d["tr_px"] * 10 ** PX_PREC).astype(np.int64) * PX_UNIT)
|
||||
tr_sz = (np.round(d["tr_qty"] * 10 ** SZ_PREC).astype(np.int64) * SZ_UNIT)
|
||||
# Ticks (price x 10^PX_PREC) and steps (quantity x 10^SZ_PREC) as int64. They become raw integers
|
||||
# (x PX_UNIT, x SZ_UNIT) one by one as Python ints: on the high-precision build a raw price is
|
||||
# 128 bits wide and an int64 array would wrap around without a word.
|
||||
tr_px = np.round(d["tr_px"] * 10 ** PX_PREC).astype(np.int64)
|
||||
tr_sz = np.round(d["tr_qty"] * 10 ** SZ_PREC).astype(np.int64)
|
||||
if tier == "l2":
|
||||
n_d = len(d["d25_ts"])
|
||||
ts_d = d["d25_ts"]
|
||||
px_d = d["d25_px"] * PX_UNIT # ticks (= price x 10^PX_PREC) -> raw
|
||||
sz_d = (np.round(d["d25_qty"] * 10 ** SZ_PREC).astype(np.int64) * SZ_UNIT)
|
||||
px_d = d["d25_px"]
|
||||
sz_d = np.round(d["d25_qty"] * 10 ** SZ_PREC).astype(np.int64)
|
||||
side_d, last_d = d["d25_side"], d["d25_last"]
|
||||
else:
|
||||
n_d = len(d["q_ts"])
|
||||
@@ -87,10 +117,10 @@ def stream(d, tier, iid, feed_ns, conv_clock):
|
||||
F_LAST = int(RecordFlag.F_LAST)
|
||||
feed = int(feed_ns)
|
||||
if tier != "l2":
|
||||
q_bp = d["q_bpx"] * PX_UNIT
|
||||
q_ap = d["q_apx"] * PX_UNIT
|
||||
q_bq = (np.round(d["q_bqty"] * 10 ** SZ_PREC).astype(np.int64) * SZ_UNIT)
|
||||
q_aq = (np.round(d["q_aqty"] * 10 ** SZ_PREC).astype(np.int64) * SZ_UNIT)
|
||||
q_bp, q_ap = d["q_bpx"], d["q_apx"]
|
||||
q_bq = np.round(d["q_bqty"] * 10 ** SZ_PREC).astype(np.int64)
|
||||
q_aq = np.round(d["q_aqty"] * 10 ** SZ_PREC).astype(np.int64)
|
||||
pu, su = PX_UNIT, SZ_UNIT
|
||||
for a in range(0, len(order), CHUNK):
|
||||
t0 = time.perf_counter()
|
||||
out = []
|
||||
@@ -102,14 +132,14 @@ def stream(d, tier, iid, feed_ns, conv_clock):
|
||||
if tier == "l2":
|
||||
q = int(sz_d[j])
|
||||
push(Delta(iid, dele if q == 0 else upd, buy if side_d[j] == 0 else sell,
|
||||
int(px_d[j]), PX_PREC, q, SZ_PREC, 0, F_LAST if last_d[j] else 0, 0, t, t + feed))
|
||||
int(px_d[j]) * pu, PX_PREC, q * su, SZ_PREC, 0, F_LAST if last_d[j] else 0, 0, t, t + feed))
|
||||
else:
|
||||
push(Quote(iid, int(q_bp[j]), int(q_ap[j]), PX_PREC, PX_PREC, int(q_bq[j]), int(q_aq[j]),
|
||||
SZ_PREC, SZ_PREC, t, t + feed))
|
||||
push(Quote(iid, int(q_bp[j]) * pu, int(q_ap[j]) * pu, PX_PREC, PX_PREC, int(q_bq[j]) * su,
|
||||
int(q_aq[j]) * su, SZ_PREC, SZ_PREC, t, t + feed))
|
||||
else:
|
||||
i = j - n_d
|
||||
t = int(d["tr_ts"][i])
|
||||
push(Trade(iid, int(tr_px[i]), PX_PREC, int(tr_sz[i]), SZ_PREC,
|
||||
push(Trade(iid, int(tr_px[i]) * pu, PX_PREC, int(tr_sz[i]) * su, SZ_PREC,
|
||||
a_buy if d["tr_side"][i] == 0 else a_sell, TradeId(str(i)), t, t + feed))
|
||||
conv_clock[0] += time.perf_counter() - t0
|
||||
yield out
|
||||
@@ -380,7 +410,10 @@ def _make_taker_class():
|
||||
self.sent_pos = target
|
||||
|
||||
def on_order_filled(self, event):
|
||||
self.fills.append((event.ts_event, event.last_px.as_double(), event.last_qty.as_double(),
|
||||
# as_double() of a 128-bit raw price is off by one unit in the last place on some prices
|
||||
# (high-precision build); the price is a whole number of ticks, so round it back to one
|
||||
# and both builds hand over the same double.
|
||||
self.fills.append((event.ts_event, round(event.last_px.as_double(), PX_PREC), event.last_qty.as_double(),
|
||||
1 if event.order_side == OrderSide.BUY else -1))
|
||||
|
||||
def on_order_rejected(self, event):
|
||||
|
||||
@@ -15,8 +15,10 @@ import engine_nt as N
|
||||
import spec
|
||||
|
||||
L3_PX_PREC, L3_SZ_PREC = 4, 0
|
||||
L3_PX_UNIT = 10 ** (9 - L3_PX_PREC) # ITCH prices are in 1e-4 units: raw = price x 1e5
|
||||
L3_SZ_UNIT = 10 ** (9 - L3_SZ_PREC)
|
||||
# ITCH prices are in 1e-4 units and sizes in shares: raw = ticks x unit, the unit being that of the build
|
||||
# of Nautilus (see engine_nt.raw_unit), not a constant.
|
||||
L3_PX_UNIT = N.raw_unit(L3_PX_PREC)
|
||||
L3_SZ_UNIT = N.raw_unit(L3_SZ_PREC)
|
||||
|
||||
|
||||
def l3_instrument(symbol):
|
||||
@@ -181,6 +183,7 @@ def _make_maker_class():
|
||||
|
||||
def run_mbo(m, t_open, t_close, strategy=True):
|
||||
N._imports()
|
||||
N.check_raw_scale(L3_PX_PREC, L3_SZ_PREC)
|
||||
from nautilus_trader.model.currencies import USD
|
||||
|
||||
Maker = _make_maker_class()
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""The raw scale of the NautilusTrader adapter, on whichever build of Nautilus is installed.
|
||||
|
||||
python -m pytest benchmarks/vs_hft/test_nt_scale.py -q
|
||||
|
||||
The standard wheel (Windows) keeps prices and quantities on 9 decimals and the
|
||||
high-precision wheel (Linux) on 16. The adapter builds its data objects from raw integers,
|
||||
so a scale written for one build is wrong on the other without any error: prices read as
|
||||
zero and nothing trades. These tests read the objects back.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
np = pytest.importorskip("numpy")
|
||||
pytest.importorskip("nautilus_trader")
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
|
||||
import engine_nt as N # noqa: E402
|
||||
|
||||
|
||||
def _data():
|
||||
return {
|
||||
"tr_ts": np.array([2_000], dtype=np.int64), "tr_px": np.array([120101.7]),
|
||||
"tr_qty": np.array([0.000123]), "tr_side": np.array([0], dtype=np.int8),
|
||||
"q_ts": np.array([1_000], dtype=np.int64), "q_bpx": np.array([1201016], dtype=np.int64),
|
||||
"q_apx": np.array([1201017], dtype=np.int64), "q_bqty": np.array([1.5]), "q_aqty": np.array([2.25]),
|
||||
"d25_ts": np.array([1_000], dtype=np.int64), "d25_px": np.array([1201016], dtype=np.int64),
|
||||
"d25_qty": np.array([0.75]), "d25_side": np.array([0], dtype=np.int8), "d25_last": np.array([1], dtype=np.int8),
|
||||
}
|
||||
|
||||
|
||||
def _objects(tier):
|
||||
N._imports()
|
||||
_, iid, _ = N.instrument()
|
||||
(chunk,) = list(N.stream(_data(), tier, iid, 0, [0.0]))
|
||||
return chunk
|
||||
|
||||
|
||||
def test_raw_units_follow_the_build():
|
||||
N._imports()
|
||||
from nautilus_trader.model.objects import FIXED_PRECISION
|
||||
assert N.PX_UNIT == 10 ** (FIXED_PRECISION - N.PX_PREC)
|
||||
assert N.SZ_UNIT == 10 ** (FIXED_PRECISION - N.SZ_PREC)
|
||||
N.check_raw_scale(N.PX_PREC, N.SZ_PREC)
|
||||
N.check_raw_scale(4, 0)
|
||||
|
||||
|
||||
def test_best_bid_ask_ticks_and_trades_read_back():
|
||||
quote, trade = _objects("bbo")
|
||||
assert quote.bid_price.as_double() == pytest.approx(120101.6, rel=1e-12)
|
||||
assert quote.ask_price.as_double() == pytest.approx(120101.7, rel=1e-12)
|
||||
assert quote.bid_size.as_double() == pytest.approx(1.5, rel=1e-12)
|
||||
assert quote.ask_size.as_double() == pytest.approx(2.25, rel=1e-12)
|
||||
assert trade.price.as_double() == pytest.approx(120101.7, rel=1e-12)
|
||||
assert trade.size.as_double() == pytest.approx(0.000123, rel=1e-12)
|
||||
|
||||
|
||||
def test_book_deltas_read_back():
|
||||
delta, trade = _objects("l2")
|
||||
assert delta.order.price.as_double() == pytest.approx(120101.6, rel=1e-12)
|
||||
assert delta.order.size.as_double() == pytest.approx(0.75, rel=1e-12)
|
||||
assert trade.price.as_double() == pytest.approx(120101.7, rel=1e-12)
|
||||
Reference in New Issue
Block a user