Introducing

Build a NYSE Order Imbalance Tracker with Massive's WebSocket API

May 13, 2026

TL;DR: Massive's NOI WebSocket channel streams NYSE Net Order Imbalance data in real time, including imbalance quantity, paired shares, and the indicative clearing price. In this post, we walk through how NYSE auctions work, why order imbalances are one of the most actionable pre-auction signals available, what an NOI event looks like field by field, and two runnable Python examples: a firehose that prints every update, and a smart monitor with buy/sell direction labels, convergence tracking, and CLI-based ticker and size filtering.

In this post, we'll cover what Net Order Imbalance data represents and why it matters, the NOI event schema, how to read direction, magnitude, and convergence signals from the stream, and how to build a smart monitor that surfaces only the imbalances you care about.

Who this is for: Quantitative traders, algorithmic trading teams, and fintech builders who want pre-auction order flow intelligence from NYSE in real time.

Plan requirement: NOI data requires a plan with NYSE order imbalance access.

New to WebSockets? If you haven't connected to Massive's WebSocket API before, start with the WebSocket quickstart. It walks through the connect, authenticate, and subscribe pattern, the Python SDK installation, and the shape of a minimal client. The rest of this post assumes familiarity with those basics.

What NOI Is

Massive's NOI WebSocket channel gives you real-time visibility into buy and sell pressure ahead of NYSE auctions. By subscribing, you can stream imbalance quantity, paired shares, and indicative clearing prices as they update, giving you a continuous read on auction dynamics before the print happens.

To understand why that matters, it helps to know how auctions differ from regular trading. Most stock transactions happen through continuous trading, where buyers and sellers are matched individually throughout the day as orders hit the book. NYSE auctions work differently. Rather than matching orders one by one, NYSE batches all eligible orders together and determines a single clearing price that maximizes the number of shares traded. NYSE runs these auctions at three specific moments: market open, following a trading halt, and market close. Every participant in that auction transacts at that one price.

In the minutes leading up to each auction, buy and sell orders accumulate at different prices and sizes. The gap between buy-side and sell-side volume is the order imbalance. It signals which direction the auction price may need to move to attract enough liquidity to clear, making it one of the more actionable pre-auction signals available.

Each NOI event exposes three core fields:

  • Imbalance quantity: the net difference between buy and sell orders
  • Paired shares: shares already matched and ready to clear
  • Indicative clearing price: the projected auction price given current order flow

NYSE begins disseminating open imbalance data around 9:00 AM ET, roughly 30 minutes before the 9:30 AM open auction. Close imbalance data starts around 3:50 PM ET. These two windows are where the most actionable NOI data is concentrated. Outside of them, events are primarily driven by trading halt auctions, which are less predictable in timing but follow the same imbalance mechanics.

NOI Event Structure

The NOI channel is available on

wss://socket.massive.com/stocks
. Coverage is NYSE-listed tickers only. You can subscribe to a single ticker (
NOI.JPM
), a specific set (
NOI.JPM,NOI.BAC,NOI.GS
), or the full firehose (
NOI.*
).

A sample NOI event looks like this:

{
  "ev": "NOI",
  "T": "JPM",
  "t": 1601318039223013600,
  "at": 930,
  "a": "M",
  "i": 44,
  "x": 10,
  "o": 480,
  "p": 440,
  "b": 25.03
}

Each NOI message contains:

FieldSDK AttributeDescription
evevent_typeAlways NOI
TsymbolNYSE-listed ticker
ttime_stampNanosecond Unix epoch timestamp
atauction_timeScheduled auction time (HHMM format, Eastern)
aauction_typeM = market open, C = close, H = halt
oimbalance_quantityNet imbalance in shares (positive = buy-side, negative = sell-side)
ppaired_quantityShares already matched and ready to clear
bbook_clearing_priceIndicative auction clearing price
xexchange_idExchange identifier
isymbol_sequenceSequence number

Reading the Data

Order imbalance direction, magnitude, and convergence all carry signal.

Direction. A positive imbalance quantity (buy-side imbalance) means there are more buy orders than sell orders in the auction queue. To clear, the auction needs additional sell-side liquidity. NYSE's indicative clearing price will typically rise until enough sellers are attracted to close the gap. If the imbalance remains large into the auction, expect the opening or closing print to be above the last traded price. Sell-side imbalance is the inverse. Excess sell orders push the indicative clearing price lower as the auction seeks buy-side liquidity. A persistent sell-side imbalance heading into the close, for example, often results in a closing print below the last traded price.

