nautechsystems/nautilus_trader · error · anyhow::Error
No instrument found for sprd_id
Error message
No instrument found for sprd_id: {} What it means
parse_spread_order_msg resolves an OKX spread order message's sprd_id against the local instrument cache; an unknown sprd_id means the spread's legs/definition were never loaded, so no ExecutionReport can be built and this error is thrown. Spread instruments must be explicitly loaded before spread order stream messages are parsed.
Solutions
- Load/subscribe the spread instrument on the adapter before expecting spread order updates
- Verify the sprd_id string matches the cached key exactly (OKX spread IDs like 'BTC-USDT_BTC-USDT-250328')
- On unknown sprd_id, fetch the spread definition via OKX HTTP API and insert into the instruments cache, then re-parse
- Skip and log unknown-spread order messages instead of failing the dispatch loop
Example fix
// before
let instrument = instruments
.get(&msg.sprd_id)
.ok_or_else(|| anyhow::anyhow!("No instrument found for sprd_id: {}", msg.sprd_id))?;
// after
let instrument = match instruments.get(&msg.sprd_id) {
Some(inst) => inst,
None => {
log::warn("Skipping spread order for unknown sprd_id {sprd_id}; load the spread first");
return Ok(None);
}
}; Defensive patterns
Strategy: validation
Validate before calling
// Rust, before parsing spread order updates
if !instruments.contains_key(&msg.sprd_id) {
log::warn("Spread {sprd_id} not cached; loading spread definition first");
load_spread_instrument(msg.sprd_id)?;
} Type guard
fn spread_cached(map: &AHashMap<Ustr, InstrumentAny>, sprd_id: &Ustr) -> bool {
map.contains_key(sprd_id)
} Try / catch
match parse_spread_order_msg(&msg, &instruments, account_id, &filled_qty_cache, ts_init) {
Ok(report) => dispatch(report),
Err(e) if e.to_string().starts_with("No instrument found for sprd_id") => {
log::warn("{e}; subscribe/load the spread before expecting its order updates");
}
Err(e) => return Err(e),
} Prevention
- Explicitly load every spread instrument the strategy trades before enabling the spread orders stream
- Keep sprd_id strings exactly as OKX reports them
- Persist/restore spread instrument cache across adapter restarts
- Reconcile open spread orders at startup to backfill missing definitions
When it happens
Trigger: A private spread 'orders' ('sprd-orders') WebSocket message arrives whose sprd_id is not present in the instruments AHashMap — the spread was not subscribed/loaded on this adapter session.
Common situations: Spread orders placed via OKX web UI for spreads the adapter never instantiated; sprd_id string mismatch vs cached key; adapter restart losing cached spread instruments while live spread orders stream updates.
Understand the failure class
Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.
Related errors
- No instrument found for inst_id
- Cannot determine fill quantity: fill_sz=
- Cannot determine fill quantity: fill_sz is empty/zero and…
- Cannot determine spread fill quantity: fill_sz=
- Empty asks array for
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/e5c6219d18ecfa1c.
Report an issue: GitHub.
Appendix: source
Thrown at crates/adapters/okx/src/websocket/parse.rs:1423
}
}
/// Parses a single OKX spread order message into an [`ExecutionReport`].
///
/// # Errors
///
/// Returns an error if the instrument cannot be found or if parsing the
/// underlying order payload fails.
pub fn parse_spread_order_msg(
msg: &OKXSpreadOrder,
account_id: AccountId,
instruments: &AHashMap<Ustr, InstrumentAny>,
filled_qty_cache: &AHashMap<Ustr, Quantity>,
ts_init: UnixNanos,
) -> anyhow::Result<ExecutionReport> {
let instrument = instruments
.get(&msg.sprd_id)
.ok_or_else(|| anyhow::anyhow!("No instrument found for sprd_id: {}", msg.sprd_id))?;
let previous_filled_qty = filled_qty_cache.get(&msg.ord_id).copied();
let has_new_fill = (!msg.fill_sz.is_empty() && msg.fill_sz != "0")
|| !msg.trade_id.is_empty()
|| has_acc_fill_sz_increased_value(
Some(msg.acc_fill_sz.as_str()),
previous_filled_qty,
instrument.size_precision(),
);
match msg.state {
OKXOrderStatus::Filled | OKXOrderStatus::PartiallyFilled if has_new_fill => {
match parse_spread_order_fill_report(
msg,
instrument,
account_id,
previous_filled_qty,
ts_init,
)? {View on GitHub (pinned to 18893faf8b)