Infrastructure·Compliance Reports·Intermediate

Wash Trade Detection

Detect potential wash trading patterns by algorithmically analyzing trade sequences for self-trading, circular trading between controlled accounts, and matched orders that could indicate market manipulation attempts or compliance policy violations requiring investigation and remediation.

complianceinfrastructurepattern-recognition

Compliance & Audit: Detect Potential Wash Trades

Wash trading is a form of market manipulation where an investor simultaneously buys and sells the same financial instruments to create artificial, misleading activity in the marketplace. This notebook implements a modular system to detect such patterns using trade data analysis.

Key Concepts

ConceptDescription
Self-TradingWhen the buyer and seller are the same entity or controlled by the same entity.
Circular TradingA group of accounts trading among themselves to inflate volume.
Round-Trip TradingRapidly buying and selling a position with no change in beneficial ownership.
Volume InflationThe primary goal of wash trading to attract genuine investors.
[1]
!pip install pandas matplotlib seaborn numpy
Requirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2)
Requirement already satisfied: matplotlib in /usr/local/lib/python3.12/dist-packages (3.10.0)
Requirement already satisfied: seaborn in /usr/local/lib/python3.12/dist-packages (0.13.2)
Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (2.0.2)
Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2)
Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2026.2)
Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.3.3)
Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (4.63.0)
Requirement already satisfied: kiwisolver>=1.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.5.0)
Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (26.2)
Requirement already satisfied: pillow>=8 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (11.3.0)
Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (3.3.2)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)
[2]
import logging
import random
import time
from collections import deque
from datetime import datetime, timedelta

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

Function Name: create_detector_state

Initializes the state dictionary for the wash trade detection system.

Parameters:

  • lookback_window (int): Number of trades to keep in the rolling buffer.

Returns:

  • dict: The initialized state.
[3]
def create_detector_state(lookback_window: int = 1000) -> dict:
    """
    Initializes the state for wash trade detection.

    Parameters
    ----------
    lookback_window : int, optional
        Size of the rolling window for trade history, defaults to 1000.

    Returns
    -------
    dict
        A dictionary containing buffers and tracking metrics.
    """
    logger.info(f"Initializing detector state with lookback window of {lookback_window}")
    return {
        'trade_history': deque(maxlen=lookback_window),
        'flagged_trades': [],
        'metrics': {
            'total_processed': 0,
            'total_flagged': 0,
            'suspicious_volume': 0.0
        }
    }

Function Name: detect_self_trades

Checks for immediate self-trades where the buyer and seller IDs match.

Parameters:

  • state (dict): Current detector state.
  • trade (dict): Single trade record.

Returns:

  • dict: Updated state.
[4]
def detect_self_trades(state: dict, trade: dict) -> dict:
    """
    Identifies trades where the buyer and seller are the same.

    Parameters
    ----------
    state : dict
        Current state dictionary.
    trade : dict
        Trade data containing 'buyer_id', 'seller_id', 'price', and 'quantity'.

    Returns
    -------
    dict
        Updated state with flagged trades if self-trade detected.
    """
    state['metrics']['total_processed'] += 1

    if trade['buyer_id'] == trade['seller_id']:
        logger.warning(f"Self-trade detected! Asset: {trade['asset']}, Entity: {trade['buyer_id']}")
        flag = trade.copy()
        flag['reason'] = 'Self-Trade'
        state['flagged_trades'].append(flag)
        state['metrics']['total_flagged'] += 1
        state['metrics']['suspicious_volume'] += (trade['price'] * trade['quantity'])

    state['trade_history'].append(trade)
    return state

Function Name: detect_round_trips

Analyzes the window of history to find round-trip trades (A -> B -> A) within a short timeframe.

Parameters:

  • state (dict): Current detector state.
  • time_threshold_seconds (int): Max time allowed for a round-trip.

Returns:

  • dict: Updated state.
[5]
def detect_round_trips(state: dict, time_threshold_seconds: int = 60) -> dict:
    """
    Detects A-B-A trading patterns in the current trade window.

    Parameters
    ----------
    state : dict
        Current state.
    time_threshold_seconds : int, optional
        Seconds within which the reversal must occur, defaults to 60.

    Returns
    -------
    dict
        State with round-trip violations added to flagged_trades.
    """
    history = list(state['trade_history'])
    if len(history) < 2:
        return state

    latest = history[-1]
    for prev in reversed(history[:-1]):
        time_diff = (latest['timestamp'] - prev['timestamp']).total_seconds()
        if time_diff > time_threshold_seconds:
            break

        # Check if A sold to B and then B sold back to A for similar price/qty
        if (latest['buyer_id'] == prev['seller_id'] and
            latest['seller_id'] == prev['buyer_id'] and
            abs(latest['price'] - prev['price']) / prev['price'] < 0.01):

            logger.info("Round-trip trade pattern detected.")
            flag = latest.copy()
            flag['reason'] = 'Round-Trip'
            state['flagged_trades'].append(flag)
            state['metrics']['total_flagged'] += 1
            break

    return state

