CUCKOOTRADE
Guide

How to test a trading bot against a market crash, halt, or gap

The market conditions that break trading software are exactly the ones you cannot schedule. Here is how to force a 25% drawdown, a mid-session halt, a 12% overnight gap, or a feed that freezes while looking perfectly healthy — on a Tuesday afternoon, reproducibly.

Any language No key required 10 minutes

The untested path is the one that matters

Trading software spends almost all of its life in ordinary market conditions, so that is what gets tested. The code that runs during a crash — the risk limits, the circuit breaker, the reconnect, the alert that should page someone — runs for the first time during an actual crash.

You cannot ask a real data provider for a crash. You can wait for one, which is not a test plan, or you can hand-build fixtures, which takes real effort and encodes your own assumptions about what a crash looks like — and those fixtures never cover the case nobody thought of, like a feed that keeps ticking with the same price.

A scheduled catastrophe. CuckooTrade reserves a set of ticker symbols that return scripted market behavior. Each is anchored to the calendar rather than to your query window, with one guarantee: the signature behavior appears in any 30-day view. Ask for the last month of CRASH and there is always a crash in it.

A crash, right now

shell — the last 30 days of CRASH
curl 'https://cuckootrade.com/api/v1/alpaca/v2/stocks/bars?symbols=CRASH&timeframe=1Day'

Roughly a 25% drawdown over a handful of sessions, then a slow grind back. Because it is deterministic, the drawdown is in the same place every time you run the test — you can assert on it:

python — a real assertion about a crash
import httpx

URL = "https://cuckootrade.com/api/v1/alpaca/v2/stocks/bars"


def closes(symbol, **extra):
    r = httpx.get(URL, params={"symbols": symbol, "timeframe": "1Day",
                               "start": "2026-07-01", "end": "2026-07-31",
                               **extra})
    r.raise_for_status()
    return [bar["c"] for bar in r.json()["bars"][symbol]]


def max_drawdown(series):
    peak, worst = series[0], 0.0
    for price in series:
        peak = max(peak, price)
        worst = min(worst, price / peak - 1)
    return worst


def test_risk_limit_trips_on_a_crash():
    series = closes("CRASH")
    assert max_drawdown(series) < -0.15      # the crash is really there
    # ... now feed `series` to your risk engine and assert it halted trading

That test does not expire. Next year, same window, same numbers.

The full catalogue

Each ticker isolates one failure mode, so a red test names the bug rather than just reporting that something broke.

TickerBehaviorThe bug it finds
CRASH~25% drawdown mid-month, slow recovery.Risk limits, circuit breakers, drawdown alerts.
MOONParabolic run-up, then a hard correction.Position sizing, take-profit logic, y-axis rescaling.
GAPPY±5–15% overnight gaps most days.Stop orders that assume continuity; overnight risk.
HALTSMinute bars absent during halt windows.Gap handling; loops that assume one bar per minute.
STALEPrice frozen, v=0, timestamps advancing.Freshness checks. See below — this is the cruel one.
SPIKEYSingle-minute wicks that instantly revert.Indicators and alerts triggered by one bad print.
FLATZero-range bars pinned at $100.00.Chart autoscaling and any (high - low) divisor.
PENNY~$0.30 prices with four decimals.Float precision, rounding, currency formatting.
CHOPPYHigh volatility, zero net drift.Mean-reversion logic; overtrading on noise.
SPLITSMonthly 2:1 split that rewrites prior closes.Reconciliation against stored history.
DIVVYA dividend adjustment landing five sessions late.The small restatement that slips past a naive check.
REVISEDA bad print carried until the exchange busts it.Vendor restatements with no corporate action to explain them.
shell — several at once
curl 'https://cuckootrade.com/api/v1/alpaca/v2/stocks/bars?symbols=CRASH,GAPPY,SPIKEY,FLAT&timeframe=1Day'

The one nobody tests: a feed that lies

Most code checks did I get a response. Very little checks is this response current. A stale feed is worse than a dead one, because every liveness check you have reports green while your positions are being managed against a price from twenty minutes ago.

STALE and HALTS are deliberate opposites, and the pairing is the point:

HALTSSTALE
Bars in windowAbsent.Present: v=0, price unchanged.
StreamSilent — heartbeats only.Ticking, with a frozen timestamp.
Naive client sees“Nothing arrived.”“Everything’s fine.”
CatchesGap handling, reconnect.Freshness checks.
shell — a feed that freezes mid-session
# minute bars: watch volume go to zero while the clock keeps moving
curl 'https://cuckootrade.com/api/v1/alpaca/v2/stocks/bars?symbols=STALE&timeframe=1Min&start=2026-07-15&end=2026-07-16'

# on the stream: punctual ticks, frozen `t`, healthy heartbeats
curl -N 'https://cuckootrade.com/api/v1/stream?symbols=STALE'

One catch-up bar absorbs the whole move when the window ends, and the last minute of the session is never stale — so the day always closes on a real print and the session volume is never zero. If your code only notices the problem after the catch-up bar arrives, it noticed too late.

History that changes underneath you

If you store bars in a database, you have a reconciliation problem whether or not you have a reconciliation job. Real feeds restate: a split or a late dividend rewrites bars you already saved.

as_of models this without giving up determinism. Pin it and the bytes are frozen forever; omit it and history moves the way a real vendor’s does. So you can test a restatement by moving as_of across the announcement instead of waiting a month:

shell — the same window, either side of a split
BASE='https://cuckootrade.com/api/v1/alpaca/v2/stocks/bars'
W='symbols=SPLITS&timeframe=1Day&start=2026-06-01&end=2026-06-30'

curl -s "$BASE?$W&as_of=2026-07-09" | md5sum   # before the announcement
curl -s "$BASE?$W&as_of=2026-07-13" | md5sum   # after -- different bytes

# and what changed, with announce/ex/process dates
curl 'https://cuckootrade.com/api/v1/corporate-actions?symbols=SPLITS,DIVVY'

DIVVY is the cruel one: its adjustment is 1–2%, small enough to slip straight past a “did anything move more than 10%” sanity check, and it lands about five sessions after the ex-date. Every bar response reports X-Cuckoo-Restated: N actions applied, so you can see whether the window you asked for was actually rewritten.

Query behind the ex-date. An action only rewrites bars dated before it, and omitting start/end defaults to the last 30 days — often the wrong window to see a restatement in. If you get 0 actions applied, move the window back.

And when the connection itself breaks

Scenario tickers break the data. They never break the wire — scripted tickers always return well-formed responses, on purpose. To break the transport, ask explicitly:

shell — a crash on a connection that also fails
curl -i 'https://cuckootrade.com/api/v1/alpaca/v2/stocks/bars?symbols=CRASH&scenario=flap:2'

Nothing fires without scenario=, and every fault is deterministic. Testing retry logic and API failures →

What this cannot tell you

These scenarios test your code, not your strategy. The prices are generated, so a strategy that survives CRASH has survived one scripted synthetic drawdown — it has learned nothing about real markets, and a profitable backtest here means exactly nothing. What you can conclude is that your risk limit fired, your reconnect worked, your chart did not divide by zero, and your alert went out. That is worth having, and it is the whole claim.

Next