[Kraken] A failed TradeVolume request aborts spot instrument loading, so the execution client never connects

Author: zhaow-deCreated Sep 16, 2026Updated Sep 16, 2026

Bug Report

Confirmation

Before opening a bug report, please confirm:

  • I've re-read the relevant sections of the documentation.
  • I've searched existing issues and discussions to avoid duplicates.
  • I've reviewed or skimmed the source code (or examples) to confirm the behavior is not by design.
  • I've tested this issue using a recent pre-release or development wheel (2.0.0rcN, dev develop, or a nightly) and can still reproduce it.

Expected behavior

A failure to resolve account fee rates should not prevent Kraken spot instruments from loading, and should never prevent the execution client from connecting.

The adapter already carries a public fee fallback. resolve_fee_rates (crates/adapters/kraken/src/common/parse.rs) returns the public base-tier rates from the AssetPairs definition whenever no account rate is supplied, which is how unauthenticated clients get fees today. The commit that introduced account fees describes this explicitly as "Preserve public fee fallback for unauthenticated clients".

When the authenticated TradeVolume request fails, instruments should fall back to those same public rates rather than failing to load.

Actual behavior

The error aborts the entire instrument listing, and because the listing runs on the execution client's connect path, the execution client never connects.

In crates/adapters/kraken/src/http/spot/client.rs, request_instruments propagates the fee error (locate by symbol; line numbers move):

rust
let asset_pairs = self.inner.get_asset_pairs(pairs.clone(), None).await?;
let fee_rates = self.request_fee_rates(&asset_pairs, None).await?;   // aborts the whole listing

request_fee_rates skips out only when there are no credentials at all:

rust
if self.inner.credential().is_none() || pairs.is_empty() {
    return Ok(AHashMap::new());
}

so an authenticated client has no path to the public rates once the call errors.

request_instruments is called from KrakenSpotExecutionClient::connect (crates/adapters/kraken/src/execution/spot.rs) via .context("Failed to load Kraken spot instruments")?. The consequence is far larger than missing fees: the execution client does not connect, so there is no account in the Cache and no reconciliation. A live trading node comes up disarmed and silently without account state.

That asymmetry is the heart of this report. "Correct fees or no instruments" is a defensible policy for a pricing consumer and an unsafe one for an execution consumer, and the adapter cannot tell which it is serving. Fees are instrument metadata with an existing documented public fallback; the execution client is not there to price fees.

Note there are two fee requests on this path and both are fatal. The second requests tokenized (xStocks) fees with aclass=equity_pair. Inside that block, failing to fetch the tokenized pairs is already tolerated (Err(e) => log::warn!("Failed to fetch tokenized asset pairs: {e}")), while failing to get their fees aborts everything — an inconsistency worth resolving in the same place.

Steps to reproduce

The defect itself is deterministic and needs no venue access. Only the trigger is intermittent.

Deterministic reproduction, no credentials required. Make /0/private/TradeVolume return a Kraken API error and connect a spot execution client. Against develop @ 88cb0c7640, KrakenSpotExecutionClient::connect() returns Err:

execution client must connect when the account fee request is denied:
Failed to load Kraken spot instruments

which is the same top-level context seen in production. Any TradeVolume failure reproduces it; the response body we used was {"error":["EGeneral:Permission denied"],"result":{}}.

How it arises in production. The denial is intermittent, which is what makes the hard abort dangerous rather than merely inconvenient:

  1. Configure a Kraken spot execution client (KrakenExecutionClientConfig) with valid credentials holding Funds permissions - Query.
  2. Repeat POST /0/private/TradeVolume with the full spot pair list.
  3. Occasionally the call returns EGeneral:Permission denied while identical neighbouring calls with the same key and the same body succeed.

When that lands on a node's connect(), the node loses its execution client entirely:

[ERROR] nautilus_execution::engine: Failed to connect execution client:
Failed to load Kraken spot instruments: API error: EGeneral:Permission denied

once, with no retry and no recovery.

