CUCKOOTRADE
Guide

How to test a streaming market data client when markets are closed

A live tick stream that works at 3am on a Sunday, with no key and no WebSocket client — plus dropped sockets, truncated frames and silent connections on demand, so reconnect logic finally has something to reconnect from.

SSE curl / Python / JS 8 minutes

Streaming code has a scheduling problem

Real market data streams are live for six and a half hours a day, five days a week. That leaves most of the week — and all of every weekend — where a streaming client cannot be exercised at all. So the work happens on Tuesday afternoon, and the demo is on Monday morning.

The specific things that go untested:

  • Reconnect after a dropped socket, which requires a dropped socket.
  • Idle timeouts, which require a stream that goes quiet.
  • Parse-error recovery, which requires a malformed frame that does not end the stream.
  • Anything at all, on a Sunday.

A stream that is always open

The endpoint is Server-Sent Events rather than a WebSocket, on purpose: SSE is curl-able, which makes it its own documentation.

shell — works right now, whatever time it is
curl -N 'https://cuckootrade.com/api/v1/stream?symbols=CUCKOO,AAPL'
the frames
event: hello
data: {"clock":"demo","symbols":["CUCKOO","AAPL"],"generation":1}

event: tick
data: {"S":"CUCKOO","p":102.34,"t":"2026-08-20T14:31:02Z"}

: hb

event: tick
data: {"S":"AAPL","p":231.08,"t":"2026-08-20T14:31:03Z"}

One hello on connect, then tick events carrying S (symbol), p (price) and t (the instant it claims to be). The : hb lines are SSE comments, not events — most clients drop them silently, which is exactly their purpose.

ParamMeaning
symbolsUp to 10, comma-separated. Scenario tickers work here too.
clock=demoDefault. An always-open synthetic session. Never sleeps.
clock=realFollows the NYSE calendar — silent while the market is closed.
seedSelects an alternate universe, as everywhere else.
scenarioTransport faults. See below.
Both clocks are useful. clock=demo is what a demo or a dashboard wants: alive regardless of the hour. clock=real is what you test against when the behavior under test is “what does my app do when the market is closed” — a case that is otherwise only reproducible by waiting.

Limits: 10 symbols, 5 concurrent streams per address, and 15 minutes per connection. That last one is deliberate: a client that cannot handle the server ending a stream is a client with a bug, and this surfaces it within the first quarter hour.

Consuming it

javascript — browser EventSource
const es = new EventSource(
  'https://cuckootrade.com/api/v1/stream?symbols=CUCKOO,CRASH'
)

es.addEventListener('hello', (e) => {
  console.log('connected:', JSON.parse(e.data))
})

es.addEventListener('tick', (e) => {
  const { S, p, t } = JSON.parse(e.data)
  console.log(S, p, t)
})

// EventSource reconnects on its own. Your job is to notice that it did,
// and to decide whether the gap matters.
es.onerror = () => console.warn('stream interrupted; browser will retry')
python — httpx, no SSE library needed
import json

import httpx

URL = "https://cuckootrade.com/api/v1/stream"


def ticks(symbols, **params):
    with httpx.stream("GET", URL, timeout=None,
                      params={"symbols": symbols, **params}) as r:
        event = None
        for line in r.iter_lines():
            if line.startswith(":"):          # heartbeat comment
                continue
            if line.startswith("event: "):
                event = line[7:]
            elif line.startswith("data: ") and event == "tick":
                yield json.loads(line[6:])


for tick in ticks("CUCKOO"):
    print(tick["S"], tick["p"])

CORS is wide open and GET-only, so the browser example works from any origin, including a local file.

Breaking it on purpose

This is the part a real feed cannot give you. Add scenario= and the transport misbehaves — deterministically, so the resulting test is not flaky and can live in CI permanently.

EffectWhat happensWhat it catches
drop:SSocket closes at T+S, mid-frame, no close event.Reconnect logic.
truncateOne frame cut mid-JSON, connection stays up.Parser resync — the hard one.
garbage:NN invalid data: payloads among the good ones.A parse error that must not kill the stream.
silent:SData and heartbeats stop for S seconds.Read timeouts, liveness detection.
slow:MSDelays the next frame.Backpressure, buffering.
shell — four failures you cannot otherwise schedule
# the socket dies twenty seconds in, mid-frame
curl -N 'https://cuckootrade.com/api/v1/stream?symbols=CUCKOO&scenario=drop:20s'

# one frame arrives cut in half; the connection stays up
curl -N 'https://cuckootrade.com/api/v1/stream?symbols=CUCKOO&scenario=truncate'

# three unparseable payloads mixed in with the good ones
curl -N 'https://cuckootrade.com/api/v1/stream?symbols=CUCKOO&scenario=garbage:3'

# thirty seconds of total silence -- no data, no heartbeats
curl -N 'https://cuckootrade.com/api/v1/stream?symbols=CUCKOO&scenario=silent:30s'

drop and truncate look similar and are not. A dropped socket is the easy case — the connection ends and something reconnects. A truncated frame on a live connection is the case that finds bugs, because nothing tells the client anything went wrong; it has to resynchronise its own parser.

python — asserting the reconnect actually happens
import time

import httpx


def test_client_reconnects_after_a_drop():
    started = time.monotonic()
    received = 0
    try:
        with httpx.stream("GET", URL, timeout=None,
                          params={"symbols": "CUCKOO",
                                  "scenario": "drop:5s"}) as r:
            for _ in r.iter_lines():
                received += 1
    except httpx.RemoteProtocolError:
        pass                                  # the drop, as requested

    elapsed = time.monotonic() - started
    assert received > 0                       # it streamed before it died
    assert 4 < elapsed < 9                    # and died on schedule

The failure that looks like success

A dropped stream is loud. The dangerous one is a stream that keeps delivering punctual, well-formed frames whose values never change — every liveness check green, every number twenty minutes old.

shell — healthy socket, dead data
curl -N 'https://cuckootrade.com/api/v1/stream?symbols=STALE'

On the demo clock STALE freezes for twenty seconds out of every minute, on a fixed schedule, so you see it without waiting. During the freeze t and p repeat verbatim and the heartbeats keep flowing — that last detail is the load-bearing one. The socket is healthy, the ticks are punctual, the data is dead. If your client cannot tell the difference, neither can your monitoring.

A note on heartbeats, if you are building one of these

The : hb comment every ~15 seconds is not decoration. Load balancers close idle connections — 60 seconds is a common default — and a stream that is legitimately quiet (HALTS goes silent by design) looks exactly like an idle one. Without heartbeats, correct quiet behavior gets killed by infrastructure.

An SSE comment is the right tool because it keeps the connection warm without emitting an event the client has to know about. scenario=silent:30s stops the heartbeats too, which is how you test the timeout that is supposed to catch this.

Next