Release Notes¶
0.3.38¶
Fixed: filled orders now heal missed position cache updates
After a terminal order update with non-zero fill quantity, the base OMS now schedules a short delayed REST position resync. This repairs position cache misses when the exchange order callback arrives but the private position stream update is delayed or dropped. Overlapping fill-triggered resyncs are coalesced, and a fill that arrives while a resync is pending marks the task to run one more pass afterward.
0.3.37¶
Fixed: Binance futures kline streams use routed market endpoints
BinanceKlineDirectWSClient now normalizes USD-M and COIN-M futures kline
WebSocket URLs to /market/stream?streams=. Legacy custom roots such as
wss://fstream.binance.com and legacy combined stream URLs are upgraded
automatically, while already-routed market stream URLs and spot stream URLs are
preserved. This matches Binance’s post-2026-04-23 futures market stream routing
and prevents reconnect loops on the retired root endpoint.
0.3.36¶
Fixed: cache TTL cleanup handles missing order timestamps safely
Orders whose optional timestamp is None no longer crash
AsyncCache TTL cleanup or stop the periodic cache sync task.
Fixed: persistent open-order snapshots are synced after TTL cleanup
The periodic cache sync now writes order history, evicts expired in-memory orders, and then writes open-order indexes. Expired non-terminal orders remain in order history but no longer survive in persistent open-order snapshots for an extra sync interval.
0.3.35¶
Fixed: order cache TTL cleanup clears stale open-order indexes
Expired non-terminal orders are now removed from all in-memory order indexes
when AsyncCache evicts them, including open-order, symbol-open,
cancel-intent, inflight, and symbol-history indexes. TWAP child orders that
miss terminal callbacks therefore no longer remain visible as open after the
order object expires.
Cache cleanup now emits one summary warning per cleanup cycle instead of one
warning per expired order, reducing repeated
AsyncCache: order ... is not closed, but expired logs during or after TWAP
execution.
0.3.34¶
Fixed: REST position resync clears stale cached positions
Binance, Bitget, Bybit, OKX, and HyperLiquid now clear cached positions that
are missing from the latest REST position snapshot during startup and reconnect
resync. Binance also applies positionAmt=0 REST positions through the cache
so stale open positions are removed consistently with WebSocket updates.
0.3.33¶
Fixed: subscription refresh operations preserve caller order
SubscriptionManagementSystem now uses one FIFO operation queue for
subscribe and unsubscribe requests. BookL2 refresh flows that enqueue
unsubscribe_bookl2() followed by subscribe_bookl2() now execute in that
order, preventing the real exchange subscription from being removed last.
0.3.32¶
Fixed: fast-closing create ACKs are reconciled through REST
Binance, Bitget, Bybit, OKX, and HyperLiquid now schedule short REST order
lookup after successful REST or WebSocket create ACKs for MARKET and
IOC / FOK orders. If the private order stream misses a fast terminal
event, the real FILLED / CANCELED / EXPIRED state is written back
instead of leaving the order stuck in PENDING or ACCEPTED until cache
expiry.
Fixed: cache order status updates now have a public API
AsyncCache.update_order_status() is now the public wrapper for validated
order status cache writes, so OMS code no longer needs to call the private
_order_status_update() method directly.
Fixed: batch create ACKs use the same terminal reconciliation
Binance, Bybit, OKX, and HyperLiquid batch create success paths now reuse the same post-ACK reconciliation helper for fast-closing orders.
0.3.31¶
Fixed: Binance cancel ACK terminal states are applied immediately
Binance WebSocket cancel ACKs now apply returned terminal states such as
CANCELED or FILLED immediately instead of always writing
CANCELING and waiting for a private order-stream event.
Fixed: cancel success paths use short REST reconciliation across exchanges
Binance, Bitget, Bybit, OKX, and HyperLiquid now schedule a short delayed REST
order lookup after successful cancel ACKs that leave the order in
CANCELING. If the private order stream misses the terminal event, the real
FILLED / CANCELED / EXPIRED state is written back quickly.
Fixed: Binance user-data stream expiry is recovered explicitly
Binance listenKeyExpired events now trigger user-data stream recovery and
resync, and private connector health includes latest order/account update
timestamps.
0.3.30¶
Fixed: Bybit server-time sync uses curl_cffi response attributes
Bybit signed REST request preparation now reads response.content and
response.status_code from curl_cffi responses, preventing repeated
'Response' object has no attribute 'read' warnings. Failed time-sync
attempts now use a short 5 second retry backoff instead of retrying on every
signed request.
Fixed: cancel ``Unknown order`` / not-found errors trigger REST reconciliation
Binance, Bitget, Bybit, OKX, and HyperLiquid WebSocket cancel not-found style errors now immediately trigger REST order lookup. Closed REST states are written back and resolve the cancel ACK wait, reducing the chance that a maker fill remains unpaired until a long strategy timeout fires.
0.3.29¶
Fixed: OKX import dependency is declared
orjson is now included in the base dependencies because the OKX exchange
module imports it during the nexustrader.engine import chain. This prevents
fresh installs from failing with a missing dependency that downstream broad
exception handlers could misreport as nexustrader not available.
0.3.28¶
Fixed: Linux/macOS base installs no longer require MetaTrader5
MetaTrader5 is now available only through the Windows-only tradfi extra,
so base installs on Linux/macOS do not attempt to install the Windows-only MT5
wheel. requirements.txt and Docker installation paths were also synchronized
with the current production dependency set.
0.3.27¶
Fixed: Bybit TradFi position cache now stays synchronized with MT5
Bybit TradFi uses a local MetaTrader 5 terminal process instead of a private
exchange WebSocket. NexusTrader now refreshes MT5 positions_get() into the
strategy cache after connector startup, on a background polling loop, after
immediate market fills, and after pending-order close events.
This makes position reads reliable during live MT5 trading:
position = self.cache.get_position("XAUUSD_s.BYBIT_TRADFI")
Fixed: stale Bybit TradFi positions are cleared
When MT5 returns an empty positions list, stale BYBIT_TRADFI positions are
removed from the cache. When MT5 returns None, NexusTrader treats that as a
read failure and keeps the existing cache state to avoid falsely flattening
positions during transient terminal/API errors.
Documentation: standard symbols and side semantics
The Bybit TradFi and cache docs now clarify that strategy APIs and cache lookups must use NexusTrader symbols, not raw MT5 broker symbols:
# Correct
self.cache.get_position("XAUUSD_s.BYBIT_TRADFI")
# Wrong
self.cache.get_position("XAUUSD.s")
Position.side uses PositionSide.LONG / PositionSide.SHORT /
PositionSide.FLAT. Order direction remains OrderSide.BUY /
OrderSide.SELL across Bybit TradFi and all other exchanges.
Fixed: OKX market loading accepts integer ``instIdCode`` values
OKX can return numeric instIdCode values from the public instruments
endpoint. NexusTrader now accepts both string and integer values when decoding
OKX market metadata, so startup keeps the BTCUSDT-PERP.OKX to
BTC-USDT-SWAP mapping required by public subscriptions and WebSocket order
operations.
Verification
uv run ruff check
uv run pytest
0.3.26¶
Fixed: ``on_start()`` can access the engine event loop
Engine.start() now sets the engine loop as the current thread event loop
before calling the user on_start() hook. Existing strategy code can use
asyncio.get_event_loop() in on_start() and schedule tasks without
accessing the private engine._loop attribute.
Fixed: ``Strategy.set_timer()`` compatibility restored
The old public set_timer(callback, interval, ...) API has been restored as
a deprecated wrapper around schedule(..., trigger="interval"). It now emits
DeprecationWarning and maps interval, name, start_time, and
stop_time to the scheduler instead of failing with AttributeError.
Verification
Regression coverage was added for both compatibility fixes:
uv run pytest test\core\test_engine_start_loop.py test\core\test_strategy_timer_compat.py -q
0.3.25¶
Fixed: credential TOML file is no longer required at import/startup
Importing nexustrader.constants no longer raises FileNotFoundError when
.keys/.secrets.toml is missing. The runtime now emits a warning instead,
so public-only market-data workflows, mock setups, tests, and direct-credential
configurations are not blocked by the absence of a TOML secrets file.
Fixed: multiple credential sources are supported again
BasicConfig now accepts the supported credential sources without forcing
users into one file format:
direct constructor values:
BasicConfig(api_key="...", secret="...")Dynaconf lookup:
BasicConfig(settings_key="BYBIT.DEMO")plain environment variables:
BasicConfig.from_env("BYBIT")
Dynaconf lookup supports both .keys/.secrets.toml and NEXUS_ prefixed
nested environment variables such as NEXUS_BYBIT__DEMO__API_KEY. Plain
environment lookup reads variables like BYBIT_API_KEY, BYBIT_SECRET,
and BYBIT_PASSPHRASE.
Operational note
Private trading still requires valid API credentials. This patch only changes when and where credential validation happens: startup/import is permissive, and private connector initialization or exchange requests remain responsible for failing when required credentials are actually missing.
Verification
The hotfix was checked by importing nexustrader.constants without
.keys/.secrets.toml, resolving credentials through both
BasicConfig.from_env() and BasicConfig(settings_key=...), then running
ruff check and py_compile on the changed config modules.
0.3.24¶
New: local compatibility layer for core runtime primitives
NexusTrader now includes lightweight in-repo replacements for the core
nautilus-style primitives used by the runtime and tests, including
MessageBus, LiveClock, TraderId, UUID4, signature helpers, and
minimal HTTP/WebSocket shims. Strategies and tests no longer need to import
these primitives directly from nautilus-trader.
New: websocket health reporting
Public and private websocket clients now expose operational health through
get_health(). The health payload includes connection state, subscription
count, last message timestamp and age, message count, reconnect count, and
disconnect timestamps.
New: direct Binance kline websocket subscriptions
Binance kline subscriptions now support direct combined-stream websocket clients with stream chunking, heartbeat handling, receive timeouts, reconnect backoff, and dedicated health reporting for the direct kline stream.
Changed: dependency and platform profile simplified
Project metadata now targets Python >=3.11,<3.13, skips uvloop on
Windows, and uses a smaller dependency set centered on the currently supported
runtime path. Engine selects WindowsSelectorEventLoopPolicy on Windows
so aiohttp / aiodns can run reliably without uvloop.
Changed: exchange and documentation surface narrowed
The active documented exchange paths are now Binance, Bybit, and OKX. Legacy or unused factory modules, CLI/web app modules, persistence backends, Bybit TradFi, Bitget, and HyperLiquid implementation/docs were removed or excluded from the main documentation surface. Sphinx API, concept, exchange, installation, and quickstart docs were refreshed to match the current runtime.
Fixed: Bybit startup timestamp failures
Bybit market loading now disables ccxt’s private currency prefetch by default, enables time-difference adjustment, and uses a 10 second receive window. This prevents startup from failing on private coin-info timestamp checks when local system time drifts outside Bybit’s default receive window.
Fixed: Bybit signed REST timestamp drift
The Bybit REST client now uses a 10 second receive window and syncs a cached
Bybit server-time offset before signed requests. Public Bybit subscription
examples now configure public connectors only, avoiding
/v5/account/wallet-balance initialization for market-data-only demos.
Fixed: OKX market loading sandbox parsing failures
OKX markets are now loaded from raw public instrument endpoints. Malformed instruments are skipped without aborting engine startup.
Testing
Focused coverage was added or updated for websocket health tracking, local message bus behavior, OKX raw market loading, and Bybit exchange/REST timestamp configuration. The focused Bybit checks were verified with:
uv run pytest test\exchange\test_bybit_exchange_config.py test\exchange\test_bybit_rest_api.py -q
0.3.23¶
Fixed: ``cancel_all_orders()`` skipped already-marked orders on Bitget / HyperLiquid / OKX EMS
Strategy.cancel_all_orders() marks all currently open OIDs with cancel intent
before the request is handed to EMS. The Bitget / HyperLiquid / OKX EMS
overrides then called get_open_orders() without include_canceling=True,
so they could immediately observe an empty set and never send the real
per-order cancel requests. Those paths now fetch the full open-order set and
fan out the cancel requests correctly.
Fixed: OKX ``cancel_all_orders()`` was a no-op
OkxOrderManagementSystem.cancel_all_orders() now submits batched REST
requests through /api/v5/trade/cancel-batch-orders instead of doing
nothing. Requests are chunked to 20 orders per batch, successful entries are
marked CANCELING, and rejected entries are surfaced as CANCEL_FAILED
with the exchange error message preserved per order.
Fixed: amend-order success paths no longer violate the order state machine
Binance / Bybit / OKX previously wrote OrderStatus.PENDING after a
successful modify request. For an order already in ACCEPTED or
PARTIALLY_FILLED state, this creates an invalid transition such as
ACCEPTED -> PENDING that is rejected by STATUS_TRANSITIONS, causing the
cache update to be dropped silently. The amend paths now reuse the cached live
status so valid no-op transitions like ACCEPTED -> ACCEPTED and
PARTIALLY_FILLED -> PARTIALLY_FILLED are accepted.
Fixed: amend-order cache writes no longer erase known fill state
Bybit / OKX amend success handlers were creating partial Order snapshots
that reset fields such as amount, filled, remaining, side,
type, and time_in_force when only price or size was amended. This was
especially dangerous for partially filled orders because the local cache could
lose the known execution state immediately after a successful amend. These
handlers now preserve the cached execution metadata and only overwrite fields
that genuinely changed; Binance also preserves the effective amount on
price-only amend requests.
0.3.22¶
Changed: logging backend replaced — ``nexuslog`` → ``loguru``
The nexuslog Rust-backed logging package has been replaced with
loguru — a pure-Python logger with
zero build requirements (no C++ or Rust toolchain needed on any platform).
The Logger shim interface (self.log.info(), self.log.debug(), etc.)
is fully backward-compatible. No strategy code changes are required.
Changed: time-based log rotation
setup_nautilus_core() / setup_nexus_core() now supports automatic
daily log rotation when a filename is provided:
from nexustrader.config import Config, LogConfig
config = Config(
...,
log_config=LogConfig(
filename="logs/nexus.log", # enables rotation
),
)
Rotation defaults:
When: midnight (
rotation_when="midnight")Retention: 30 days (
rotation_backup_count=30)Encoding: UTF-8
Mode: async non-blocking (
enqueue=True)
All defaults can be overridden by passing the corresponding kwargs
directly to setup_nautilus_core().
Changed: ``TRACE`` level now fully supported
loguru natively supports TRACE (level 5). logger.trace() calls are
emitted at true TRACE severity — no longer remapped to DEBUG.
0.3.20¶
Improved: Bybit TradFi tick polling interval reduced to 10 ms
The default TICK_POLL_INTERVAL for BybitTradeFiPublicConnector has been
reduced from 100 ms to 10 ms. Because each symbol_info_tick() call to
the MT5 terminal completes in roughly 15–60 µs over the local named-pipe IPC,
the 100 ms default was far more conservative than necessary. At 10 ms the
average bid/ask detection latency drops from ~50 ms to ~5 ms with negligible
additional CPU load (~0.6 % per polling symbol).
New: ``tick_poll_interval`` configurable via ``PublicConnectorConfig``
Users can now override the MT5 tick polling rate at configuration time without subclassing or patching the connector:
# Default (10 ms)
PublicConnectorConfig(account_type=BybitTradeFiAccountType.DEMO)
# Custom (20 ms)
PublicConnectorConfig(account_type=BybitTradeFiAccountType.DEMO, tick_poll_interval=0.02)
# Aggressive (5 ms)
PublicConnectorConfig(account_type=BybitTradeFiAccountType.DEMO, tick_poll_interval=0.005)
The field is None by default, in which case the connector’s built-in
default (0.01 s) is used. Other exchanges ignore this field entirely.
New: ``price_type`` parameter for Bybit TradFi limit orders
create_order() and create_order_ws() now accept a price_type
keyword argument for limit orders on Bybit TradFi. When supplied, the OMS
calls symbol_info_tick() at the moment the order is actually dispatched to
MT5, replacing the price the strategy computed from a potentially stale
on_bookl1 event:
|
Price sent to MT5 |
|---|---|
(not set) |
The |
|
Latest best bid, re-fetched at submission time |
|
Latest best ask, re-fetched at submission time |
|
Best ask for a buy order, best bid for a sell order |
# Cross the spread — aggressive limit, price fetched fresh at dispatch
self.create_order(
symbol="EURUSD.BYBIT_TRADFI",
side=OrderSide.BUY,
type=OrderType.LIMIT,
amount=Decimal("0.1"),
price_type="opponent",
)
This parameter is only effective for Bybit TradFi (MT5) limit orders. Passing it to any other exchange connector has no effect.
0.3.19¶
Fixed: Bybit WebSocket ping timeout misaligned with official recommendation
Both BybitWSClient and BybitWSApiClient used ping_idle_timeout=5
and ping_reply_timeout=2. Bybit’s official documentation recommends
sending a heartbeat every 20 seconds; the 5-second interval was
unnecessarily aggressive and the 2-second reply window was too tight for
normal network jitter between the client and Bybit servers.
Fix: Updated both constructors to ping_idle_timeout=20 /
ping_reply_timeout=5.
Fixed: OKX WebSocket ping timeout misaligned with official recommendation
Both OkxWSClient and OkxWSApiClient used ping_idle_timeout=5
and ping_reply_timeout=2. OKX closes the connection if no message is
exchanged within 30 seconds; the 5-second idle timeout was overly
aggressive and the 2-second reply window was too tight for typical latency
between a cloud host and OKX servers.
Fix: Updated both constructors to ping_idle_timeout=30 /
ping_reply_timeout=5, aligning with Bitget and HyperLiquid which already
used these values.
0.3.18¶
Fixed: Bybit WS API pong never recognized → infinite reconnect loop
BybitWSApiClient.user_api_pong_callback decoded pong frames using
BybitWsApiGeneralMsg, which has required fields retCode and retMsg.
The actual Bybit API WS pong response is {"op": "pong", "connId": "xxx"}
and does not carry those fields, so msgspec.json.decode raised
DecodeError on every pong, the callback returned False, picows
triggered a 2-second timeout, disconnected, and reconnected 1 second later —
an infinite reconnect loop.
Fix 1: user_api_pong_callback now decodes with BybitWsMessageGeneral
(all fields optional; is_pong checks both op == "pong" and
ret_msg == "pong"), matching the approach already used by
user_pong_callback for the public WS.
Fix 2: BybitWSApiClient.__init__ now passes
auto_ping_strategy="ping_periodically", consistent with BybitWSClient.
Previously the omission defaulted to "ping_when_idle", causing
behavioural inconsistency between the public and private WS clients.
Fixed: OKX WebSocket clients missing ``auto_ping_strategy``
Both OkxWSClient and OkxWSApiClient were constructed without an
explicit auto_ping_strategy, defaulting picows to "ping_when_idle".
This is inconsistent with the "ping_periodically" strategy used by every
other exchange’s WS clients and can cause connection drops during low-traffic
periods.
Fix: Added auto_ping_strategy="ping_periodically" to both OKX WS
client constructors.
0.3.17¶
Fixed: WebSocket reconnect loop missing ``await`` on ``disconnect()``
In WSClient._connection_handler(), the call to self.disconnect() inside
the reconnect loop was not await-ed. Since disconnect() is an
async def, the bare call produced a coroutine object that was silently
discarded, generating RuntimeWarning: coroutine 'WSClient.disconnect' was
never awaited on every reconnect cycle. The stale transport was never torn
down, causing resource leaks and potential duplicate connections.
Fix: Added await to the self.disconnect() call in the reconnect
branch of _connection_handler().
Fixed: ``Engine.dispose()`` deadlock when called from a signal handler
When user code registered a signal.signal() handler that called
engine.dispose() while engine.start() was blocking on
loop.run_until_complete(self._start()), dispose() would attempt another
loop.run_until_complete() on the already-running event loop, raising
RuntimeError: This event loop is already running.
Fix: dispose() now checks self._loop.is_running() first. If the
loop is active, it schedules task_manager._shutdown_event.set() via
loop.call_soon_threadsafe() and returns immediately. This allows
_start() → task_manager.wait() to unblock naturally, and start()’s
finally block performs the actual disposal after the loop has stopped.
For users who override signal handling, the recommended pattern is:
def _signal_handler(self, signum, frame):
self.engine.dispose() # now safe — returns immediately when loop is running
engine.start() # blocks until shutdown_event is set
# dispose() runs in start()'s finally block after the loop stops
0.3.16¶
Fixed: WebSocket disconnect not properly awaited
WSClient.disconnect() was a synchronous method. Callers that held the
result of disconnect() without await would silently drop the coroutine,
leaving the underlying transport open and the associated asyncio tasks running
until the event loop was forcibly closed.
Symptom: After engine.dispose(), stray Task was destroyed but it is
pending warnings appeared in the log. On some runs the process hung for
several seconds after the strategy finished.
Fix: WSClient.disconnect() is now async. It saves a local
reference to the transport before clearing self._transport, calls
transport.disconnect(), then await asyncio.wait_for(transport.wait_disconnected(), timeout=3.0)
to confirm the connection is fully closed before firing the on_disconnected
hook. A 3-second timeout prevents indefinite blocking on a stuck transport.
All call sites updated accordingly:
PublicConnector.disconnect()—await self._ws_client.disconnect()(previously the call was commented out as “not needed”).PrivateConnector.disconnect()—await self._oms._ws_client.disconnect().OkxPublicConnector.disconnect()—await self._business_ws_client.disconnect().bybit_tradfi._NullWsClientStub.disconnect()— promoted toasync defto satisfy the awaitable contract.
Fixed: engine shutdown leaves pending tasks uncollected
Engine._close_event_loop() cancelled all remaining asyncio tasks but never
awaited them, so their CancelledError handlers never ran and the event loop
could not close cleanly.
Fix: After cancelling every task, the engine now calls
loop.run_until_complete(asyncio.gather(*pending_tasks, return_exceptions=True))
to drain them before loop.close(). The post-disconnect sleep was also
increased from 0.2 s to 0.5 s to give the WS transports time to finish their
handshake before the task sweep begins.
Fixed: Binance historical kline fetch called outside async context
BinancePublicConnector._get_index_price_klines() and
_get_historical_klines() assigned the raw coroutine returned by the REST
client to klines_response without running it, resulting in a
coroutine was never awaited warning and an empty response.
Fix: Both calls are now wrapped with self._run_sync(), consistent with
every other exchange (OKX, Bybit, Bitget) that already used _run_sync for
the same pattern.
0.3.15¶
Fixed: reconnect resync deadlock (event-loop freeze)
After a private WebSocket disconnected and reconnected, the post-reconnect
resync task called the synchronous _init_account_balance() /
_init_position() helpers. These helpers internally use
asyncio.run_coroutine_threadsafe(coro, loop).result(). When called from
a coroutine that is already running inside the event loop (the resync is
launched via create_task), .result() blocks the event-loop thread
while waiting for a future that can only be resolved by that same loop —
a classic deadlock.
Symptom: The resynced event never appeared after a reconnect, and
the entire event loop froze — no market data, no order callbacks, nothing.
Fix: A new _async_resync_init() helper in the base OMS offloads both
sync inits to a thread-pool executor via asyncio.run_in_executor(). The
executor thread blocks on .result() while the event loop remains free to
drive the submitted REST coroutines. Applied to OKX, Binance, Bybit, Bitget,
and HyperLiquid.
Fixed: spurious ``on_accepted_order`` callbacks after reconnect
After reconnect, _resync_after_reconnect() fetches all current open orders
via REST and passes them to order_status_update(). Because the
ACCEPTED → ACCEPTED state transition is deliberately allowed (to support
modify-order flows), every already-open order triggered a fresh
on_accepted_order callback — even though nothing had changed.
Symptom: cnt_accepted was higher than cnt_submitted in tests. In
production strategies, any logic placed in on_accepted_order (hedging,
position tracking, risk checks) would be re-executed for every open order
after every reconnect.
Fix: order_status_update() gains an optional silent=True parameter.
When set, the local cache is updated but no strategy callbacks are dispatched.
_resync_after_reconnect() in OKX, Binance, and Bybit now compares the
fetched status against the cached status and passes silent=True when they
are identical. Status transitions that represent a genuine change
(PENDING → ACCEPTED, ACCEPTED → FILLED, etc.) continue to fire the
corresponding callbacks as before.
0.3.14¶
New: order-create idempotency controls
CreateOrderSubmit now includes an optional idempotency_key and
Strategy.create_order() / create_order_ws() accept both
client_oid and idempotency_key. AsyncCache keeps a canonical
OID per idempotency key, so repeated strategy signals can safely reuse the
same logical order instead of generating duplicate submissions.
New: explicit WebSocket ACK error types
Added WsRequestNotSentError, WsAckTimeoutError, and
WsAckRejectedError to distinguish between three failure modes:
the request never left because the WS API socket was down,
the request was sent but the exchange never acknowledged it in time,
the exchange explicitly rejected the WS order/cancel request.
New: ``WsOrderResultType`` structured result enum
A new WsOrderResultType enum (REQUEST_NOT_SENT, ACK_REJECTED,
ACK_TIMEOUT, ACK_TIMEOUT_CONFIRMED) is now returned by
create_order_ws() and cancel_order_ws() and is also published via
the new on_ws_order_request_result() strategy callback, giving
strategies a structured way to inspect WS ACK outcomes without catching
exceptions.
New: ``on_ws_order_request_result()`` strategy callback
A new overrideable Strategy method fires whenever a background WS
order or cancel task produces a structured ACK outcome:
def on_ws_order_request_result(self, result: dict):
# result keys: oid, symbol, exchange, result_type, reason, timestamp
if result["result_type"] == WsOrderResultType.ACK_TIMEOUT:
self.log.warning(f"WS ACK timeout for {result['oid']}: {result['reason']}")
This replaces the need to catch WsAckTimeoutError / WsAckRejectedError
inside background tasks manually.
Changed: WS ACK handling is now consistent across exchanges
OKX, Binance, Bybit, Bitget, and HyperLiquid now register a pending ACK future for each WS order or cancel request, reject all pending waiters when the WS API disconnects, wait up to 5 seconds for an ACK, and then attempt a REST confirmation before raising an ACK-timeout error. This closes the gap between “request sent” and “exchange state known” during transient WS issues.
Changed: Bitget and HyperLiquid gain WS fallback parity
create_order_ws() and cancel_order_ws() on Bitget and HyperLiquid now
support the same ws_fallback=True behavior already available on OKX,
Binance, and Bybit. If the WS send fails immediately, the OMS can retry via
REST instead of failing the order path outright.
Changed: duplicate create submissions are rejected earlier
Base EMS now suppresses repeated create requests when the order OID is
already inflight, already registered, or already present in cache. The
HyperLiquid EMS applies the same protection after converting the strategy OID
into the exchange cloid representation.
New: Bitget and HyperLiquid open-order REST recovery
Bitget now provides the REST wrappers needed to query pending UTA, futures,
and spot orders, while HyperLiquid now exposes get_open_orders() from the
authenticated REST client. Both OMS implementations now use these helpers in
fetch_order(), fetch_open_orders(), and reconnect resync flows.
Fixed: pending ACK waiters no longer hang on disconnect
When a WS API connection drops mid-request, any coroutines still awaiting an
ACK are now failed immediately with WsRequestNotSentError instead of being
left pending in memory.
Changed: ``fetch_order()`` gains a ``force_refresh`` parameter
Strategy.fetch_order() and all exchange OMS fetch_order()
implementations now accept force_refresh=True to skip the local cache
and query the exchange REST API directly. This is used internally by
ACK-timeout recovery paths and can be called from strategy code when
authoritative exchange state is required:
order = self.fetch_order(symbol, oid, force_refresh=True)
Fixed: duplicate-submit and ACK-recovery regressions are covered by tests
Three new regression suites were added:
test/test_order_idempotency.pycovers canonical OID reuse and duplicate create suppression in strategy and EMS paths.test/test_ws_ack.pycovers not-sent requests, ACK timeout with REST confirmation, explicit rejection, disconnect cleanup, and fallback behavior.test/test_ack_and_reconcile.pycovers reconnect reconciliation logic, pending ACK rejection on disconnect, and REST-confirmation after timeout across multiple exchanges.
0.3.13¶
New: WebSocket lifecycle hooks
WSClient now exposes set_lifecycle_hooks(on_connected, on_disconnected,
on_reconnected). Hooks are fired automatically on each connection state
change and can be sync or async callables. The OMS registers hooks at startup
and publishes private_ws_status and private_ws_resync_diff events to
the message bus so strategies can react via the new on_private_ws_status()
and on_private_ws_resync_diff() callbacks.
New: automatic reconnect order reconciliation
After a private WebSocket reconnects, the OMS re-fetches balances, positions,
and open orders, then emits a diff summary containing positions opened/closed
and orders added/removed. OKX, Binance, and Bybit each have exchange-specific
_resync_after_reconnect() overrides with a configurable grace window
(set_reconnect_reconcile_grace_ms()) that prevents false order closures
from delayed snapshots.
New: ``ws_fallback`` parameter on WS order methods
create_order_ws() and cancel_order_ws() across OKX, Binance, and Bybit
now accept a ws_fallback=True keyword argument. When the WebSocket send
fails due to a ConnectionError the call automatically retries via REST
(default) or marks the order as FAILED / CANCEL_FAILED immediately
when ws_fallback=False.
New: ``fetch_order``, ``fetch_open_orders``, ``fetch_recent_trades``
New OMS methods for querying live order state via REST, also exposed directly
on Strategy:
order = self.fetch_order(symbol, oid)
open_orders = self.fetch_open_orders(symbol)
recent = self.fetch_recent_trades(symbol, limit=50)
New: OKX ``get_api_v5_trade_orders_pending``
New REST endpoint wrapper for retrieving pending open orders used internally by the OKX reconnect reconciliation.
New: Binance fetch-order REST wrappers
Added get_api_v3_order, get_fapi_v1_order, get_dapi_v1_order,
get_api_v3_open_orders, get_fapi_v1_open_orders, and
get_dapi_v1_open_orders to the Binance REST client.
Fixed: inactive symbol warnings across all exchanges
load_markets() in OKX, Binance, Bybit, and Bitget now skips instruments
where active=False before attempting to parse them (OKX additionally
skips info.state='preopen'). This eliminates the repeated
Symbol Format Error: Expected float, got null warnings that appeared for
delisted or pre-launch instruments whose precision fields are all null.
Fixed: Bitget WS API silent order drop
BitgetWSApiClient._submit() and _uta_submit() now call
_send_or_raise() instead of _send(), so a disconnected socket raises
ConnectionError rather than silently dropping the order request. This
matches the same fix already applied to OKX, Binance, and Bybit in the same
release.
Fixed: test ``nautilus_trader`` import
test/base/__init__.py, test/base/conftest.py, and
test/core/conftest.py now import TraderId from
nexustrader.core.nautilius_core instead of the removed
nautilus_trader package, so all test suites collect correctly on Windows.
Changed: ``WSClient._send()`` return value
_send() now returns bool (True on success, False when not
connected) and a new _send_or_raise() helper raises ConnectionError
when the socket is unavailable, used internally by the WS API clients.
Changed: ``uv.toml`` link-mode
Added link-mode = "copy" to suppress hardlink warnings when the uv cache
and the project .venv are on different drives.
0.3.12¶
Added: ``pandas`` dependency
pandas>=3.0.1 is now a required dependency of NexusTrader.
Changed: ``MetaTrader5`` is now optional
The MetaTrader5 package is declared under [project.optional-dependencies]
(tradfi group, Windows only) instead of a hard dependency. Installing
NexusTrader on Linux or macOS no longer fails because of a platform-incompatible
package.
Fixed: non-Windows platform error handling
_check_platform() in _mt5_bridge.py now raises SystemExit instead of
RuntimeError when not running on Windows. The error message is clearer and
the process exits cleanly without printing a traceback.
Fixed: ``ImportError`` on non-Windows during MT5 init
BybitTradeFiPrivateConnector.connect() now catches ImportError from the
mt5_initialize executor call and converts it into a SystemExit with an
actionable message, avoiding a confusing raw ImportError traceback.
Fixed: premature ``disconnect()`` crash
A new _mt5_connected boolean flag on BybitTradeFiPrivateConnector prevents
disconnect() from attempting an MT5 shutdown when the connection was never
successfully established, avoiding a crash during engine teardown after a failed
connect().
Fixed: synchronous OMS initialisation in constructor
BybitTradeFiOrderManagementSystem.__init__ no longer calls the synchronous
_init_account_balance() and _init_position() methods at construction time.
These are now deferred to the async variants invoked inside
BybitTradeFiPrivateConnector.connect(), keeping the constructor free of
blocking I/O.
Changed: demo strategy platform tips
All Bybit TradFi demo strategies (demo_market_data.py, demo_multi_symbol.py,
demo_trading.py, xau_arb_market_data.py) now include a top-of-file comment
informing users that MetaTrader 5 requires Windows.
0.3.11¶
New: Bybit TradFi — Traditional Financial Markets via MetaTrader 5
NexusTrader now supports traditional financial markets (Forex, Gold, Indices, Stocks) through the Bybit TradFi brokerage, which uses a MetaTrader 5 terminal as the execution backend.
Key additions:
BybitTradeFiPublicConnector— polling-based market data (BookL1, Trade, Kline, historical klines) via the MT5 Python API. No WebSocket required.BybitTradeFiPrivateConnector— terminal initialisation, login, broker connectivity check, market loading, and order lifecycle management.BybitTradeFiOrderManagementSystem— market orders, limit/pending orders, cancel, and polling-based status updates mapped to standard NexusTrader order states.ExchangeType.BYBIT_TRADFIandBybitTradeFiAccountType(DEMO/LIVE).Symbol naming: internal dots in MT5 names are replaced with underscores (
XAUUSD.s→XAUUSD_s.BYBIT_TRADFI,TSLA.s→TSLA_s.BYBIT_TRADFI).Demo strategies in
strategy/bybit_tradfi/.
Fixed: stdout log buffering
setup_nautilus_core now defaults batch_size=1 for nexuslog so that
log messages appear immediately instead of being buffered until 32 entries
accumulate. Previously this caused complete silence while waiting for blocking
operations such as mt5.initialize().
Fixed: ``request_klines`` deadlock
Synchronous data-request helpers (request_klines, request_ticker,
request_all_tickers) now submit work directly to the ThreadPoolExecutor
instead of going through task_manager.run_sync(), which blocked the event
loop thread while waiting for a coroutine scheduled on that same loop.
Fixed: Kline over-emission
Kline polling previously emitted the current unconfirmed bar on every poll cycle (every 0.5 s). It now only emits on bar open, on close-price change within the bar, and on bar close.
0.3.10¶
Performance: WebSocket startup ~4-5 s (was ~12 s)
Private-connector startup was dominated by fixed asyncio.sleep(5) calls
used as a safety margin after sending WebSocket auth payloads. Each exchange
had at least two sequential sleeps (WS API client + private WS client),
totalling ~10-12 seconds of idle waiting before the engine was ready.
Two complementary optimisations eliminate almost all of that overhead:
Event-driven auth completion —
asyncio.sleep(5)is replaced by anasyncio.Eventthat fires as soon as the exchange acknowledges the auth request. A 5-second timeout is kept as a safety fallback. Each exchange OMS now detects the auth/login response and callsnotify_auth_success()on the corresponding WS client.Parallel WS connection — Bybit, OKX, and Bitget private connectors now connect and authenticate both the WS API client and the private WS client concurrently via
asyncio.gather(). Binance non-spot accounts similarly parallelise the WS API connection and the REST listen-key request.
Affected exchanges: Binance, Bybit, OKX, Bitget.
0.3.9¶
Fixed: Binance startup crash on position mode check
The four REST API calls in Binance _position_mode_check were not wrapped
with _run_sync() after the sync-to-async API client migration in v0.3.5.
This caused a 'coroutine' object is not subscriptable error on startup for
Binance linear, inverse, and portfolio margin accounts.
0.3.8¶
Fixed: ``Order.reason`` now populated on failure
All exchange OMS implementations (Binance, Bybit, OKX, Bitget, HyperLiquid)
now set the Order.reason field when creating FAILED or CANCEL_FAILED
orders. Previously the error message was only logged and then discarded —
strategies receiving on_failed_order / on_cancel_failed_order callbacks
always saw order.reason = None.
The field is populated from the exchange error response across all failure paths: REST exceptions, WebSocket API errors, and batch order individual failures.
def on_failed_order(self, order: Order):
self.log.error(f"Order {order.oid} failed: {order.reason}")
def on_cancel_failed_order(self, order: Order):
self.log.error(f"Cancel {order.oid} failed: {order.reason}")
0.3.7¶
Performance: ~70 ms cold import (was several seconds)
The nautilus-trader dependency has been removed. It was the dominant source of
slow startup because of its Rust/Cython initialisation overhead. All functionality
is now provided by pure-Python code and the lightweight nexuslog package.
What changed
nautilus-traderis no longer installed. A Rust toolchain orbuild-essentialis no longer required on any platform.nexuslog(>=0.4.0) is the new logging backend.All previously Rust-backed components are now pure Python:
MessageBus— pub/sub and point-to-point endpoint routing.LiveClock— wall-clock time, asyncio-based repeating timers.TimeEvent— timer event dataclass (ts_event,ts_initin nanoseconds).hmac_signature,rsa_signature,ed25519_signature— crypto signing helpers.TraderId,UUID4— identifier utilities.
LogColorinnexustrader.constantsis now a plain PythonEnumwith the same attribute names (NORMAL,GREEN,BLUE,MAGENTA,CYAN,YELLOW,RED).
Migration — zero changes required for most users
Existing strategy code is fully compatible:
self.log.info(msg, color=LogColor.BLUE)continues to work unchanged.from nexustrader.constants import LogColorcontinues to work unchanged.LogConfigand allEngine/ConfigAPIs are unchanged.
The only internal breaking change is that setup_nautilus_core() now returns
(msgbus, clock) instead of the former three-tuple (log_guard, msgbus, clock).
This affects only code that calls setup_nautilus_core directly (not typical
strategy code).
0.3.6¶
Improvements
Lazy credential validation: Importing
nexustraderno longer crashes withFileNotFoundErrorwhen.keys/.secrets.tomlis missing. A warning is emitted instead, allowing public-only, mock, and backtest workflows to run without any credential file.Multi-source credential resolution:
BasicConfignow supports three credential sources in priority order:Direct pass (highest priority) — existing behaviour, fully backward-compatible:
BasicConfig(api_key="xxx", secret="yyy", testnet=True)
Settings auto-resolve via the new
settings_keyparameter — reads from.keys/.secrets.tomlorNEXUS_prefixed environment variables:
# Resolves from [BINANCE.DEMO] in .secrets.toml # or from NEXUS_BINANCE__DEMO__API_KEY / NEXUS_BINANCE__DEMO__SECRET env vars BasicConfig(settings_key="BINANCE.DEMO", testnet=True)
Plain environment variables via the new
from_env()classmethod:
# Reads BINANCE_API_KEY, BINANCE_SECRET, BINANCE_PASSPHRASE BasicConfig.from_env("BINANCE", testnet=True) # Custom variable names BasicConfig.from_env("X", api_key_var="MY_KEY", secret_var="MY_SECRET")
All three methods can be combined — directly passed values always take precedence over auto-resolved values.
Fixed
Fixed
BasicConfig.passphrasetype annotation fromstr = Nonetostr | None = None.
0.3.5¶
Breaking Changes
Order identifier renamed — ``oid`` / ``eid``: The internal order identifier previously exposed as
order.idororder.uuidis noworder.oid(Order ID). The exchange-assigned identifier is noworder.eid(Exchange ID). Update all strategy and handler code that references these fields:# Before self.log.info(f"filled: {order.uuid}") self.cancel_order(symbol=symbol, uuid=my_uuid) # After self.log.info(f"filled: {order.oid}") self.cancel_order(symbol=symbol, oid=my_oid)
``OrderRegistry`` simplified: The registry no longer maintains a bidirectional UUID↔ORDER_ID mapping. It now tracks active OIDs with a flat API:
register_order(oid),is_registered(oid),unregister_order(oid),register_tmp_order(order),get_tmp_order(oid),unregister_tmp_order(oid). Direct lookup from OID → EID or EID → OID is no longer available through the registry; usecache.get_order(oid)to access full order objects.``AsyncCache`` constructor: The
registry=keyword argument has been removed. Aclock: LiveClockargument is now required. If you instantiateAsyncCachedirectly (e.g. in tests or custom components), update the call:# Before cache = AsyncCache(msgbus=msgbus, registry=registry, ...) # After from nexustrader.core.nautilius_core import LiveClock cache = AsyncCache(msgbus=msgbus, clock=LiveClock(), ...)
Improvements
Sync/async REST API unification: Exchange REST API clients now expose a unified interface. Async REST methods are transparently callable in a synchronous context — the
__getattr__wrapper auto-detects async methods and dispatches them viarun_sync()using the running event loop. No manualasyncio.run()wrappers are needed.Internal ``_order_status_update()``: A single unified method now covers the full order lifecycle internally, replacing the former
_order_initialized()entry point.
Fixed
Fixed an
AttributeErrorinBaseConnectorwhere newly createdOrderobjects used the removedid=field instead ofoid=, causing order tracking to fail silently.
0.3.4¶
Changed
Simplified Cache API:
cache.get_position()andcache.get_order()now returnOptional[T]directly instead of aMaybemonad. Replace.value_or(None)with a directNonecheck, and.bind_optional(lambda o: o.field).value_or(False)witho.field if o else False.ZeroMQ is now an optional dependency: The
zmqpackage is no longer installed by default. Users who rely onZeroMQSignalConfigmust install the extras:pip install nexustrader[signal].Windows: signal handler warning suppressed: The unsupported
asynciosignal handler on Windows no longer emits aUserWarning; it is now silently logged at DEBUG level.
Removed
``returns`` library removed: Replaced with standard
Optional[T]return types — no external dependency needed.Dead production dependencies removed:
streamz,pathlib(Python stdlib),bcrypt,cython, andcertifihad zero usage and have been removed, reducing the install footprint.``pyinstrument`` moved to dev-only: The profiling tool is no longer pulled in for regular users.
0.3.3¶
New Features & Improvements
Batched WebSocket subscriptions: Both initial subscriptions and reconnection resubscriptions now process symbols in batches of 50 with a short delay between batches, enabling reliable support for thousands of symbols without overloading the connection.
Binance Spot: migrated to signature-based user data stream: Replaced the deprecated
listenKeymechanism with the newuserDataStream.subscribe.signatureWebSocket API for Binance Spot accounts (effective since 2026-02-20). Futures, Margin, and Portfolio Margin accounts continue to use the existinglistenKeyflow.OKX: ``instIdCode`` support for WebSocket order operations: Adapted to OKX’s upcoming parameter migration from
instIdtoinstIdCodein WebSocket order and cancel-order requests (Phase 1: 2026-03-26, Phase 2: 2026-03-31). The system usesinstIdCodewhen available and falls back toinstIdfor backward compatibility.Inflight order tracking: Orders that have been submitted to the exchange but not yet acknowledged are now tracked per symbol via
cache.get_inflight_orders()andcache.wait_for_inflight_orders(). This prevents race conditions when rapidly submitting and cancelling orders.Synchronous cancel-intent marking:
cancel_order,cancel_order_ws, andcancel_all_ordersnow mark cancel intent synchronously at the Strategy layer, eliminating a window where the local order state could conflict with incoming exchange updates.Order ``reason`` field: The
Orderstruct now carries an optionalreasonfield to capture human-readable failure context (e.g. exchange error messages) forFAILEDandCANCEL_FAILEDorders.OMS null-OID guard:
order_status_updategracefully skips orders withoid=None(e.g. exchange-initiated liquidations), preventingKeyErrorcrashes.RetryManager utility: A generic retry helper with exponential backoff and jitter is now available at
nexustrader.base.retry.RetryManagerfor resilient asynchronous operations.
0.3.1¶
New Features & Improvements
Windows support: NexusTrader now runs natively on Windows. On Windows,
uvloopis automatically skipped and the standardasyncioevent loop is used instead.