When to Use
Use this skill when a firm runs order-generating processes whose combined output could, without anyone intending it, produce the shape of a wash trade or of layering — and the firm wants to see that shape before a regulator or exchange does.
Two patterns are screened on the firm's own event stream:
- Self-match / wash trade. An incoming order that would cross the firm's own resting order on the opposite side of the same instrument, under the same beneficial owner. The prohibition is CEA s.4c(a)(1) and (2)(A) (7 U.S.C. 6c(a)) for futures and CME Rule 534 on Globex; FINRA Rule 5210.02 governs self-trades in securities.
- Layering / spoofing. An execution on one side accompanied by the withdrawal of materially larger same-owner size on the other. CEA s.4c(a)(5)(C) (added by Dodd-Frank s.747) names spoofing as "bidding or offering with the intent to cancel the bid or offer before execution"; FINRA Rule 5210.03 Type 1 describes the securities shape; CME Rule 575 is the Globex analogue.
It is the streaming, US-centric counterpart to the batch EU screening in
eu-market-abuse-regulation-mar-surveillance, and the intent-layer companion to the
venue-side mechanism in exchange-self-match-prevention-configuration.
When NOT to Use
- Not as the mechanism that prevents a self-match. This module observes; the venue
prevents. Configure native SMP/STP through
exchange-self-match-prevention-configurationand treat this engine's output as the audit and calibration layer around it. CME's own guidance treats SMP as a preventive tool, not a defence — self-matching on more than an incidental basis may still be deemed to violate Rule 534. - Not as a determination that a rule was broken. Wash trades under CEA s.4c(a) turn on intent; CME Rule 534 applies a "knew or should have known" standard; spoofing under s.4c(a)(5)(C) requires scienter — the CFTC's interpretive guidance (78 FR 31890, 28 May 2013) states that reckless conduct does not violate the provision. Intent is not in the order stream. Every output here is an indicator for a human analyst.
- Not a substitute for the venue's own view. The engine sees the firm's orders as the firm sent them. Rejects, venue-side modifications and queue position are invisible; reconcile against execution reports.
- Not cross-venue or cross-instrument. Alerts group by (beneficial owner, instrument). Manipulation of a future through its underlying, or across two venues, is out of scope.
- Not the EU filing path. A suspicion arising in EU/EEA instruments becomes a STOR
under MAR Article 16 and Delegated Regulation (EU) 2016/957 — see
eu-market-abuse-regulation-mar-surveillance. - Not a position-netting or fee-saving tool. To cross opposing internal orders
deliberately rather than detect them accidentally, see
multi-order-netting-before-routing.
Prerequisites
- Python 3.10+, standard library only.
- An order event stream carrying
event_id,order_id,trader_id,account_id,symbol,side,quantity,action(PLACE/CANCEL/FILL) and a timezone-awaretimestamp. Naive timestamps are rejected: sub-second lifespan logic on an ambiguous clock is not defensible, and MiFID II RTS 25 (Delegated Regulation (EU) 2017/574) bounds business-clock divergence from UTC at 100 microseconds for HFT. pricewhere the order carries one.Nonemeans unpriced — a market order (which crosses every own level) or a bare cancel.- An account → beneficial owner map. Without it ownership is a string comparison and the cross-account self-cross this skill exists to catch is invisible. Ownership is supplied, never inferred.
strategy_idper order where known, for the FINRA Rule 5210.02 relatedness test.- Detection parameters calibrated to the firm's own microstructure. The defaults (90% cancel ratio, 1,000 ms window, 3.0x size ratio, 2 layered orders) are library heuristics, not regulatory thresholds — no regulator prescribes a number.
Workflow
- Map beneficial ownership before anything else. Construct
WashTradeAndSpoofingDetectionEngine(beneficial_owner_map={...})keyed by account id (trader id also resolves). Every detector and every metric groups by owner, because grouping by raw trader id dilutes one manipulator's activity across the accounts it trades and hides same-owner/different-account crossing entirely. - Feed every event through
ingest_order_event. It validates, screens, and returns the alerts that event raised. The check methods are called by it; call them directly only for a pre-trade query, and note thatcheck_spoofing_pattern_on_fillopens detector state as a side effect.- Decision point — a duplicate
event_idis an error, not a warning. A replayed event inflates both the cancellation ratio and the withdrawn-size test, so the engine raises rather than double-counting. - Decision point — a naive timestamp is rejected outright. Silently assuming UTC turns a clock bug into a fabricated sub-second lifespan.
- Decision point — a duplicate
- Read the self-match alert as a crossing test, not a price-equality test. A resting
own bid at 150.05 is reached by an incoming own offer at 150.00; the venue matches them
at 150.05. An equality test reports nothing and the print still happens. Unpriced orders
cross every own level.
- Decision point — a resting order's age does not make it safe. The self-match
window defaults to
None(any resting order). Settingwash_trade_window_secondsnarrows it to a "matched trade" pattern and buys false negatives; do that only deliberately.
- Decision point — a resting order's age does not make it safe. The self-match
window defaults to
- Read severity as review priority, not legal conclusion. A self-cross between two
known and different
strategy_idvalues is reported at MEDIUM, because FINRA Rule 5210.02 generally treats self-trades from unrelated algorithms as bona fide. It is never suppressed — CME Rule 534 carries no equivalent carve-out, and an unknownstrategy_idis treated conservatively as related. - Let the layering context settle before judging a fill. FINRA Rule 5210.03 Type 1
places the cancellations after the opposite-side execution, so a fill cannot be scored
when it arrives.
check_spoofing_pattern_on_fillopens a context recording the opposite-side orders resting at that instant; subsequent cancels attach to it throughcheck_layering_on_canceland the alert fires when the count and size tests are met.- Decision point — size and count are what separate layering from quoting. A
two-sided market maker cancels opposite-side size around nearly every fill. Requiring
layering_size_ratio(default 3.0x the executed quantity) acrossmin_layered_orders(default 2, matching Rule 5210.03's "multiple limit orders") is what keeps the detector from firing on ordinary quote maintenance.
- Decision point — size and count are what separate layering from quoting. A
two-sided market maker cancels opposite-side size around nearly every fill. Requiring
- Treat the cancellation ratio as hygiene, and let it latch. It fires once per owner
and stays latched until
reset_cancellation_ratio_alert; re-emitting on every subsequent event buries the alert that mattered. It is cancels/placements — not the MiFID II RTS 9 order-to-trade ratio, which Delegated Regulation (EU) 2017/566 places on the trading venue, per member and per instrument, on both volumes and numbers. - Escalate to a human, and retain the parameters. Every alert carries
requires_human_review=True, anindicator_referenceand a deterministicalert_id. Persist the detection parameters beside the alerts: a threshold you cannot reconstruct years later is a threshold you cannot defend.
Full procedure: see
references/workflows.md. Legal sources and calibration guidance: seereferences/standards.md. Printable sign-off checklist: seeassets/checklist.md.
Common Pitfalls
- Testing price equality instead of crossing. The wash-trade condition is that the orders would match, not that they carry the same limit. A resting own bid better than the incoming own offer executes; equality testing misses every such print and reports a clean book.
- Comparing raw ids instead of resolving the beneficial owner. Wash-trade exposure attaches to the owner. Two desks with different trader ids and different account ids under one entity self-cross, and an id comparison finds nothing. The mirror-image error is matching on a shared generic account string and flagging unrelated participants.
- Scoring a fill at the moment it arrives. In the Rule 5210.03 Type 1 shape the cancellations follow the execution, so the decisive evidence does not exist yet. A detector that only inspects history at fill time never sees the canonical pattern.
- Alerting on any opposite-side cancellation near a fill. Without a size and count test this is a market-maker alarm, not a surveillance control — and an alert channel that cries wolf all day is the one nobody reads on the day it is right.
- Emitting "VIOLATION" from an automated detector. Spoofing under CEA s.4c(a)(5)(C) requires scienter and FINRA Rule 5210.03 requires a frequent pattern. Labelling a single indicator a violation both overstates the finding and, in a produced record, hands a regulator the firm's own characterisation of its conduct.
- Re-emitting a threshold alert on every subsequent event. A latched ratio breach that fires ten thousand times is the same information ten thousand times, and it displaces everything else in the queue.
- Deriving alert ids from a whole-second timestamp. Two alerts in the same second collide, and an id that cannot be cited is not an audit trail. Ids here are derived from the contributing event ids and are stable across reruns.
- Assuming SMP makes the exposure go away. CME's guidance treats self-match prevention as a preventive tool, not a defence; it does not operate during the Globex pre-open, and self-matching on more than an incidental basis may still be deemed to violate Rule 534.
- Citing "CFTC Rule 1.38" as the wash-trade prohibition. 17 CFR 1.38 is "Execution of transactions", the competitive-execution requirement. The wash-sale prohibition is CEA s.4c(a)(1),(2)(A) (7 U.S.C. 6c(a)). A wrong citation in a compliance record is worse than no citation.
- Assuming one retention period. MiFID II RTS 6 Article 28 requires order records for five years; US broker-dealer retention runs on SEA Rule 17a-4 and FINRA Rule 4511(b) (six years where no other period applies). OATS was retired on 1 September 2021 — the US order audit trail is CAT under SEC Rule 613.
Verification
- Crossing, not equality: a resting BUY at 150.05 followed by an own SELL at 150.00 must raise exactly one wash-trade alert; a resting BUY at 149.00 with an own SELL at 150.00 must raise none. An unpriced order must cross every own level.
- Ownership: with
beneficial_owner_map={"ACC1": "ENTITY_A", "ACC2": "ENTITY_A"}, a BUY fromT1/ACC1crossed by a SELL fromT2/ACC2must alert withbeneficial_owner_id == "ENTITY_A". Without a map, the same two events must not alert. - Age: a resting order placed an hour earlier must still self-match under the default
wash_trade_window_seconds=None, and must not when a 2.0-second window is set. - Relatedness: distinct known
strategy_idvalues downgrade the alert toMEDIUMwithout suppressing it; equal or unknown values giveCRITICAL. - Layering after the fill: three 5,000-lot offers, a 100-lot BUY execution, then the
offers pulled — the first cancel must raise nothing (one order is not "multiple") and the
second must raise one
HIGHalert withpattern_shape == CANCEL_AFTER_FILL. Further cancels must not re-alert. - Market-maker negative check: two 100-lot offers withdrawn around a 100-lot fill (2.0x, below the 3.0x default) must raise nothing. Same-side cancels, and cancels outside the window, must raise nothing.
- Metrics: ten placements and nine cancels give
cancellation_ratio_pct == 90.0; cancels at 100 ms and 300 ms giveavg_order_lifespan_ms == 200.0; a cancel whose placement was never seen incrementsunmatched_cancelsand is excluded from the average, as is a cancel timestamped before its own placement. - Latching: the ratio alert appears exactly once even as the ratio climbs to 100%, and
reappears only after
reset_cancellation_ratio_alert. - Audit trail: two self-crosses within the same second must have different
alert_idvalues, and the same stream replayed on a fresh engine must produce identical ids. - Negative checks — each must raise
SurveillanceError: a naive timestamp, a duplicateevent_id, re-use of a liveorder_id, a non-positive or non-finite price or quantity, a blank identifier, a non-enumsideoraction, and a non-aware cutoff passed toexpire_orders_before. A rejected event must leave no state behind. - Run
python -m unittest discover -s skills/wash-trade-and-spoofing-self-detection/scriptsand confirm a 100% pass rate.
Related Skills
exchange-self-match-prevention-configurationeu-market-abuse-regulation-mar-surveillancemulti-order-netting-before-routingorder-to-trade-ratio-fee-penalty-avoidancemifid-ii-algo-trading-compliance-eusec-rule-15c3-5-risk-controls-usuk-fca-algorithmic-trading-systems-controlscross-account-aggregate-risk-view