Demonstration: Simulation and Visualization

We will simulate a series of trades including normal behavior and injected wash trade patterns.

[6]
def simulate_market_data(n_trades: int = 100):
    data = []
    start_time = datetime.now()
    for i in range(n_trades):
        # Random jitter for timing
        start_time += timedelta(seconds=random.randint(1, 5)) + timedelta(milliseconds=random.random() * 100)

        # Normal trade
        trade = {
            'timestamp': start_time,
            'asset': 'BTC/USD',
            'buyer_id': f"user_{random.randint(1, 20)}",
            'seller_id': f"user_{random.randint(1, 20)}",
            'price': 50000 + random.uniform(-100, 100),
            'quantity': random.uniform(0.1, 2.0)
        }

        # Inject Wash Trade at index 50 (Self trade)
        if i == 50:
            trade['buyer_id'] = "malicious_actor"
            trade['seller_id'] = "malicious_actor"

        # Inject Round Trip at index 80-81
        if i == 81:
            prev = data[-1]
            trade['buyer_id'] = prev['seller_id']
            trade['seller_id'] = prev['buyer_id']
            trade['price'] = prev['price']

        data.append(trade)
    return data

# Run simulation
trades = simulate_market_data(200)
detector = create_detector_state()

for t in trades:
    detector = detect_self_trades(detector, t)
    detector = detect_round_trips(detector, time_threshold_seconds=30)

# Visualization
df_all = pd.DataFrame(trades)
df_flagged = pd.DataFrame(detector['flagged_trades'])

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10))

sns.lineplot(data=df_all, x='timestamp', y='price', ax=ax1, label='Market Price')
if not df_flagged.empty:
    ax1.scatter(df_flagged['timestamp'], df_flagged['price'], color='red', zorder=5, label='Flagged Wash Trade')
ax1.set_title('Market Activity with Detected Wash Trades')
ax1.legend()

sns.barplot(x=['Total Trades', 'Flagged Trades'], y=[len(df_all), len(df_flagged)], ax=ax2, palette='muted')
ax2.set_title('Detection Statistics Summary')
plt.tight_layout()
plt.show()

display(pd.DataFrame([detector['metrics']]))
WARNING:__main__:Self-trade detected! Asset: BTC/USD, Entity: user_7
WARNING:__main__:Self-trade detected! Asset: BTC/USD, Entity: malicious_actor
WARNING:__main__:Self-trade detected! Asset: BTC/USD, Entity: user_15
WARNING:__main__:Self-trade detected! Asset: BTC/USD, Entity: user_1
WARNING:__main__:Self-trade detected! Asset: BTC/USD, Entity: user_19
WARNING:__main__:Self-trade detected! Asset: BTC/USD, Entity: user_5
WARNING:__main__:Self-trade detected! Asset: BTC/USD, Entity: user_20
WARNING:__main__:Self-trade detected! Asset: BTC/USD, Entity: user_10
WARNING:__main__:Self-trade detected! Asset: BTC/USD, Entity: user_14
WARNING:__main__:Self-trade detected! Asset: BTC/USD, Entity: user_4
/tmp/ipykernel_1646/4054621549.py:53: FutureWarning: 

Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.

  sns.barplot(x=['Total Trades', 'Flagged Trades'], y=[len(df_all), len(df_flagged)], ax=ax2, palette='muted')
cell output
total_processed total_flagged suspicious_volume
0 200 17 555968.190627

Production Considerations

When deploying these detection algorithms in a live audit environment, consider the following:

PracticeImplementation Detail
LatencyUse high-performance streaming (e.g., Kafka) instead of batch processing for real-time flags.
False PositivesImplement a scoring system rather than binary flags to reduce noise from market makers.
ScalabilityDistributed state management (Redis) if monitoring thousands of assets simultaneously.
Data IntegrityUse cryptographic hashes to ensure trade logs haven't been tampered with post-audit.

Conclusion

In this notebook, we implemented a modular compliance tool for detecting wash trades.

  • Created a state-based architecture to track trade history.
  • Implemented detection logic for Self-Trading and Round-Trip patterns.
  • Demonstrated the effectiveness of these checks using simulated market data and visual analysis.
  • Provided guidelines for transition into a production audit pipeline.