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: .. code-block:: bash pip install "nexustrader[tradfi]" or: .. code-block:: bash 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: .. code-block:: python 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: .. code-block:: python # 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: .. list-table:: :header-rows: 1 :widths: 20 80 * - ``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 .. code-block:: python # 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.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: 1. **Event-driven auth completion** — ``asyncio.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. .. code-block:: python 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: .. code-block:: python BasicConfig(api_key="xxx", secret="yyy", testnet=True) 2. **Settings auto-resolve** via the new ``settings_key`` parameter — reads from ``.keys/.secrets.toml`` or ``NEXUS_`` prefixed environment variables: .. code-block:: python # 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) 3. **Plain environment variables** via the new ``from_env()`` classmethod: .. code-block:: python # 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: .. code-block:: python # 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: .. code-block:: python # 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.