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.36

Fixed: cache TTL cleanup handles missing order timestamps safely

AsyncCache._cleanup_expired_data() now skips orders whose optional timestamp is None instead of raising TypeError and stopping the periodic cache sync task.

Fixed: persistent open-order snapshots are synced after TTL cleanup

The periodic cache sync now writes order history first, evicts expired in-memory orders, and then syncs open-order indexes. This keeps expired non-terminal orders in order history while preventing stale open-order rows from surviving for an extra sync interval.

0.3.35

Fixed: order cache TTL cleanup clears stale open-order indexes

When an expired non-terminal order is evicted from AsyncCache, NexusTrader now removes that order id from open-order, symbol-open, cancel-intent, inflight, and symbol-history indexes. This prevents TWAP child orders that missed terminal callbacks from remaining visible as open after the order object itself has expired.

Cache cleanup now also emits one summary warning per cleanup cycle instead of one warning per expired order, reducing log floods such as repeated AsyncCache: order ... is not closed, but expired 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 authoritative REST position snapshot during startup and reconnect resync. This prevents a previously open local cache entry from surviving after the exchange reports the position as flat or omits it from the active positions response.

Binance REST position application now also matches the WebSocket account update path by always writing decoded positions through AsyncCache._apply_position(), so positionAmt=0 removes stale cached positions instead of being skipped.

0.3.33

Fixed: subscription refresh operations preserve caller order

SubscriptionManagementSystem now uses one FIFO operation queue for subscribe and unsubscribe requests. Back-to-back refresh flows such as unsubscribe_bookl2() followed by subscribe_bookl2() now execute in that exact order, preventing the subscribe path from no-oping on an existing topic before the unsubscribe path removes the real exchange subscription.

0.3.32

Fixed: fast-closing create ACKs are reconciled through REST

Binance, Bitget, Bybit, OKX, and HyperLiquid now schedule short REST fetch_order(..., force_refresh=True) reconciliation after successful REST or WebSocket create ACKs for orders that should reach a terminal state quickly: MARKET orders and IOC / FOK limit orders. If the private order stream misses or delays the terminal callback, NexusTrader writes back the real FILLED / CANCELED / EXPIRED state instead of leaving the order in PENDING or ACCEPTED until cache expiry.

The reconciliation is deduplicated per order id, skips already closed orders, and only dispatches callbacks when the REST result is meaningfully different from the cached order state.

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. OMS implementations use that public API instead of calling 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 post-ACK terminal reconciliation helper for fast-closing orders. Bitget batch create remains unimplemented, and Bybit TradFi continues to use the MT5 synchronous / polling model.

0.3.31

Fixed: Binance cancel ACK terminal states are applied immediately

Binance WebSocket cancel responses now parse the returned order fields, including status, origQty, executedQty, price, avgPrice, side/type metadata, and update timestamps. When Binance returns a terminal state such as CANCELED or FILLED in the cancel ACK, NexusTrader writes and dispatches that real state immediately instead of first marking the order CANCELING and waiting for a private user-data event.

Fixed: cancel success paths use short REST reconciliation across exchanges

Binance, Bitget, Bybit, OKX, and HyperLiquid now schedule a short delayed REST fetch_order(..., force_refresh=True) after successful cancel ACKs that still leave the local order in CANCELING. If the private order stream misses or delays the terminal event, the REST result quickly writes back the true FILLED / CANCELED / EXPIRED state and dispatches the corresponding callback. This closes the blind window where a maker fill could remain unpaired until a long strategy-level timeout.

Fixed: Binance user-data stream expiry is recovered explicitly

Binance listenKeyExpired events now trigger user-data stream recovery and a private reconnect resync. Binance private connector health also reports the latest order and account update timestamps, making stale or half-dead private streams easier for strategies and operators to detect.

0.3.30

Fixed: Bybit server-time sync uses curl_cffi response attributes

Bybit signed REST requests call _sync_time_if_needed() before generating request timestamps. That method now reads response.content and response.status_code from curl_cffi.requests.AsyncSession responses instead of the aiohttp-style await response.read() and response.status. This prevents repeated 'Response' object has no attribute 'read' warnings before signed Bybit REST calls such as order lookup, ACK timeout confirmation, cancel confirmation, and order status checks.

Fixed: failed Bybit time sync no longer logs on every signed request

Server-time sync failures now set a short 5 second retry backoff. Short network or exchange-side failures therefore do not produce a warning for every signed request, while successful syncs still use the normal 60 second interval.

Fixed: cancel ``Unknown order`` / not-found errors trigger REST reconciliation

Binance, Bitget, Bybit, OKX, and HyperLiquid now immediately fetch the order via REST before reporting cancel failure when WebSocket cancel returns an unknown/not-found/already-canceled/already-filled style error. If REST confirms the order is already closed, the real FILLED / CANCELED / EXPIRED state is written back and the cancel ACK wait is resolved. This reduces the blind window where a strategy waits for its own long timeout before discovering that the maker order already filled.

0.3.29

Fixed: OKX import dependency is declared

nexustrader.exchange.okx.exchange imports orjson while nexustrader.engine imports the exchange package. Version 0.3.28 did not declare orjson in the base dependencies, so a fresh environment could fail while importing nexustrader.engine. Downstream code that caught broad Exception values could then report the misleading message nexustrader not available even though the real failure was the missing orjson dependency.

orjson is now declared in both package metadata and requirements.txt.

0.3.28

Fixed: Linux/macOS base installs no longer require MetaTrader5

