Building a WebSocket Market News Consumer in Python: From First Event to Threshold-Gated Signals
Subscribe to Forecite's scored WebSocket market news feed in Python, survive disconnects, and gate events into a clean signal queue.
If you're building a trading bot, raw headlines are a liability. What you want from a websocket market news feed is not more events — it's fewer, scored ones, arriving fast enough to act on. Forecite delivers exactly that: 180,000+ news items a week ingested one hop from the publisher (SEC EDGAR, wire services, exchanges, central banks, macro releases), each scored by the Verdict Engine and pushed to subscribers in under 50 milliseconds.
This tutorial walks through a production-shaped consumer using the Python SDK: first event on screen, then reconnects, backpressure, threshold gating into a signal queue, and REST catch-up so a dropped connection doesn't become a blind spot.
What you're consuming
Every event on the feed carries two numbers you'll build your logic around:
- Actionability (0 to 1): how likely this headline is to move price in the short term. This is the noise filter — boilerplate filings score low.
- Verdict (−1.0 to +1.0): combined directional score, calibrated to realized 1-hour return. Headlines are labelled by the market, not by humans, and the model retrains weekly.
That calibration detail matters for bot builders: you're not filtering on whether language sounds positive, you're filtering on what historically moved price. Generic sentiment models score language; this scores market reaction.
Setup and first event
Install the Python SDK and export your API key. The Free tier needs no card and gives you the unscored realtime feed to test connectivity; you'll want Starter or above for scored events.
pip install forecite
The minimal consumer is a few lines:
import asyncio
from forecite import ForeciteClient
client = ForeciteClient(api_key="fc_live_...")
async def main():
async for event in client.stream():
print(
f"{event.published_at} "
f"act={event.actionability:.2f} "
f"verdict={event.verdict:+.2f} "
f"{event.headline}"
)
asyncio.run(main())
Run it during US market hours and events arrive continuously. Note the timestamps: Forecite publishes per-event latency telemetry, so you can measure publication-to-you delay yourself rather than taking "sub-50 ms" on faith.
Reconnects: assume the socket will die
Any long-lived WebSocket connection will drop — deploys, network blips, laptop sleep. The consumer's job is to reconnect with backoff and remember where it left off, because the gap is where you miss the thing.
Wrap the stream in a supervisor loop:
import asyncio
import logging
log = logging.getLogger("consumer")
async def run_forever(client, handler):
backoff = 1
last_event_id = None
while True:
try:
async for event in client.stream():
last_event_id = event.id
backoff = 1 # healthy connection resets backoff
await handler(event)
except ConnectionError as exc:
log.warning("stream dropped: %s — reconnecting in %ss", exc, backoff)
await asyncio.sleep(backoff)
backoff = min(backoff * 2, 30)
if last_event_id:
await catch_up(client, since_id=last_event_id, handler=handler)
Two deliberate choices here. Backoff is capped at 30 seconds — on a feed where latency is the edge, minutes-long backoff defeats the purpose. And we record the last event ID before handling, so the catch-up call (below) can close the gap exactly.
Catch-up over REST
Forecite exposes the same scored corpus over REST, which turns "we were disconnected for 40 seconds" from an unknown into a bounded query:
async def catch_up(client, since_id, handler):
missed = await client.history(since_id=since_id)
log.info("replaying %d missed events", len(missed))
for event in missed:
await handler(event)
How far back you can reach depends on tier: Starter covers a 48-hour historical window, Pro 30 days, Quant a full year. For gap-filling after a disconnect, even the Starter window is far more than you'll ever need; the longer windows matter when you graduate to backtesting, where the same historical API supports deterministic replay — point-in-time honest data, so your backtest sees exactly what the feed would have shown at that moment.
If your infrastructure can't hold a socket open at all (serverless, restrictive networks), the platform also delivers over webhooks — same scored events, pushed to an HTTPS endpoint you control. A reasonable production posture is WebSocket as primary and a webhook endpoint as an independent fallback path writing into the same queue, deduplicated by event ID.
Backpressure and the signal queue
The Verdict Engine sustains thousands of verdicts per second at peak (the platform quotes 6.4k/second throughput). Your strategy code almost certainly can't — and shouldn't — process every event synchronously in the stream handler. If your handler blocks, you stall the socket read loop and your "real-time" feed quietly becomes a delayed one.
The fix is the standard producer–consumer split, with the threshold gate applied at the cheapest possible point — before the queue:
import asyncio
ACTIONABILITY_FLOOR = 0.7 # tune against your strategy's backtest
QUEUE_MAX = 1000
signal_queue: asyncio.Queue = asyncio.Queue(maxsize=QUEUE_MAX)
async def gate(event):
"""Fast path: score check only. Never block here."""
if event.actionability < ACTIONABILITY_FLOOR:
return # boilerplate filings and low-impact noise die here
try:
signal_queue.put_nowait(event)
except asyncio.QueueFull:
# Explicit policy beats silent stalling. Here: drop oldest.
_ = signal_queue.get_nowait()
signal_queue.put_nowait(event)
log.warning("queue full — dropped oldest signal")
async def strategy_worker():
while True:
event = await signal_queue.get()
if abs(event.verdict) >= 0.5:
await place_or_adjust(event) # your execution logic
signal_queue.task_done()
Three patterns worth stealing even if you change everything else:
Gate on actionability first, direction second. Actionability answers "is this worth compute at all?" — it's your volume filter. The verdict's sign and magnitude answer "which way, and how confidently?" Keeping the checks separate makes each threshold independently tunable.
Make the overflow policy explicit. A bounded queue with drop-oldest means that during an event storm — a Fed release, a cluster of filings — your bot acts on the freshest signals rather than working through a stale backlog. For news-driven strategies, a 90-second-old headline is usually worth less than the one that just landed. If your strategy disagrees, choose drop-newest or block — but choose.
Keep the socket loop non-blocking. The gate coroutine does one comparison and one queue operation. Everything slow — order placement, position checks, logging to disk — lives in the worker.
Wiring it together
async def main():
client = ForeciteClient(api_key="fc_live_...")
await asyncio.gather(
run_forever(client, handler=gate),
strategy_worker(),
)
asyncio.run(main())
That's the whole skeleton: a supervised stream feeding a bounded queue through a score gate, with REST catch-up sealing the gaps. From here, the natural next steps are tuning your actionability floor against the historical API (Quant's 1-year window and deterministic replay exist for exactly this), and — if you're building AI agents rather than classic bots — pointing them at the @forecite/mcp server so they consume scored events natively.
One honest caveat: nothing above is investment advice, and a scored feed doesn't make a strategy — it makes the input to one trustworthy. Forecite is confident enough in that input to run a public live desk: three real-money accounts trading US equities with one decision input, the Forecite feed. Past performance is not indicative of future results, but the telemetry is there to inspect.
Start streaming
The Free tier takes no card and gets you connected to the realtime feed today; Starter ($39/month, billed annually) adds the scored feed and the 48-hour historical API this tutorial leans on. Grab an API key, run the twenty-line consumer above, and see what a Python SDK for financial news looks like when the headlines arrive already judged — signal, not volume.
Top comments (0)