Magnitude. A large buy imbalance on a thinly traded name carries significantly more weight than the same figure on a high-volume stock. A practical approach is to normalize imbalance quantity against the stock's average daily volume (ADV) or average auction volume. An imbalance representing 5% of ADV is a very different signal than one representing 0.05%.

Convergence. As the auction approaches, a shrinking imbalance means liquidity is arriving on the contra side. This is an important signal: it suggests the market is moving toward equilibrium and the final print is likely to be close to the current indicative clearing price. A large imbalance that is not shrinking as the auction nears suggests the indicative price may still have room to move.

Cloning the Example Repo

Both example scripts are published in Massive's community GitHub repo. You'll need Python 3.10 or later, uv for dependency management, and a Massive API key.

git clone https://github.com/massive-com/community.git
cd community/examples/websocket/noi-imbalance-monitor
uv sync
cp .env.example .env
# Add your MASSIVE_API_KEY to .env

From there, run any script with

uv run <script>.py
.

Basic Firehose

Subscribe to

NOI.*
and print every event:

from massive import WebSocketClient
from massive.websocket.models import Feed, Market
import os
from dotenv import load_dotenv

load_dotenv()
API_KEY = os.getenv("MASSIVE_API_KEY")

client = WebSocketClient(
    api_key=API_KEY,
    feed=Feed.RealTime,
    market=Market.Stocks
)
client.subscribe("NOI.*")

def handle_msg(msgs):
    for m in msgs:
        print(m)

print("NOI Basic Firehose Started")
print("Streaming ALL NYSE order imbalance updates in real time")
print("Press Ctrl+C to stop\n")

try:
    client.run(handle_msg)
except KeyboardInterrupt:
    pass

Raw output:

Imbalance(event_type='NOI', symbol='JPM', imbalance_quantity=5200, paired_quantity=42800, book_clearing_price=142.5, auction_type='M', ...)
Imbalance(event_type='NOI', symbol='BAC', imbalance_quantity=-3100, paired_quantity=28400, book_clearing_price=38.75, auction_type='M', ...)

The firehose is useful for understanding what the stream looks like during the open and close windows. For real monitoring, you'll want filtering and structure.

Smart Monitor: Direction, Convergence, and Filtering

The smart monitor transforms the raw feed into a structured table with buy/sell direction labels, convergence tracking between updates, and CLI-based filtering by ticker or minimum imbalance size.

import argparse
import os
from datetime import datetime
from zoneinfo import ZoneInfo
from dotenv import load_dotenv
from massive import WebSocketClient
from massive.websocket.models import Feed, Market, Imbalance

load_dotenv()
API_KEY = os.getenv("MASSIVE_API_KEY")
EASTERN = ZoneInfo("America/New_York")

AUCTION_LABELS = {"M": "Open", "C": "Close", "H": "Halt"}
prev_imbalance: dict[str, int] = {}


def fmt_time(ts_ns: int) -> str:
    return datetime.fromtimestamp(
        ts_ns / 1_000_000_000, tz=EASTERN
    ).strftime("%H:%M:%S")


def direction(qty: int) -> str:
    if qty > 0:
        return "BUY"
    elif qty < 0:
        return "SELL"
    return "FLAT"


def convergence(symbol: str, current: int) -> str:
    previous = prev_imbalance.get(symbol)
    prev_imbalance[symbol] = current
    if previous is None:
        return ""
    if abs(current) < abs(previous):
        return "shrinking"
    elif abs(current) > abs(previous):
        return "growing"
    return ""


parser = argparse.ArgumentParser()
parser.add_argument("--ticker", nargs="+")
parser.add_argument("--min-imbalance", type=int, default=0)
args = parser.parse_args()

if args.ticker:
    subscriptions = [f"NOI.{t.upper()}" for t in args.ticker]
else:
    subscriptions = ["NOI.*"]

client = WebSocketClient(
    api_key=API_KEY,
    feed=Feed.RealTime,
    market=Market.Stocks,
    subscriptions=subscriptions,
)

header = (
    f"{'Time':>8}  {'Symbol':6}  {'Auction':7}  {'Dir':4}  "
    f"{'Imbalance':>10}  {'Paired':>10}  {'Price':>10}  Trend"
)
print(header)
print("-" * len(header))


def handler(msgs):
    for m in msgs:
        if not isinstance(m, Imbalance):
            continue

        imb = m.imbalance_quantity or 0
        if abs(imb) < args.min_imbalance:
            continue

        symbol = m.symbol or ""
        paired = m.paired_quantity or 0
        price = m.book_clearing_price or 0.0
        auction = AUCTION_LABELS.get(m.auction_type or "", "")
        ts = fmt_time(m.time_stamp) if m.time_stamp else "--:--:--"
        trend = convergence(symbol, imb)

        print(
            f"{ts:>8}  {symbol:6}  {auction:7}  {direction(imb):4}  "
            f"{imb:>10,}  {paired:>10,}  ${price:>9.2f}  {trend}",
            flush=True,
        )


