nautechsystems/nautilus_trader · error

Lighter {label} subscriptions require a perpetual instrument

Error message

Lighter {label} subscriptions require a perpetual instrument: {instrument_id}

What it means

perp_market_stats_channel resolves an instrument to a Lighter MarketStats websocket channel, but only CryptoPerpetual instruments are eligible. The instrument was found in the local cache yet is not a perpetual (e.g. a spot or other instrument type), so the subscription is refused.

Source

Thrown at crates/adapters/lighter/src/data/mod.rs:789

            self.spawn_task(async move {
                if let Err(e) = unsubscribe_market_stats_channel(ws, channel).await {
                    log::error!("Failed to unsubscribe from Lighter {label}: {e:?}");
                }
            });
        }
    }

    fn perp_market_stats_channel(
        &self,
        instrument_id: InstrumentId,
        label: &str,
    ) -> anyhow::Result<LighterWsChannel> {
        let instrument = self
            .instruments
            .get_cloned(&instrument_id)
            .ok_or_else(|| InstrumentLookupError::not_found(instrument_id))?;

        anyhow::ensure!(
            matches!(instrument, InstrumentAny::CryptoPerpetual(_)),
            "Lighter {label} subscriptions require a perpetual instrument: {instrument_id}",
        );

        let market_index = self.registry.market_index(&instrument_id).ok_or_else(|| {
            anyhow::anyhow!("No Lighter market_index registered for {instrument_id}")
        })?;

        Ok(LighterWsChannel::MarketStats(
            LighterMarketSelection::Market(market_index),
        ))
    }

    fn index_market_stats_channel(
        &self,
        instrument_id: InstrumentId,
    ) -> anyhow::Result<LighterWsChannel> {
        let instrument = self

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Use a perpetual instrument ID (e.g. the PERP symbol) for mark-price and funding-rate subscriptions.
  2. Filter your subscription list to perpetual instruments before calling subscribe_mark_prices/subscribe_funding_rates.
  3. Check the instrument cache to confirm what type the ID resolves to.

Example fix

// before
data.subscribe_funding_rates(InstrumentId::from("ETH/USDT.LIGHTER"))?; // spot
// after
data.subscribe_funding_rates(InstrumentId::from("ETH-PERP.USDT.LIGHTER"))?;
Defensive patterns

Strategy: validation

Validate before calling

if !instrument_id.symbol.value.ends_with("-PERP") {
    return Err(anyhow::anyhow!("mark price/funding requires a perp: {instrument_id}"));
}

Type guard

fn is_perp(inst: &InstrumentAny) -> bool { matches!(inst, InstrumentAny::CryptoPerpetual(_)) }

Try / catch

match subscribe_funding_rates(instrument_id) {
    Ok(()) => (),
    Err(e) if e.to_string().contains("require a perpetual") => warn!("skip non-perp {instrument_id}"),
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling subscribe_mark_prices or subscribe_funding_rates with an InstrumentId that resolves to a non-perpetual cached instrument — e.g. requesting mark prices or funding rates for a spot pair.

Common situations: Subscribing to funding rates for spot instruments (which have none); a stale or wrong instrument ID pointing at a spot symbol; configuring subscriptions from a symbol list that mixes spot and perp symbols.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/76b85a1b6d06860c. Report an issue: GitHub.