We probed this against a live account (about 30 read-only TradeVolume calls, 27 of them with a valid nonce, no orders) specifically to find a deterministic cause, and found none. Each of these was ruled out:

  • Payload size. The full public AssetPairs pair list succeeds, and so does that same list plus one additional pair — the latter immediately after a denial on the former.
  • The tokenized/equity-class request. aclass=equity_pair succeeds outright on this account.
  • JSON encoding. Single-pair JSON calls succeed; form versus JSON makes no difference.
  • Repetition. Eight consecutive identical full-list calls all succeeded.

Observed frequency, from a small sample and offered as nothing more: one denial in ten identical well-formed requests, plus the failure that prompted this report, on one account on one day. That supports "intermittent" and nothing quantitative beyond it.

We are therefore reporting this as a rare transient venue response, not a property of the request. To be explicit: we cannot reproduce the denial on demand, and we are not asking anyone to try. The deterministic reproduction above is the one that can be run.

This also touches the retry classification: kraken_http_should_retry treats an ApiError as retryable only when the message contains "Rate limit", so a transient EGeneral:Permission denied is a single attempt. That classification may be incomplete, but the fallback is the more robust remedy because it holds regardless of how retryability is resolved.

There is no way to avoid the call from configuration: KrakenExecutionClientConfig has no fee-resolution switch, and neither client config can narrow the instrument listing, so the adapter always loads the venue's full universe.

Code snippets or logs

The public fallback that is never reached on error (crates/adapters/kraken/src/common/parse.rs):

rust
fn resolve_fee_rates(
    definition: &AssetPairInfo,
    account_fee_rates: Option<(Decimal, Decimal)>,
) -> (Option<Decimal>, Option<Decimal>) {
    account_fee_rates.map_or_else(
        || {
            (
                definition.fees_maker.first().map(|(_, fee)| *fee / dec!(100)),
                definition.fees.first().map(|(_, fee)| *fee / dec!(100)),
            )
        },
        |(maker, taker)| (Some(maker), Some(taker)),
    )
}

Passing an empty fee map therefore yields fully-formed instruments carrying public base-tier fees. Catching the error inside request_fee_rates and returning an empty map gives correct-fees-if-available, public-fees-if-not.

Related

  • #4789 — the motivating issue for the account-fee change (stale AssetPairs ladder), closed by the commit that introduced this path. Different defect: it reports wrong fee values on instruments that load successfully.
  • #4890 — the contributor PR for that change. Its review already raised this class of concern: "get_trade_volume bypasses the normal retry and cancellation path... A retryable network or 5xx failure can now abort instrument initialization." The landed change addressed retry; a non-retryable error such as this one still propagates and aborts the listing.
  • #4289 (merged) — "Fix Binance futures leverage initialization aborting execution client connect", which made the auxiliary call best-effort "so a leverage-init issue never prevents the execution client from connecting". Same remedy, already accepted in this project.
  • Commit a21304e9f6, "Fix Bybit demo exec client fee rate error on connect" (resolving #3742), is the closest precedent of all: "Fall back to default fees on BybitError in fetch_fee_map". That fallback is still in place today — BybitHttpClient::fetch_fee_map (crates/adapters/bybit/src/http/client.rs) matches Err(BybitHttpError::BybitError { .. }), logs a warning and returns an empty fee map so default rates apply. That is structurally the change proposed here, for the same reason, on the same connect path, in this repository.

We note #4729 and #4617 were closed on the principle that a failed venue query must not be silently swallowed, and we think this case is distinguishable: position and order reports are authoritative state, whereas fees are instrument metadata for which this adapter already ships a public fallback and already uses it for unauthenticated clients. The proposal is to log a warning, not to hide the failure.

Specifications

  • Adapter/venue: Kraken (spot)
  • Reproduced against: develop @ 88cb0c7640

Deployment details for the affected node are omitted deliberately. They reached us second-hand and we could not verify them first-hand, so we would rather leave the field empty than state it wrongly. The deterministic reproduction above is against develop and needs no venue access.

Source: nautechsystems/nautilus_trader