WTWilly Tai
Case studyCapital-markets surveillance · MAR / MAS

Surveillance where the order book decides.

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.

20 / 20
manipulations caught
On the held-out synthetic labelled suite.
3
false alarms / 180 benign
Across adversarial near-misses.
100%
clauses cited-or-abstained
No clause without an event.
23 / 23
ledger replay verified
Tamper-evident, re-derivable.

Figures are measurements on a synthetic labelled suite. They prove the mechanism, not production coverage. See the honest limits in §6.

01 / The pain

Forty million alerts, almost all of them wrong.

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.

02 / Why naive AI fails here

The signal is in the timing, and the look-alikes are everywhere.

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:

What a keyword model flags

"Large order, then cancel" → spoofing. "Many price levels" → layering. "Trade near a headline" → insider/news abuse. All three fire on perfectly legal activity.

What the timing logic clears

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.

03 / The trust boundary

Code computes the disposition. The model only writes the report.

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.

DETERMINISTIC · computed, reproducible, decides MODEL · language only, read-only, cited Event streamquotes·orders·cancels·trades·news Signal engine5 rules · the clock Dispositionescalate / dismiss Alerttyped STOR draftcite-or-abstain Analyst gateapproves filing Append-only, hash-chained ledgerevery disposition, draft and approval written here · tamper-evident · replayable
fig.1 · the trust boundary: the engine decides, the model phrases, the human files, and the ledger remembers

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.

04 / Worked example

One escalation fully cited, and three look-alikes correctly cleared.

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.

EpisodePatternWhat the engine measuredVerdictWhy
EP-01Spoofingcancel/fill 6× · 9,000 qty pulled · <2s resting · no fillsescalateover every spoofing threshold
BN-01Market-makingquotes both sides, fills → cancel/fill ≈ 0clearedgenuine two-sided liquidity
BN-02Iceberg1 price level, each child slice filled before the nextclearednot enough distinct levels to layer
BN-03News tradebuy lands 22s after the public results headlineclearednews-to-trade is negative, so it is reacting
The discrimination is the point. Three of these four would trip a keyword filter. The timing logic clears all three and still catches the real spoof, because it measures the relationship between events rather than the presence of words. In the live demo, toggling "reveal benign near-misses" drops BN-01 to BN-03 into the queue and the engine clears each one, on the same rules, in front of you.
05 / The measurement

A held-out labelled suite, not a circular figure.

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.

06 / Honest limits

What these numbers do not prove.

AI Control Ledger · entryintegrity verified · replay 23/23
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))
Each hash covers its contents AND the previous hash. Flip a decision or an evidence list after the fact and the chain breaks at exactly that seq, which the live demo's "Tamper" button does precisely.

Why I built it this way.

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.

See it run → Scrub the order book, click an alert to trace its citations, run the labelled suite, and tamper with the ledger to watch the chain break.

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.