
Tagging 8-K disclosures with AI: corporate events, labelled by what actually happened
A SEC 8-K API that labels what actually happened. Query filings by event type (CEO departures, cybersecurity incidents, M&A), with source-cited tags.

rian

Introducing
May 6, 2026
TL;DR: A huge share of U.S. equity volume executes off-exchange, in dark pools and other venues that never show up on a lit exchange feed. Those prints still have to get reported to FINRA, which publishes them through its Trade Reporting Facilities (TRFs). In this post, we walk through what dark pool activity is, how it surfaces in Massive's real-time stock trades WebSocket channel, and a runnable Python scanner (under 150 lines) that streams every TRF-reported trade, filters for institutional-sized notional, and prints a clean tape of block activity you can watch during market hours.
In this post, we'll cover what dark pool trading is and why the data matters, how a dark pool print shows up inside Massive's stock trades stream, and how to build a scanner that filters the firehose down to the prints that actually signal institutional activity.
Who this is for: Quantitative traders, algorithmic trading teams, risk managers, and fintech builders who want real-time visibility into off-exchange block activity across U.S. equities.
Plan requirement: This demo requires a Massive Stocks Advanced plan.
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 install, and the shape of a minimal client. The rest of this post assumes familiarity with those basics.
A dark pool is a private trading venue where large orders can be matched without publishing a pre-trade quote. Institutional investors use them to move size without telegraphing their intent. If a pension fund needs to buy ten million shares of AAPL, doing it in the lit market would move the price against them before they finished filling. A dark pool lets them cross size with a counterparty and only report the trade after it executes.
Dark pools are one of several off-exchange venues. Others include single-dealer platforms, internalizers, and OTC market makers that handle retail-internalized flow. Regardless of which off-exchange venue actually clears the trade, it has to be reported to a FINRA Trade Reporting Facility (TRF) within seconds of execution. That reporting is what makes the data visible.
There are three TRFs:
| TRF ID | Venue Name |
|---|---|
| 201 | FINRA/NYSE TRF |
| 202 | FINRA/NASDAQ TRF Carteret |
| 203 | FINRA/NASDAQ TRF Chicago |
For a developer, the interesting part is that every TRF-reported trade flows through the same real-time stock trades feed as on-exchange prints. Filter the firehose for TRF-reported activity and you have a real-time tape of off-exchange volume.
Massive's stock trades WebSocket channel (
A trade is a dark pool (or otherwise off-exchange) print if and only if
From there, a notional threshold (price multiplied by size) is the cleanest way to narrow the feed down to institutional-sized prints. The default in this scanner is $100,000, which cuts out most of the small-lot retail fills and leaves you with the block-sized activity that carries real signal.
The scanner is published in Massive's community GitHub repo. You'll need Python 3.10 or later, uv for dependency management, and a Massive API key on the Stocks Advanced plan.
git clone https://github.com/massive-com/community.git cd community/examples/websocket/dark-pool-scanner uv sync cp .env.example .env # Add your MASSIVE_API_KEY to .env
The
The full scanner is a single
#!/usr/bin/env python3 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 EquityTrade, Market load_dotenv() API_KEY = os.getenv("MASSIVE_API_KEY") if not API_KEY: raise ValueError( "MASSIVE_API_KEY not found in environment variables. " "Please set it in your .env file." ) EASTERN = ZoneInfo("America/New_York")
A few notes:
Each TRF-reported trade carries an ID telling you which facility handled the report. Mapping those IDs to human-readable names makes the output easier to scan:
TRF_NAMES = { 201: "FINRA/NYSE TRF", 202: "FINRA/NASDAQ TRF Carteret", 203: "FINRA/NASDAQ TRF Chicago", }
Trades arrive with millisecond epoch timestamps. Convert them to Eastern time so they align with U.S. equity market hours:
def fmt_time(ms: int) -> str: """Convert a millisecond epoch timestamp to an Eastern Time string.""" return datetime.fromtimestamp(ms / 1000, tz=EASTERN).strftime( "%Y-%m-%d %H:%M:%S %Z" )
Notional is the total dollar value of the trade: price per share multiplied by the number of shares. The display function formats each field into a readable block:
def show_trade(trade: EquityTrade) -> None: """Pretty-print a single dark pool trade.""" notional = trade.price * trade.size venue = TRF_NAMES.get(trade.trf_id, f"TRF {trade.trf_id}") conditions = getattr(trade, "conditions", None) pl = lambda label, value, w=12: f"{(label + ':'):<{w}} {value}" print( "\n".join( [ "", pl("Symbol", trade.symbol), pl("Price", f"${trade.price:,.2f}"), pl("Size", f"{trade.size:,} shares"), pl("Notional", f"${notional:,.2f}"), pl("Venue", venue), pl("Timestamp", fmt_time(trade.timestamp)), pl("Conditions", conditions if conditions else "N/A"), "", "------------------------", ] ), flush=True, )
The
This is where the filtering logic sits. The scanner does three things:
def main(): parser = argparse.ArgumentParser( description="Stream dark pool (TRF) trades filtered by notional value." ) parser.add_argument( "--min-notional", type=float, default=100_000, help="Minimum notional value in USD (default: 100,000)", ) parser.add_argument( "--ticker", nargs="+", help="One or more tickers to watch (default: all)", ) args = parser.parse_args() if args.ticker: subscriptions = [f"T.{t.upper()}" for t in args.ticker] else: subscriptions = ["T.*"] client = WebSocketClient( api_key=API_KEY, market=Market.Stocks, subscriptions=subscriptions ) tickers_label = ( ", ".join(t.upper() for t in args.ticker) if args.ticker else "all tickers" ) print( f"[info] connecting to stock trade stream ({tickers_label})" f" | filter: TRF trades >= ${args.min_notional:,.0f} notional", flush=True, ) def handler(msgs): for trade in msgs: if not isinstance(trade, EquityTrade): continue if trade.exchange != 4 or trade.trf_id is None: continue if trade.price * trade.size < args.min_notional: continue show_trade(trade) try: client.run(handle_msg=handler) except KeyboardInterrupt: pass finally: client.close() if __name__ == "__main__": main()
The dark pool detection is two lines inside the handler:
Only trades that satisfy both conditions are off-exchange prints. Everything else (trades on NYSE, Nasdaq, ARCA, IEX, and other lit venues) gets filtered out. The notional threshold then narrows the feed to institutional-sized activity.
uv run main.py
By default, the scanner shows only TRF trades with a notional value of $100,000 or more. You can adjust the threshold or filter to specific tickers:
# Lower the notional threshold to $50,000 uv run main.py --min-notional 50000 # Watch only NVDA and TSLA dark pool prints uv run main.py --ticker NVDA TSLA # Combine both uv run main.py --ticker AAPL --min-notional 200000
Press Ctrl+C to stop the stream.
Symbol: NVDA Price: $172.62 Size: 5,000 shares Notional: $863,100.00 Venue: FINRA/NASDAQ TRF Carteret Timestamp: 2026-01-06 10:31:22 EST Conditions: N/A ------------------------
Each print shows the ticker, price per share, block size, total dollar value, the TRF venue that reported it, and a timestamp in Eastern time.
The scanner as written is a starting point. A few directions that extend it naturally:
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.

Cole Power
cole
See what's happening at Massive

A SEC 8-K API that labels what actually happened. Query filings by event type (CEO departures, cybersecurity incidents, M&A), with source-cited tags.

rian

In this post, we'll walk through how Earnings Watcher built its analytics layer on Massive's options chains and snapshots, why Fair Market Value anchors its scanner, and how the same API pattern extended from options into post-earnings drift analysis on the underlying equity.

alexnovotny

Massive's LULD WebSocket channel streams Limit Up/Limit Down price band updates, halts, and resumptions for every NMS stock in real time.
editor