Manipulation detectors

Spoofing, layering, quote-stuffing, wash trading, momentum ignition, and iceberg/reload detection. Each tied to a regulatory or academic source.

Six detectors ship in v0.1. Each implements the Detector protocol (detect(events, context) -> Iterable[AnomalyFinding]) and carries the canonical citation for its heuristic in the AnomalyFinding.citation field. Compliance reviewers open that citation to verify the detection methodology before relying on the finding.

The FlowEngine fans out incoming MarketEvents across all registered detectors; each maintains its own bounded state. Detector failures are swallowed (never break ingest); test coverage catches logic errors well before production.

Detector protocol

python
from typing import Iterable, Protocol, runtime_checkable
from horizon.flow.events import AnomalyFinding, DetectorContext, MarketEvent

@runtime_checkable
class Detector(Protocol):
 name: str
 def detect(
 self,
 events: Iterable[MarketEvent],
 context: DetectorContext,
 ) -> Iterable[AnomalyFinding]: ...

DetectorContext carries now, venue_name, recent_book (top-N orderbook snapshots per market), recent_trades, and actor_profiles (if a profile extractor is attached). Detectors consume whichever slice they need and ignore the rest.

Spoofing

Reference. Lee, Eom, Park (2013). Journal of Financial Markets, 16(2), 227–252.

Pattern. Place a large “bait” on the side opposite your actual intent, wait for the book imbalance to shift the mid, aggress on your true side, cancel the bait before anyone fills it.

Heuristic. Detected when all of:

  • An OrderPlaced with quantity ≥ min_bait_size while the top-of-book imbalance is ≥ min_book_imbalance.
  • Within cancel_window_ms of that placement: an OrderFilled on the OPPOSITE side by the SAME actor, with size at most bait_size / bait_to_aggressor_ratio.
  • A subsequent OrderCanceled of the bait before it materially fills.

Config. SpoofingConfig. Defaults calibrated for prediction-market cadence (cancel_window_ms=2000, bait_to_aggressor_ratio=5.0, min_bait_size=500.0). Tighten cancel_window_ms to 100–500 ms for equities.

Confidence. Composite of cancel speed + size ratio + book imbalance at placement. Severity escalates at 0.7 (High) and 0.85 (Critical).

python
from horizon.flow.config import FlowConfig
from horizon.flow.detectors import SpoofingDetector

det = SpoofingDetector(FlowConfig())

Layering

Reference. FINRA Rule 5210 + FINRA Regulatory Notice 13-39. SEC Release No. 34-75710.

Pattern. Like spoofing, but uses multiple price levels instead of one large order. Stack orders at several nearby levels on one side, shift the mid, aggress on the other side, cancel the stack.

Heuristic. Detected when:

  • min_layers OrderPlaced events from the same actor, same market, same side.
  • Prices within max_layer_spacing_bps of each other (a tight cluster).
  • All canceled within cancel_within_ms.
  • No more than max_fills_tolerated of them filled (default 0. Any meaningful fill disqualifies).

Config. LayeringConfig. Defaults min_layers=3, max_layer_spacing_bps=20, cancel_within_ms=3000. Tune cancel_within_ms down for tighter venues.

Confidence. Weighted by layer count, spacing tightness, cancel-window speed.

Quote stuffing

Reference. Egginton, Van Ness, Van Ness (2016). Financial Management, 45(3), 583–608.

Pattern. Burst of place+cancel messages at a rate well above baseline, with a fill rate near zero. Intended to clog the market-data channel for competitors or manipulate other participants’ microstructure signals.

Heuristic. Per (market_id, actor_id) bucket (also per-market when actor_id is absent):

  • Rolling min_burst_duration_s window.
  • min_msgs_per_sec messages (placements + cancels + amends).
  • Fill rate ≤ max_fill_rate.

Config. QuoteStuffingConfig. Defaults min_msgs_per_sec=20, min_burst_duration_s=5, max_fill_rate=0.05. Prediction markets are slower than equities; these thresholds are conservative for Polymarket.

De-duplication. After a finding fires for a given key, a second one won’t emit for the same burst-duration window. Prevents runs of duplicate findings during a sustained stuffing event.

Wash trade

Reference. Cong, L. W., Li, X., Tang, K., Yang, Y. (2023). Management Science, 69(11), 6427–6454. The authors estimate over 70% of reported volume on unregulated crypto exchanges is wash.

Pattern. Traders (often the same party using two wallets) buy and sell between themselves to inflate volume and manipulate price-discovery metrics.

Heuristic. Three composite signals, evaluated over a rolling window_s:

  1. Round-number clustering. Fraction of trades with “clean” sizes (multiples of 10, 100, 1000; powers of 10; specific exact values like 0.5, 1.0, 2.0, 5.0) ≥ round_number_bias_threshold.
  2. Benford chi-squared. Χ² test of leading-digit distribution against Benford’s expected, threshold benford_chi2_threshold (default 15.0, roughly the p=0.05 critical value for 9 bins).
  3. Same-origin on-chain. Trades where buyer and seller are in the same wallet cluster (via WalletHeuristicLinker). Counts ≥ min_same_origin_pairs.

A finding fires when any single signal trips strongly, or when two trip moderately. All three firing = Critical severity.

Config. WashTradeConfig. Defaults round_number_bias_threshold=0.35, benford_chi2_threshold=15.0, min_same_origin_pairs=3, window_s=300.

python
from horizon.flow.actors.wallet_heuristics import WalletHeuristicLinker
from horizon.flow.detectors import WashTradeDetector

linker = WalletHeuristicLinker(config)
det = WashTradeDetector(config, wallet_linker=linker)

Without a linker, signal 3 (same-origin) is skipped; the detector still emits findings based on signals 1 and 2.

Momentum ignition

Reference. Li, T., Shin, D., Wang, B. (2023). CFTC banging-the-close guidance. SEC Release No. 34-61358.

Pattern. Aggressive trade designed to spark a momentum move; once followers pile in and price moves further, the instigator reverses at the new price.

Heuristic.

  1. Aggressive fill by actor A, size ≥ min_aggressor_size.
  2. Price moves ≥ min_price_move_bps within reversal_window_s.
  3. Actor A reverses (opposite side) with size ≥ reversal_size_ratio × original_aggressor_size before the window closes.

Config. MomentumIgnitionConfig. Defaults min_aggressor_size=1000, min_price_move_bps=15, reversal_window_s=30, reversal_size_ratio=0.5.

Implementation detail. The detector calls _maybe_reverse BEFORE _maybe_start so that a large opposite-side fill first closes the pending ignition (allowing the finding to emit) rather than overwriting it with a new same-actor ignition on the other side.

Iceberg / reload

Reference. Hautsch, N., Huang, R. (2012). JEDC. Esser, A., Mönch, B. (2007). Finance Research Letters, 4, 68–81. Moinas, S. (2010). Journal of Finance.

Pattern. An “iceberg” posts small visible quantity while the real size sits hidden. Repeated fills at the same level see the visible depth reload rather than shrink, signaling the hidden parent.

Heuristic. Per (market_id, side, price_level) bucket:

  • Track visible size from BookSnapshot events at that level.
  • On an OrderFilled at that level, log the trade size and the surviving visible size from the next book snapshot.
  • Count a “reload” when the next visible size ≥ 80% of the prior visible size despite a material fill (≥ 30% of visible).
  • Emit when reload count ≥ min_reloads (default 3).

Config. IcebergConfig. Defaults min_reloads=3, reload_tolerance_bps=2.0.

Note on actor attribution. Most venues don’t expose who is behind a resting level. On Polymarket and Hyperliquid, the L3 book includes the wallet; the detector captures this in evidence when available but does not require it.

Threshold calibration

Every detector’s config is a dataclass you can override per venue via dataclasses.replace:

python
from dataclasses import replace
from horizon.flow.config import FlowConfig, SpoofingConfig

cfg = FlowConfig()
cfg.detectors.spoofing = replace(
 cfg.detectors.spoofing,
 cancel_window_ms=300, # tighter for equities
 min_bait_size=2_000,
)

The threshold-sensitivity tests in tests/flow/test_detector_thresholds.py exercise each detector’s negative cases. Just-below-threshold inputs that must not trigger. Run them when you tune thresholds to catch regressions.

Citations

  • Lee, E. J., Eom, K. S., Park, K. S. (2013). “Microstructure-based Manipulation: Strategic Behavior and Performance of Spoofing Traders.” Journal of Financial Markets, 16(2).
  • Egginton, J. F., Van Ness, B. F., Van Ness, R. A. (2016). “Quote Stuffing.” Financial Management, 45(3).
  • Cong, L. W., Li, X., Tang, K., Yang, Y. (2023). “Crypto Wash Trading.” Management Science, 69(11).
  • Li, T., Shin, D., Wang, B. (2023). “Cryptocurrency Pump-and-Dump Schemes.” Journal of Financial and Quantitative Analysis.
  • Hautsch, N., Huang, R. (2012). “The market impact of a limit order.” JEDC.
  • Esser, A., Mönch, B. (2007). “The navigation of an iceberg.” Finance Research Letters, 4, 68–81.
  • Moinas, S. (2010). “Hidden liquidity: Some new light on dark trading.” Journal of Finance.
  • FINRA Rule 5210 + Regulatory Notice 13-39; SEC Release No. 34-75710; CFTC spoofing / banging-the-close guidance.