Building a WebSocket Market News Consumer in Python: From First Event to Threshold-Gated Signals

작성자

카테고리:

← 피드로
DEV Community · Forecite · 2026-08-25 개발(SW)

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

Enter fullscreen mode Exit fullscreen mode

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())

Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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()

Enter fullscreen mode Exit fullscreen mode

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())

Enter fullscreen mode Exit fullscreen mode

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.

원문에서 계속 ↗