All articles
Strategies·October 27, 2025·5 min read

Building an Order-Book Imbalance Signal on Solana OpenBook v2

Constructs a real-time bid-ask volume imbalance feature from OpenBook v2's on-chain account state, evaluates its predictive horizon at the 50ms and 500ms mark, and integrates it into a momentum entry filter. Includes a TypeScript listener that streams account deltas via gRPC.

OpenBook v2 exposes the full limit order book on-chain as a pair of critbit trees — one for bids, one for asks — serialised inside a BookSide account. If you can stream account state changes faster than the rest of the market processes them, the bid-ask volume imbalance (OBI) sitting in those trees is a usable short-horizon signal. This post walks through exactly how to compute it, how long it stays predictive, and how to gate momentum entries with it using a custom Solana bot.

Reading the BookSide Account in Real Time

The naive approach — polling via getAccountInfo on every slot — is too slow and too noisy. The right path is subscribing to account deltas via accountSubscribe over the Solana gRPC interface (Yellowstone/Geyser plugin on your own validator, or a commercial endpoint from Triton or Helius). Each UpdateOneof::Account message carries the full account data, not a diff, so you re-deserialise the entire BookSide on each update. At typical order-book churn rates that is 5–30 messages per second per market, which is workable.

import { CommitmentLevel, SubscribeRequest } from "@triton-one/yellowstone-grpc";
import Client from "@triton-one/yellowstone-grpc";
import { deserializeBookSide } from "./openbook-v2-decode";

const client = new Client(GRPC_ENDPOINT, TOKEN, {});
const stream = await client.subscribe();

const req: SubscribeRequest = {
  accounts: {
    ob: {
      account: [BIDS_PUBKEY, ASKS_PUBKEY],
      owner: [],
      filters: [],
    },
  },
  commitment: CommitmentLevel.PROCESSED,
  // other fields omitted for brevity
};

stream.on("data", (update) => {
  if (!update.account) return;
  const pubkey = update.account.account?.pubkey?.toString("base58");
  const data   = update.account.account?.data as Buffer;
  const side   = deserializeBookSide(data);
  onBookUpdate(pubkey, side);
});

stream.write(req);

Use CommitmentLevel.PROCESSEDCONFIRMED adds ~400ms of latency that destroys the signal.

Computing the Imbalance Feature

Once you have the deserialized BookSide, collapse the top-N levels into a single scalar:

OBI = (bidVol_N − askVol_N) / (bidVol_N + askVol_N)

N = 5 levels is a reasonable default for most OpenBook v2 markets. Fewer levels respond faster but are too noisy from single large resting orders; more levels dilute the near-touch signal with stale liquidity far from mid. Apply a denominator guard of Math.max(total, 1e-9) to avoid division-by-zero during thin-book conditions.

Store updates in a ring buffer keyed on wall-clock timestamp (not slot number — slot boundaries are uneven). You need both the current value and a 200ms exponential moving average to separate the persistent imbalance from transient order placement.

Predictive Horizon: What the Numbers Actually Say

Testing on SOL/USDC during normal session hours (09:00–17:00 UTC), sampling at 10ms resolution:

  • 50ms horizon: Spearman rank correlation between OBI and subsequent mid-price return is 0.18–0.24, depending on volatility regime. This is usable but requires low-latency execution infrastructure — your fill has to land within 20–30ms of signal generation or the edge decays.
  • 500ms horizon: Correlation drops to 0.08–0.12. Still statistically significant but not worth trading on its own; the Sharpe contribution from OBI alone at this horizon barely clears transaction costs at typical taker fees.

The takeaway: OBI is a conditioning signal, not a standalone alpha source. It raises win rate on entries you were already going to take; it does not generate independent edge.

Split your measurement into calm vs. volatile regimes using a rolling 30s realised volatility estimate. OBI predictive power roughly doubles in low-volatility windows, which is the regime where your limit orders are more likely to get filled without adverse selection anyway.

Integrating OBI Into a Momentum Entry Filter

The cleanest integration is as a gating condition on a faster momentum signal — for example, a 500ms price-rate-of-change trigger:

function shouldEnter(signal: MomentumSignal, book: BookSnapshot): boolean {
  const obi = computeOBI(book, 5);
  const ema = book.obiEma200ms;
  const delta = obi - ema;

  // Only enter longs when imbalance is directionally confirming
  // and the persistent component (EMA) hasn't already repriced
  if (signal.direction === "long")  return obi > 0.15 && delta > 0.05;
  if (signal.direction === "short") return obi < -0.15 && delta < -0.05;
  return false;
}

The delta check — current OBI minus its short-term EMA — filters cases where the imbalance has been in place for several hundred milliseconds and the price has likely already moved. You want to catch the leading edge of the imbalance, not the lagging tail.

In backtests on 30 trading days of tick data, this filter improves entry-to-peak return by roughly 15 basis points on average for the long side at the 500ms horizon, while reducing trade count by ~35%. Whether that tradeoff is worth it depends on your fill rate assumptions.

Practical Pitfalls

  • Account data lag: Geyser plugins on shared validators can queue updates during congestion. Timestamp every update on receipt and discard book snapshots older than 100ms before acting on them.
  • Iceberg orders: OpenBook v2 supports partially-hidden orders. The visible book OBI will be systematically biased when large icebergs are resting. There is no clean way to detect this from account state alone; you have to cross-reference the trade feed for unusual clip-size regularity.
  • Maker rebates and fee tiers: If you are also providing liquidity, OBI tells you when to skew your quotes. Pulling quotes early when OBI crosses ±0.3 and resetting after the imbalance normalizes reduces toxic fill exposure meaningfully — we have seen it cut adverse-selection cost by 20–30% in practice.

Infrastructure Requirements

This pipeline only makes sense if your compute is colocated with your Geyser source. Round-trip from a remote server adds 20–80ms depending on geography, which consumes almost all of the predictive window at the 50ms horizon. Run your listener process on the same machine as the validator (or on a bare-metal box in the same datacenter as a Triton Geyser node). Keep the deserialization path in a tight Rust function exported via N-API if you need the extra microseconds — pure TypeScript deserialization of a full BookSide account runs in about 80–150µs, which is fine for 50ms targets but starts to matter if you push below 10ms.


If you want OBI or similar microstructure features running in production on your Solana strategy, reach out — we build and operate the full stack, from Geyser listener through execution.

Need a bot like this built?

We design, build and run trading bots on Solana, Hyperliquid and Polymarket.

Start a project
#strategies#solana#openbook#order-book#market-microstructure#typescript#grpc