MetaTrader5 is a Windows-only package. NexusTrader no longer declares it as a default dependency, so pip install nexustrader and normal Linux/macOS source installs do not try to resolve the Windows-only wheel. TradFi users on Windows should install the extra instead:

pip install "nexustrader[tradfi]"

or:

uv add "nexustrader[tradfi]"

The lockfile now mirrors this behavior: MetaTrader5 remains available only for the tradfi extra on Windows.

Fixed: Linux installation paths no longer pull stale heavy dependencies

requirements.txt has been synchronized with the current production dependency set. Removed legacy entries include nautilus-trader, streamz, pathlib, cython, and other packages that were no longer part of the supported runtime path and could break or slow Linux installs.

Fixed: Docker build uses the current project

The Dockerfile now installs NexusTrader from the local build context instead of cloning an external private repository. A .dockerignore file was added to exclude virtual environments, git metadata, caches, keys, and generated artifacts from the image.

0.3.27

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.

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_type

Price sent to MT5

(not set)

The price value supplied by the strategy (unchanged behaviour)

"bid"

Latest best bid, re-fetched at submission time

"ask"

Latest best ask, re-fetched at submission time

"opponent"

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 to async def to 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.py covers canonical OID reuse and duplicate create suppression in strategy and EMS paths.

  • test/test_ws_ack.py covers not-sent requests, ACK timeout with REST confirmation, explicit rejection, disconnect cleanup, and fallback behavior.

  • test/test_ack_and_reconcile.py covers 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_TRADFI and BybitTradeFiAccountType (DEMO / LIVE).

  • Symbol naming: internal dots in MT5 names are replaced with underscores (XAUUSD.sXAUUSD_s.BYBIT_TRADFI, TSLA.sTSLA_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:

  1. Event-driven auth completionasyncio.sleep(5) is replaced by an asyncio.Event that 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 calls notify_auth_success() on the corresponding WS client.

  2. 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-trader is no longer installed. A Rust toolchain or build-essential is 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_init in nanoseconds).

    • hmac_signature, rsa_signature, ed25519_signature — crypto signing helpers.

    • TraderId, UUID4 — identifier utilities.

  • LogColor in nexustrader.constants is now a plain Python Enum with 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 LogColor continues to work unchanged.

  • LogConfig and all Engine / Config APIs 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 nexustrader no longer crashes with FileNotFoundError when .keys/.secrets.toml is missing. A warning is emitted instead, allowing public-only, mock, and backtest workflows to run without any credential file.

  • Multi-source credential resolution: BasicConfig now supports three credential sources in priority order:

    1. Direct pass (highest priority) — existing behaviour, fully backward-compatible:

    BasicConfig(api_key="xxx", secret="yyy", testnet=True)
    
    1. Settings auto-resolve via the new settings_key parameter — reads from .keys/.secrets.toml or NEXUS_ 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)
    
    1. 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.passphrase type annotation from str = None to str | None = None.

0.3.5

Breaking Changes

  • Order identifier renamed — ``oid`` / ``eid``: The internal order identifier previously exposed as order.id or order.uuid is now order.oid (Order ID). The exchange-assigned identifier is now order.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; use cache.get_order(oid) to access full order objects.

  • ``AsyncCache`` constructor: The registry= keyword argument has been removed. A clock: LiveClock argument is now required. If you instantiate AsyncCache directly (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 via run_sync() using the running event loop. No manual asyncio.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 AttributeError in BaseConnector where newly created Order objects used the removed id= field instead of oid=, causing order tracking to fail silently.

0.3.4

Changed

  • Simplified Cache API: cache.get_position() and cache.get_order() now return Optional[T] directly instead of a Maybe monad. Replace .value_or(None) with a direct None check, and .bind_optional(lambda o: o.field).value_or(False) with o.field if o else False.

  • ZeroMQ is now an optional dependency: The zmq package is no longer installed by default. Users who rely on ZeroMQSignalConfig must install the extras: pip install nexustrader[signal].

  • Windows: signal handler warning suppressed: The unsupported asyncio signal handler on Windows no longer emits a UserWarning; 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, and certifi had 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 listenKey mechanism with the new userDataStream.subscribe.signature WebSocket API for Binance Spot accounts (effective since 2026-02-20). Futures, Margin, and Portfolio Margin accounts continue to use the existing listenKey flow.

  • OKX: ``instIdCode`` support for WebSocket order operations: Adapted to OKX’s upcoming parameter migration from instId to instIdCode in WebSocket order and cancel-order requests (Phase 1: 2026-03-26, Phase 2: 2026-03-31). The system uses instIdCode when available and falls back to instId for 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() and cache.wait_for_inflight_orders(). This prevents race conditions when rapidly submitting and cancelling orders.

  • Synchronous cancel-intent marking: cancel_order, cancel_order_ws, and cancel_all_orders now 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 Order struct now carries an optional reason field to capture human-readable failure context (e.g. exchange error messages) for FAILED and CANCEL_FAILED orders.

  • OMS null-OID guard: order_status_update gracefully skips orders with oid=None (e.g. exchange-initiated liquidations), preventing KeyError crashes.

  • RetryManager utility: A generic retry helper with exponential backoff and jitter is now available at nexustrader.base.retry.RetryManager for resilient asynchronous operations.

0.3.1

New Features & Improvements

  • Windows support: NexusTrader now runs natively on Windows. On Windows, uvloop is automatically skipped and the standard asyncio event loop is used instead.