try:
    client.run(handle_msg=handler)
except KeyboardInterrupt:
    pass

There are two things worth highlighting in this code.

First, the convergence tracker. For each ticker, the script stores the previous absolute imbalance and compares it to the current update. A "shrinking" label means contra-side liquidity is arriving: sellers are stepping in to offset a buy imbalance, or buyers to offset a sell imbalance. This is the signal that the auction is approaching equilibrium and the final print will likely land close to the current indicative price. "Growing" means pressure is still building in one direction.

Second, the subscription-level filtering. When you pass

--ticker JPM BAC GS
, the script subscribes only to
NOI.JPM
,
NOI.BAC
, and
NOI.GS
, rather than subscribing to the firehose and filtering client-side. This reduces bandwidth and is the right approach when you know your watchlist in advance. For broad scanning with a size threshold,
NOI.*
with the
--min-imbalance
flag is the better pattern.

Sample output watching JPM, BAC, and GS:

NOI Monitor | Watching: JPM, BAC, GS
Press Ctrl+C to stop

    Time  Symbol  Auction  Dir    Imbalance      Paired       Price  Trend
------------------------------------------------------------------------
09:15:22  JPM     Open     BUY        5,200      42,800   $  142.50
09:15:22  BAC     Open     SELL      -3,100      28,400   $   38.75
09:15:22  GS      Open     BUY       12,400      85,200   $  412.30
09:15:45  JPM     Open     BUY        3,800      44,200   $  142.35  shrinking
09:15:45  BAC     Open     SELL      -4,500      27,000   $   38.60  growing
09:15:45  GS      Open     BUY        8,100      89,500   $  412.15  shrinking

In this snapshot, JPM and GS both show buy-side imbalances that are shrinking between updates, suggesting contra-side liquidity is arriving and the open auction print is likely near the current indicative price. BAC shows a sell-side imbalance that is growing, which means more sell pressure is accumulating and the clearing price may still move lower.

Running It

cd community/examples/websocket/noi-imbalance-monitor
uv sync
cp .env.example .env
# Add your MASSIVE_API_KEY to .env

uv run basic_firehose.py                         # raw stream, all tickers
uv run noi_monitor.py                            # smart monitor, all tickers
uv run noi_monitor.py --ticker JPM BAC GS        # watch specific tickers
uv run noi_monitor.py --min-imbalance 5000       # only large imbalances

Press Ctrl+C to stop.

Coverage: NOI data covers NYSE-listed tickers only. NASDAQ-listed names will not appear.

Timing and throughput: The bulk of NOI event volume is concentrated in two narrow windows: roughly 9:00 to 9:30 AM ET (before the open auction) and 3:50 to 4:00 PM ET (before the close auction). If you are processing selectively, these two windows are where the most actionable data is concentrated. Outside of them, the bulk of regular-hours events are driven by trading halt auctions, which are less predictable in timing but follow the same imbalance mechanics.

What You Can Build From Here

  • Normalize NOI imbalance against average daily volume or average auction volume for a relative measure of pre-auction pressure, so you can compare signal strength across tickers with very different trading profiles.
  • Combine NOI convergence data with trade and quote data from Massive's
    T
    and
    Q
    channels to analyze how well imbalances predict the final auction print. Log events to CSV for backtesting auction strategies.
  • Build an alerting system that triggers when a ticker's imbalance exceeds a percentage of ADV, or when close-auction imbalances flip direction in the final minutes.
  • Feed the stream into a dashboard that visualizes buy and sell pressure and clearing-price trajectories for a curated watchlist leading into each auction.

Get Started

Disclaimer

This content is for educational purposes only. Nothing in this post constitutes investment advice or a recommendation to buy or sell any securities or other financial instruments. Massive is a market data provider, not a broker-dealer, exchange, or investment adviser. Market data accessed through Massive may originate from third-party exchanges and data providers or may be derived or calculated by Massive; in either case, it is subject to the applicable terms of your Massive subscription agreement. The data and code samples provided by Massive are offered on an "as-is" basis without any warranty of accuracy, completeness, or timeliness. You are solely responsible for your use of the data provided by Massive and for compliance with all applicable terms and conditions, laws, and data licensing requirements.

From the blog

See what's happening at Massive