Trade-surveillance desks run at roughly 99.99% false positives. The temptation is to point a language model at the alert backlog and let it triage. But a hallucinated reason-for-escalation in a report filed to a regulator is itself a compliance event. So here the trust boundary is drawn hard: deterministic sequence-and-timing logic computes every signal and assigns escalate-or-dismiss; the model only drafts the report, and only from events the engine already flagged.
Figures are measurements on a synthetic labelled suite. They prove the mechanism, not production coverage. See the honest limits in §6.
PwC's Market Abuse Surveillance Survey of 17 banks found roughly 40 million trade and e-comms alerts raised in twelve months, at a false-positive rate near 99.99%, on the order of one filed report per twelve thousand alerts. The survey's own word for it was "unsustainable." Every analyst hour spent clearing noise is an hour not spent on the one alert that mattered.
So the pitch writes itself: put an LLM on the queue, have it read the alerts and summarise which to escalate. The problem is the artifact at the end. A Suspicious Transaction & Order Report (STOR) goes to the regulator. If the model invents a fact to justify an escalation, say an order ID that never existed or a sequence that never happened, the firm has not saved time. It has manufactured a false regulatory filing. Naive summarisation is not merely unhelpful here. It is a new liability.
Market manipulation is a sequence-and-timing phenomenon. Spoofing is large opposite-side size placed and then cancelled before it can fill, while the actor trades the other way. Layering is several price levels stacked and pulled near-simultaneously. Marking-the-close is aggressive volume concentrated in the closing window that moves the print. News-timing is a directional trade landing just before a public, price-moving headline. None of these is a keyword. Each is an ordered relationship between events with a clock attached. That is exactly what a language model cannot reliably compute, and exactly what it will confidently narrate anyway.
The genuinely hard part is not catching cartoon spoofing. It is not flagging the look-alikes, because legitimate behaviour mimics every pattern:
"Large order, then cancel" → spoofing. "Many price levels" → layering. "Trade near a headline" → insider/news abuse. All three fire on perfectly legal activity.
A market-maker quoting both sides and actually filling (low cancel-to-fill). An iceberg whose child slices sit at one level, each filled before the next. A trade landing after a public print, which is a reaction rather than front-running.
So the real job is precise: parse the event stream, compute cancel-to-fill ratios, distinct-level counts, close-window concentration and signed news-to-trade latency, and threshold them. Then prove the thresholds have teeth by running them against deliberately adversarial benign cases.
This is the load-bearing decision, so it's drawn precisely. Every signal and every escalate/dismiss verdict is computed in deterministic code from the event stream. Each one is reproducible, re-runnable, and auditable to the millisecond. The "LLM" (here a deterministic stub, so the demo runs offline) is strictly downstream and read-only: it drafts the STOR about a verdict the engine already produced, and it may emit a clause only if it can cite the flagged event. Where no event exists, it writes "abstained: no evidence." A model never decides whether manipulation occurred.
Here is the actual decision function the engine runs on every episode. The thresholds live in a published thresholds.json contract. Nothing is a magic number buried in code, and re-running the seed reproduces every verdict and every hash:
// the load-bearing decision , no model anywhere in it. function score(f) { const fired = []; const sp = TH.spoofing; if (f.cancel_to_fill_ratio >= sp.min_cancel_to_fill_ratio && f.cancelled_qty >= sp.min_cancelled_qty && f.max_resting_ms <= sp.max_resting_ms) // placed & pulled before fill fired.push({ sig: "spoofing", why: `cancel/fill ${f.cancel_to_fill_ratio}×` }); const mc = TH.marking_close; if (f.close_window_share >= mc.min_close_window_share && // concentrated in the close f.close_trades >= mc.min_close_trades && f.px_impact_bps >= mc.min_px_impact_bps) // and it moved the print fired.push({ sig: "marking_close", why: `+${f.px_impact_bps}bps` }); const nw = TH.news_timing; if (f.news_to_trade_ms >= 0 && // trade BEFORE the print (signed) f.news_to_trade_ms <= nw.max_news_to_trade_ms && f.pre_news_qty >= nw.min_qty) fired.push({ sig: "news_timing", why: `${f.pre_news_qty} qty pre-print` }); // ... layering & wash checks omitted for length ... return fired; // disposition = fired.length ? "escalate" : "dismiss" }
The signed news_to_trade_ms captures the whole idea in miniature. A positive value means the trade landed before the public print, which is suspicious. A negative value means it landed after, which is a reaction. A keyword model sees "trade near news" and flags both. The clock tells them apart.
Take episode EP-01, account Acct-7731. The engine sees six large bids placed below the touch, a sell that lifts the offer, then all six bids cancelled within ~1.5s without a single fill: cancel-to-fill far over threshold, qty over threshold, resting time under the fleeting-order limit. Spoofing fires. Only then does the drafter write the STOR, quoting that exact account and episode, and for every signal class that did not fire, it prints "abstained: no evidence" rather than padding the report.
| Episode | Pattern | What the engine measured | Verdict | Why |
|---|---|---|---|---|
| EP-01 | Spoofing | cancel/fill 6× · 9,000 qty pulled · <2s resting · no fills | escalate | over every spoofing threshold |
| BN-01 | Market-making | quotes both sides, fills → cancel/fill ≈ 0 | cleared | genuine two-sided liquidity |
| BN-02 | Iceberg | 1 price level, each child slice filled before the next | cleared | not enough distinct levels to layer |
| BN-03 | News trade | buy lands 22s after the public results headline | cleared | news-to-trade is negative, so it is reacting |
The deepest risk in any synthetic demo is circularity: author both the data and the rules and the headline number is "true by construction." The mitigation here is a seeded, ground-truth-labelled suite of 200 episodes: 20 injected manipulations plus 180 benign-but-suspicious hard negatives (legitimate market-making, iceberg slicing, genuine cancels on price moves, real news-driven trades). The same deterministic rules score all 200; the confusion matrix is computed live in the browser, and a third party who re-runs the seed gets byte-identical data and identical verdicts.
On that suite the engine catches 20 of 20 injected manipulations (recall 100%) with 3 false alarms across the 180 benign episodes (precision ~87%). The three false positives are not hidden. They are seeded ambiguous edge cases (an aggressive-but-legitimate market-making burst, a multi-venue child-order spray, a delayed news feed) that I deliberately pushed across the threshold, so the demo shows a non-zero, honest false-positive count rather than a too-clean 100/100.
seq : 1 agent : Sentinel action : file_stor subject : EP-01 (Acct-7731) disp : escalate evidence : ["spoofing"] decision : file STOR (analyst approved) prev_hash : 707dd3b8 hash : c1f4a9e2 ← = fnv1a(prev_hash + canonical(entry))
The hard problem is not a surveillance model that escalates. It is one that declines to escalate the look-alikes. So I put the decision in code I can re-run to the millisecond, wrote the thresholds into a published contract, and held the model on the far side of a hard line: downstream, read-only, and forbidden from writing a single clause it cannot cite. Then I did the thing a demo can dodge and an examiner cannot. I built an adversarial labelled set, let the engine post a non-zero false-positive count, and stated plainly what synthetic data can and cannot prove. In a filing that goes to a regulator, making the model cite its evidence or abstain is not a nicety. It is what makes the output safe to file.
Sources. PwC, Market Abuse Surveillance Survey (17 banks; ~40M alerts / 12 months; ~99.99% false positives). EU Market Abuse Regulation (MAR), Suspicious Transaction & Order Report (STOR) requirements. MAS market-conduct / market-abuse rules under the Securities and Futures Act.