#13569·freqtrade

Hyperliquid: websocket watch tasks die with UnsubscribeError - no candles ever arrive over WS, REST fallback and heavy log spam

Author: PitbullDigitalCreated Sep 14, 2026Updated Sep 16, 2026
LabelsCCXTAI Slop

Environment

  • freqtrade 2026.8 (freqtradeorg/freqtrade:stable image), ccxt 4.5.76
  • trading_mode: futures, margin_mode: isolated, dry_run: true
  • exchange hyperliquid (USDC), Hyperliquid strategy NostalgiaForInfinityX7, 57-pair volume pairlist
  • public market data only (no walletAddress / privateKey needed for a dry run)

What happens

The websocket never delivers a candle for any pair — for a ~10.5 h run:

  • 0 occurrences of watch done in the log (i.e. no WS data consumed at all)
  • ~3,000 errors with tracebacks:
ERROR - Exception in continuously_async_watch_ohlcv for XRP/USDC:USDC, 5m
Traceback (most recent call last):
  ...
  File "ccxt/pro/hyperliquid.py", line ..., in watch_ohlcv
    return await self.watch(url, messageHash, message, messageHash)
ccxt.base.errors.UnsubscribeError: hyperliquid candles:5m:XRP/USDC:USDC
  • ~2,190 Task finished - done lines for the same run — watch tasks ending and being re-scheduled continuously
  • Couldn't reuse watch ... falling back to REST api and Outdated history warnings
  • RateLimitExceeded ... 429 Too Many Requests warnings from the REST API

Effect: because all candle data falls back to REST, data ends up 10–20 minutes stale (5m data refreshing every 5–20 min instead of every 5), and the exchange's per-IP rate limit is hit.

Cause

This is a ccxt issue, reproduced outside of freqtrade: unsubscribing and re-subscribing the same pair/timeframe before the unsubscribe ack arrives makes the ack reject the newly armed subscription future.

1 control: fresh subscribe       OK                 1 candle(s)
2 control: re-subscribe (acked)  OK                 1 candle(s)
3 bug: re-subscribe (race)       UnsubscribeError   hyperliquid candles:5m:XRP/USDC:USDC
4 recovery: subscribe again      OK                 1 candle(s)

freqtrade produces the failing sequence routinely: cleanup_expired() drops a pair → the watch task ends → _continuous_stopped fires _unwatch_ohlcv() → and the pair is re-scheduled (schedule_ohlcv) shortly after. The ack for that unsubscribe then rejects the future of the new subscription, so the new watch task dies with ccxt.UnsubscribeError.

The death is what makes it self-sustaining: _continuous_stopped fires another unwatch for the dead task, whose ack rejects the next subscription, and so on — hence the thousands of errors and the complete absence of WS data.

Why this is not #12739

#12739 shows the same Couldn't reuse watch lines but was resolved as a config problem (privateKey vs privatekey) plus Outdated history on illiquid pairs. Here the websocket never delivers for any pair (including BTC), the errors are continuous rather than per-pair, and the failure reproduces without freqtrade in the loop (reproducer above).

Local workaround (not a proposed freqtrade fix)

We patched ccxt's un_watch_ohlcv() to a no-op for hyperliquid locally, so the ack that poisons the next subscription never happens. Result after the change: 0 errors, 0 tracebacks, the WS delivers (measured 114 consumptions per cycle on 57 pairs × 5 timeframes), no rate-limit warnings, live 5m data. It is a workaround only — subscriptions are not released — so it is not proposed upstream.

Request

Harden the watch loop so a single UnsubscribeError cannot end the stream: _continuously_async_watch_ohlcv() currently catches ccxt.BaseError, logs a traceback and terminates the task; terminating triggers another unwatch, which feeds the loop described above.

After an UnsubscribeError, ccxt has already cleaned up its state (subscription marker deleted, future rejected), so simply subscribing again succeeds — step 4 of the reproducer shows that, and it is also what a retry in the task achieves.

I have a small patch + regression test for this (catch ccxt.UnsubscribeError inside the loop and retry) and can open a PR if that is the preferred direction: