Crypto Market Surveillance: Real-Time Pattern Detection

Crypto Market Surveillance: Real-Time Pattern Detection

Market manipulation in crypto—spoofing, layering, momentum ignition, and quote stuffing—happens in milliseconds and leaves on-chain and order-book traces that streaming SQL can detect in real time, giving exchanges and regulators the evidence trail they need before manipulators erase their tracks.

Why Crypto Market Surveillance Matters

Crypto markets are 24/7, globally distributed, and until recently lightly surveilled. The same manipulation patterns that prompted the 2010 Dodd-Frank Act in traditional markets are now prevalent in crypto:

  • Spoofing: placing large orders with no intent to execute, then canceling them once the price moves in the desired direction.
  • Layering: placing multiple orders at different price levels to create a false impression of depth, then canceling all of them.
  • Momentum ignition: executing a rapid sequence of trades to create artificial price momentum, then trading against the market reaction.
  • Quote stuffing: flooding the order book with rapid-fire orders and cancellations to slow down other participants' systems.
  • Wash trading: executing trades between accounts controlled by the same entity to create artificial volume.

Regulators in the US (CFTC, SEC), EU (MiCA framework), and Asia are increasingly scrutinizing these patterns. Exchanges that can demonstrate real-time surveillance infrastructure are better positioned in regulatory discussions than those relying on after-the-fact log analysis.

The surveillance patterns that matter most:

  • Order-to-fill ratio: legitimate market makers have high fill rates; spoofing accounts place and cancel orders with low fill ratios.
  • Cancel-within-milliseconds rate: orders canceled within 100ms of placement are characteristic of layering.
  • Directional momentum followed by reversal: a cluster of same-direction trades followed by an opposite-direction trade of similar size is a momentum ignition signal.
  • Quote stuffing rate: order placement rate per second per account that exceeds normal market-making activity.

How Streaming SQL Solves This

RisingWave ingests order book events and trade executions from Kafka and maintains materialized views of behavioral patterns per account and per instrument. Detection queries run continuously—no scheduled batch jobs, no alert delays.

Building It Step by Step

Step 1: Connect the Data Source

CREATE SOURCE order_events (
    order_id        VARCHAR,
    account_id      VARCHAR,
    instrument_id   VARCHAR,
    event_type      VARCHAR,   -- 'PLACE', 'CANCEL', 'FILL', 'PARTIAL_FILL'
    side            VARCHAR,   -- 'BID', 'ASK'
    price           NUMERIC,
    quantity        NUMERIC,
    filled_quantity NUMERIC,
    time_to_cancel_ms BIGINT,  -- ms between place and cancel, null if not canceled
    event_ts        TIMESTAMPTZ
) WITH (
    connector     = 'kafka',
    topic         = 'exchange.order.events',
    properties.bootstrap.server = 'kafka:9092'
) FORMAT PLAIN ENCODE JSON;

CREATE SOURCE trade_executions (
    trade_id        VARCHAR,
    instrument_id   VARCHAR,
    buyer_id        VARCHAR,
    seller_id       VARCHAR,
    price           NUMERIC,
    quantity        NUMERIC,
    notional_usd    NUMERIC,
    aggressor_side  VARCHAR,   -- 'BID', 'ASK'
    trade_ts        TIMESTAMPTZ
) WITH (
    connector     = 'kafka',
    topic         = 'exchange.trades',
    properties.bootstrap.server = 'kafka:9092'
) FORMAT PLAIN ENCODE JSON;

Step 2: Build the Core Materialized View

Compute behavioral indicators per account over rolling windows:

CREATE MATERIALIZED VIEW account_order_behavior AS
SELECT
    account_id,
    instrument_id,
    COUNT(CASE WHEN event_type = 'PLACE'  THEN 1 END)   AS orders_placed,
    COUNT(CASE WHEN event_type = 'CANCEL' THEN 1 END)   AS orders_canceled,
    COUNT(CASE WHEN event_type IN ('FILL','PARTIAL_FILL') THEN 1 END)
                                                         AS orders_filled,
    COUNT(CASE WHEN event_type = 'CANCEL'
                AND time_to_cancel_ms < 500  THEN 1 END) AS rapid_cancels,
    -- Order-to-fill ratio: high = potential spoofing
    COUNT(CASE WHEN event_type = 'PLACE' THEN 1 END)
    / NULLIF(COUNT(CASE WHEN event_type IN ('FILL','PARTIAL_FILL') THEN 1 END), 0)
                                                         AS order_fill_ratio,
    -- Rapid cancel rate
    COUNT(CASE WHEN event_type = 'CANCEL'
                AND time_to_cancel_ms < 500 THEN 1 END)
    / NULLIF(COUNT(CASE WHEN event_type = 'PLACE' THEN 1 END), 0) * 100
                                                         AS rapid_cancel_rate_pct
FROM order_events
WHERE event_ts > NOW() - INTERVAL '1 hour'
GROUP BY account_id, instrument_id;

CREATE MATERIALIZED VIEW account_trade_behavior AS
SELECT
    buyer_id AS account_id,
    instrument_id,
    COUNT(*)                                    AS buy_count,
    SUM(notional_usd)                           AS buy_volume_usd,
    MAX(trade_ts)                               AS last_buy_ts
FROM trade_executions
WHERE trade_ts > NOW() - INTERVAL '1 hour'
GROUP BY buyer_id, instrument_id
UNION ALL
SELECT
    seller_id,
    instrument_id,
    COUNT(*),
    SUM(notional_usd),
    MAX(trade_ts)
FROM trade_executions
WHERE trade_ts > NOW() - INTERVAL '1 hour'
GROUP BY seller_id, instrument_id;

Step 3: Detection Logic and Alerts

Flag accounts showing manipulation-indicative patterns:

CREATE MATERIALIZED VIEW manipulation_flags AS
SELECT
    ob.account_id,
    ob.instrument_id,
    ob.orders_placed,
    ob.orders_canceled,
    ob.rapid_cancels,
    ob.order_fill_ratio,
    ob.rapid_cancel_rate_pct,
    -- Spoofing flag: very high order-to-fill ratio with rapid cancels
    CASE WHEN ob.order_fill_ratio > 20
          AND ob.rapid_cancels > 10
          THEN TRUE ELSE FALSE END                   AS spoofing_flag,
    -- Layering flag: many orders placed then rapidly canceled
    CASE WHEN ob.rapid_cancel_rate_pct > 80
          AND ob.orders_placed > 30
          THEN TRUE ELSE FALSE END                   AS layering_flag,
    -- Quote stuffing flag: extreme order placement rate
    CASE WHEN ob.orders_placed > 500
          THEN TRUE ELSE FALSE END                   AS quote_stuffing_flag,
    NOW() AS evaluated_at
FROM account_order_behavior ob
WHERE ob.order_fill_ratio > 10
   OR ob.rapid_cancel_rate_pct > 60
   OR ob.orders_placed > 300;

CREATE SINK surveillance_alert_sink AS
SELECT
    account_id,
    instrument_id,
    order_fill_ratio,
    rapid_cancel_rate_pct,
    spoofing_flag,
    layering_flag,
    quote_stuffing_flag,
    evaluated_at
FROM manipulation_flags
WHERE spoofing_flag OR layering_flag OR quote_stuffing_flag
WITH (
    connector                   = 'kafka',
    topic                       = 'compliance.market.surveillance',
    properties.bootstrap.server = 'kafka:9092'
) FORMAT PLAIN ENCODE JSON;

Step 4: Querying Live Results

Pull the current surveillance watchlist sorted by highest order-to-fill ratio:

SELECT
    account_id,
    instrument_id,
    orders_placed,
    orders_canceled,
    rapid_cancels,
    ROUND(order_fill_ratio, 2)          AS order_fill_ratio,
    ROUND(rapid_cancel_rate_pct, 2)     AS rapid_cancel_pct,
    spoofing_flag,
    layering_flag,
    quote_stuffing_flag,
    evaluated_at
FROM manipulation_flags
WHERE spoofing_flag OR layering_flag OR quote_stuffing_flag
ORDER BY order_fill_ratio DESC
LIMIT 10;

Comparison Table

Surveillance ApproachDetection LatencyEvidence QualityRegulatory Auditability
Manual log reviewDaysHighLow (ad hoc)
End-of-day batch scriptsHoursModerateLow
Real-time rules engineSecondsModerateMedium
Streaming SQL (RisingWave)< 1 secondHighHigh (SQL audit trail)

FAQ

What order-to-fill ratio threshold is appropriate for flagging spoofing? There is no universal threshold—it depends on the asset class and market conditions. High-frequency market makers legitimately cancel many orders as they update quotes. A baseline approach: compute the 95th percentile order-to-fill ratio across all accounts for a given instrument, and flag accounts more than 2 standard deviations above that baseline. This adaptive threshold is implementable as a materialized view that joins individual account ratios against the instrument-level distribution.

How do you detect wash trading with streaming SQL? Wash trading leaves a signature in trade_executions: the same entity appears as both buyer and seller (directly or via affiliated accounts). A direct self-trade shows buyer_id = seller_id. Network-level wash trading (using related accounts) requires a related_accounts reference table mapping account IDs to controlling entities, then a join against trade_executions to find pairs where both counterparties map to the same entity.

Can this pipeline produce audit-ready evidence for regulatory submissions? Yes. RisingWave materialized views persist query results and maintain the computation logic in SQL—a format regulators can read. For audit purposes, supplement the surveillance pipeline with an append-only Kafka sink that captures every alert with the full event context (account, instrument, behavioral metrics, timestamp) as an immutable evidence record. This log can be produced on demand for regulatory requests.

Key Takeaways

  • Spoofing, layering, and momentum ignition patterns manifest as statistical anomalies in order-to-fill ratios and cancel timing—detectable in real time with streaming SQL.
  • RisingWave maintains rolling behavioral profiles per account as continuously updated materialized views, enabling instant comparison of current behavior against baseline.
  • Streaming SQL surveillance infrastructure produces SQL-auditable evidence trails that satisfy regulatory examination requirements.
  • Detection latency under one second means surveillance flags appear before manipulators have fully covered their tracks, enabling timely intervention.

Best-in-Class Event Streaming
for Agents, Apps, and Analytics
GitHubXLinkedInSlackYouTube
Sign up for our to stay updated.