Ethical Quant Trading: Regulations & Market Manipulation › Building Compliance Monitoring Tools
Quantitative FinanceAll chapters Ethical Quant Trading: Regulations & Market Manipulation › Building Compliance Monitoring ToolsOne section · Sections in this chapter5 / 13 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Let us put these concepts into practice by building tools that a compliance team at a quantitative trading firm might use to monitor for potentially problematic trading behavior. These examples illustrate how the programming and analytical skills you have developed throughout this book apply to compliance. The tools we build here are deliberately simplified for pedagogical clarity, but they embody the same core logic that production compliance systems use at scale.
Detecting Potential Spoofing Patterns
A basic spoofing detection system looks for patterns where a trader places large orders that are cancelled shortly after a fill occurs on the opposite side. The underlying intuition is grounded in the definition of spoofing itself: the spoofer's goal is to create a false impression of supply or demand on one side of the market in order to execute profitably on the other side. Therefore, the telltale signature is an asymmetry between displayed intent and actual execution. On one side of the book, the trader places large orders that are overwhelmingly cancelled. On the other side, the trader executes smaller orders that are overwhelmingly filled. This combination of high cancellation rates on one side and profitable fills on the other is the core signal that spoofing detection systems are designed to identify.
We can build a simple detector that flags suspicious order sequences. First, let us construct a synthetic order dataset that includes both normal trading activity and an injected spoofing-like pattern. The injected pattern consists of a cluster of large sell orders that are all cancelled, interspersed with a small buy order that is filled, mimicking a spoofer who places phantom sell pressure to depress the price and then buys at the artificially lowered level.
import numpy as np
import pandas as pd
np.random.seed(42)
n_orders = 500
timestamps = pd.date_range("2024-01-15 09:30:00", periods=n_orders, freq="2s")
sides = np.random.choice(["buy", "sell"], size=n_orders, p=[0.5, 0.5])
sizes = np.random.choice(
[100, 200, 500, 1000, 5000], size=n_orders, p=[0.3, 0.3, 0.2, 0.1, 0.1]
)
statuses = np.random.choice(
["filled", "cancelled"], size=n_orders, p=[0.3, 0.7]
)
orders = pd.DataFrame(
{
"timestamp": timestamps,
"trader_id": "ALGO_001",
"side": sides,
"size": sizes,
"status": statuses,
"price": 150.0 + np.cumsum(np.random.randn(n_orders) * 0.01),
}
)
# Inject a spoofing-like pattern: large cancelled sells followed by small buy fills
spoof_indices = [50, 51, 52, 53, 55]
for idx in spoof_indices:
orders.loc[idx, "side"] = "sell"
orders.loc[idx, "size"] = 10000
orders.loc[idx, "status"] = "cancelled"
orders.loc[54, "side"] = "buy"
orders.loc[54, "size"] = 200
orders.loc[54, "status"] = "filled"Now we build the spoofing detection logic. The key insight is to look for asymmetric behavior: large orders on one side that are disproportionately cancelled, while smaller orders on the opposite side are filled. The algorithm works by sliding a time window across the order history and, within each window, computing two critical metrics for each side of the market. The first metric is the cancellation ratio: the fraction of orders on one side that were cancelled rather than filled. The second metric is the size ratio: the average size of the cancelled orders relative to the average size of the filled orders on the opposite side. When both metrics simultaneously exceed their respective thresholds, it indicates that a trader was displaying large phantom orders on one side (high cancel ratio of large orders) while executing smaller orders on the other side (small fills), which is precisely the signature of spoofing.
def detect_spoofing_patterns(
orders_df,
window_seconds=60,
cancel_ratio_threshold=0.8,
size_ratio_threshold=3.0,
):
"""
Flag time windows where a trader shows spoofing-like behavior:
1. High cancellation rate on one side
2. Large cancelled orders vs. small filled orders on the opposite side
"""
alerts = []
orders_df = orders_df.copy()
orders_df["timestamp"] = pd.to_datetime(orders_df["timestamp"])
window = pd.Timedelta(seconds=window_seconds)
unique_times = orders_df["timestamp"].unique()
for start_time in unique_times[
::10
]: # Check every 10th timestamp as window start
end_time = start_time + window
window_orders = orders_df[
(orders_df["timestamp"] >= start_time)
& (orders_df["timestamp"] < end_time)
]
if len(window_orders) < 5:
continue
for side in ["buy", "sell"]:
opposite = "sell" if side == "buy" else "buy"
side_orders = window_orders[window_orders["side"] == side]
opp_orders = window_orders[window_orders["side"] == opposite]
if len(side_orders) == 0 or len(opp_orders) == 0:
continue
cancelled = side_orders[side_orders["status"] == "cancelled"]
opp_filled = opp_orders[opp_orders["status"] == "filled"]
if len(side_orders) == 0 or len(opp_filled) == 0:
continue
cancel_ratio = len(cancelled) / len(side_orders)
avg_cancelled_size = (
cancelled["size"].mean() if len(cancelled) > 0 else 0
)
avg_filled_size = (
opp_filled["size"].mean() if len(opp_filled) > 0 else 1
)
size_ratio = avg_cancelled_size / avg_filled_size
if (
cancel_ratio > cancel_ratio_threshold
and size_ratio > size_ratio_threshold
):
alerts.append(
{
"window_start": start_time,
"window_end": end_time,
"cancelled_side": side,
"cancel_ratio": cancel_ratio,
"avg_cancelled_size": avg_cancelled_size,
"avg_filled_size": avg_filled_size,
"size_ratio": size_ratio,
"n_cancelled": len(cancelled),
"n_filled_opposite": len(opp_filled),
}
)
return pd.DataFrame(alerts)alerts = detect_spoofing_patterns(orders)Spoofing alerts generated: 4 Alert details: Window: 09:45:00 - 09:46:00 Cancelled side: sell, Cancel ratio: 93.8% Avg cancelled size: 860 vs Avg filled opposite size: 167 (ratio: 5.2x) Window: 09:45:40 - 09:46:40 Cancelled side: sell, Cancel ratio: 88.9% Avg cancelled size: 1156 vs Avg filled opposite size: 267 (ratio: 4.3x) Window: 09:46:00 - 09:47:00 Cancelled side: sell, Cancel ratio: 81.8% Avg cancelled size: 1411 vs Avg filled opposite size: 267 (ratio: 5.3x) Window: 09:46:20 - 09:47:20 Cancelled side: sell, Cancel ratio: 100.0% Avg cancelled size: 2675 vs Avg filled opposite size: 267 (ratio: 10.0x)
This detector successfully identifies the injected spoofing pattern. To better understand what the compliance system is flagging, let's visualize the specific sequence of events that triggered the alert.

In production, such a system would be far more sophisticated, incorporating order book reconstruction, cross-instrument analysis, and machine learning classifiers trained on known manipulation cases. Production systems also account for the temporal sequencing of events more precisely: they ask whether cancellations and fills co-occur within a window, then whether the cancellations systematically precede the fills in a way that suggests the cancelled orders were placed to influence the price at which the fills occurred. But the core principle remains: look for asymmetries between displayed intent (large orders placed) and actual behavior (those orders cancelled while profiting on the other side).
Key Parameters
The key parameters for the spoofing detection algorithm are carefully chosen to balance sensitivity against specificity spoofing) against specificity (avoiding false positives that would overwhelm the compliance team with spurious alerts):
- window_seconds: The time window for analyzing order patterns. Shorter windows catch rapid-fire manipulation where a spoofer places and cancels orders within seconds. Longer windows capture sustained pressure campaigns where the spoofer maintains phantom orders for minutes at a time. The appropriate window length depends on the market's typical activity pace and the style of manipulation you are trying to detect. In highly liquid futures markets, a window of 10 to 30 seconds may be appropriate, while in less liquid equity markets, windows of several minutes may be needed.
- cancel_ratio_threshold: The minimum proportion of orders on one side that must be cancelled to trigger an alert. Setting this too low generates excessive false positives, because legitimate market-making strategies routinely cancel a significant fraction of their quotes. Setting it too high risks missing spoofing where the spoofer intermingles some filled orders with the phantom ones to mask the pattern. A threshold of 0.8, meaning at least 80% of orders on one side must be cancelled, provides a reasonable starting point.
- size_ratio_threshold: The ratio of average cancelled order size to average filled execution size on the opposite side. High ratios indicate asymmetric intent, because they reveal that the cancelled orders were much larger than the filled orders. A spoofer typically uses large phantom orders to create an impression of overwhelming supply or demand, while the profitable trades are relatively small. A threshold of 3.0 means the average cancelled order must be at least three times larger than the average filled order on the opposite side.
Monitoring Pre-Trade Risk Limits
Next, let us implement a pre-trade risk check system that an algorithm would call before submitting each order. This mirrors the kind of safeguard required by the SEC's Market Access Rule, which mandates that broker-dealers implement automated controls to prevent erroneous or excessive orders from reaching the market. The design philosophy is that every order, without exception, must pass through a series of validation checks before it is permitted to enter the market. If any single check fails, the order is rejected and the rejection reason is logged for audit purposes. This "fail-safe" approach means that the system defaults to rejecting orders when uncertain, which is the appropriate behavior for a safety mechanism.
The implementation below organizes the risk checks into a class that maintains state across multiple orders, tracking cumulative positions, daily profit and loss, and order counts. This statefulness is essential because many risk limits are aggregate in nature: a single order might be perfectly acceptable in isolation but problematic in the context of the day's accumulated activity.
class PreTradeRiskManager:
"""
Pre-trade risk control system that validates orders against firm limits.
"""
def __init__(self, config):
self.config = config
self.daily_pnl = 0.0
self.positions = {} # symbol -> quantity
self.order_count = 0
self.rejected_orders = []
self.approved_orders = []
def check_order(self, order):
"""
Validate an order against all risk limits.
Returns (approved: bool, reason: str).
"""
checks = [
self._check_order_size(order),
self._check_price_collar(order),
self._check_position_limit(order),
self._check_daily_loss_limit(order),
self._check_order_rate(order),
]
for approved, reason in checks:
if not approved:
self.rejected_orders.append(
{**order, "rejection_reason": reason}
)
return False, reason
self.order_count += 1
self.approved_orders.append(order)
return True, "APPROVED"
def _check_order_size(self, order):
max_size = self.config.get("max_order_size", 10000)
if order["quantity"] > max_size:
return (
False,
f"Order size {order['quantity']} exceeds max {max_size}",
)
return True, ""
def _check_price_collar(self, order):
collar_pct = self.config.get("price_collar_pct", 0.05)
ref_price = order.get("reference_price", order["price"])
deviation = abs(order["price"] - ref_price) / ref_price
if deviation > collar_pct:
return (
False,
f"Price deviation {deviation:.1%} exceeds collar {collar_pct:.1%}",
)
return True, ""
def _check_position_limit(self, order):
max_position = self.config.get("max_position", 50000)
symbol = order["symbol"]
current = self.positions.get(symbol, 0)
delta = (
order["quantity"] if order["side"] == "buy" else -order["quantity"]
)
new_position = abs(current + delta)
if new_position > max_position:
return (
False,
f"Position {new_position} would exceed limit {max_position}",
)
return True, ""
def _check_daily_loss_limit(self, order):
max_loss = self.config.get("max_daily_loss", -100000)
if self.daily_pnl < max_loss:
return (
False,
f"Daily P&L {self.daily_pnl:,.0f} breached loss limit {max_loss:,.0f}",
)
return True, ""
def _check_order_rate(self, order):
max_orders = self.config.get("max_orders_per_session", 10000)
if self.order_count >= max_orders:
return (
False,
f"Order count {self.order_count} at session limit {max_orders}",
)
return True, ""
def update_position(self, symbol, fill_qty, side):
delta = fill_qty if side == "buy" else -fill_qty
self.positions[symbol] = self.positions.get(symbol, 0) + delta
def update_pnl(self, realized_pnl):
self.daily_pnl += realized_pnlLet us simulate a series of orders and observe how the risk manager handles them. We deliberately construct a sequence of orders that will trigger different risk controls, showing how each check catches a specific category of problem. The first order is a normal, well-behaved order that should pass all checks. The second is excessively large. The third has a price far from the reference. The fourth arrives after a simulated period of severe losses. By observing which orders pass and which are rejected, and for what reason, we can see the layered defense in action.
risk_config = {
"max_order_size": 5000,
"price_collar_pct": 0.02, # 2% from reference price
"max_position": 25000,
"max_daily_loss": -50000,
"max_orders_per_session": 10000,
}
risk_mgr = PreTradeRiskManager(risk_config)
test_orders = [
{
"symbol": "AAPL",
"side": "buy",
"quantity": 1000,
"price": 185.50,
"reference_price": 185.00,
},
{
"symbol": "AAPL",
"side": "buy",
"quantity": 15000,
"price": 185.50,
"reference_price": 185.00,
},
{
"symbol": "AAPL",
"side": "buy",
"quantity": 500,
"price": 195.00,
"reference_price": 185.00,
},
{
"symbol": "AAPL",
"side": "buy",
"quantity": 500,
"price": 185.20,
"reference_price": 185.00,
},
{
"symbol": "AAPL",
"side": "buy",
"quantity": 500,
"price": 185.30,
"reference_price": 185.00,
},
]
# Simulate position buildup for the 4th and 5th orders
risk_mgr.update_position("AAPL", 1000, "buy")
# Simulate a bad day
risk_mgr.update_pnl(-55000)
results = []
for i, order in enumerate(test_orders):
approved, reason = risk_mgr.check_order(order)
results.append({"order_num": i + 1, "approved": approved, "reason": reason})Pre-Trade Risk Check Results
======================================================================
Order 1: REJECTED | AAPL 1000 @ $185.50
Reason: Daily P&L -55,000 breached loss limit -50,000
Order 2: REJECTED | AAPL 15000 @ $185.50
Reason: Order size 15000 exceeds max 5000
Order 3: REJECTED | AAPL 500 @ $195.00
Reason: Price deviation 5.4% exceeds collar 2.0%
Order 4: REJECTED | AAPL 500 @ $185.20
Reason: Daily P&L -55,000 breached loss limit -50,000
Order 5: REJECTED | AAPL 500 @ $185.30
Reason: Daily P&L -55,000 breached loss limit -50,000The results show how different risk controls catch different types of problems, each acting as an independent safety layer. Order 1 passes all checks: its size is within bounds, its price is close to the reference, and neither the position limit nor the loss limit has been breached at that point. Order 2 is rejected for exceeding the maximum order size, which in a real trading environment would prevent a catastrophic "fat-finger" error where a trader accidentally enters 15,000 shares instead of 1,500. Order 3 is rejected because its price deviates more than 2% from the reference price, a price collar violation. This check exists to prevent the algorithm from trading at prices that are wildly inconsistent with current market conditions, which could happen if the algorithm receives a stale or corrupted price feed. Order 4 is rejected because the daily P&L has breached the loss limit, which reflects the firm's policy that once cumulative losses exceed a threshold, all trading must stop until the risk manager reviews the situation. Each rejection fires independently, catching the first violated constraint. This sequential checking approach means that an order may have multiple problems, but the system reports the first one encountered, which is typically sufficient for the compliance team to understand why the order was blocked.
Key Parameters
The key parameters for the pre-trade risk manager represent the core calibration decisions that a risk management team must make, balancing the need for safety against the need for the algorithm to operate effectively:
- max_order_size: The maximum quantity allowed for a single order, preventing "fat-finger" errors. This parameter should be calibrated to the typical order sizes for each instrument and strategy. A market-making algorithm in a large-cap equity might reasonably submit orders of several thousand shares, while a strategy trading illiquid small-cap stocks might warrant a much lower limit. Setting this too low constrains legitimate trading; setting it too high defeats the purpose of the check.
- price_collar_pct: The maximum allowable percentage deviation from a reference price. The reference price is typically the current mid-price or last traded price. A collar of 2% means that the algorithm cannot submit an order more than 2% away from the reference, which protects against situations where stale data or software bugs cause the algorithm to price orders far from the current market. In volatile markets, this collar may need to be widened temporarily to accommodate legitimate price swings.
- max_position: The absolute limit on net inventory per symbol. This prevents the algorithm from accumulating an excessively concentrated position in any single instrument, which would expose the firm to unacceptable single-name risk. The appropriate limit depends on the instrument's liquidity, the firm's capital base, and the strategy's expected holding period.
- max_daily_loss: The cumulative loss threshold that triggers a trading halt. This is perhaps the most critical parameter, because it provides a hard limit on the worst-case daily loss the firm is willing to tolerate from a given algorithm or trading desk. Once the daily P&L crosses this threshold, all new orders are blocked until a human reviews the situation and either resets the limit or shuts down the algorithm for the day.
Visualizing Order-to-Trade Ratios
Regulators monitor order-to-trade (OTR) ratios as a potential indicator of manipulative behavior. The OTR ratio is simply the number of orders submitted divided by the number of trades executed. Extremely high OTR ratios, where a firm submits many orders but executes very few, may indicate spoofing or excessive messaging. The logic is intuitive: if a firm is submitting thousands of orders for every trade it executes, it raises the question of what purpose those unexecuted orders serve. Some exchanges impose penalties for excessive OTR ratios, both to discourage potential manipulation and to reduce the computational burden that excessive messaging places on exchange matching engines. Let us visualize this metric across simulated trading desks to see how different strategy types produce different OTR profiles.
import numpy as np
np.random.seed(123)
desks = [
"Stat Arb A",
"Market Making",
"Momentum",
"Event Driven",
"Stat Arb B",
"HFT Desk",
]
orders_submitted = [5000, 120000, 2000, 800, 8000, 500000]
trades_executed = [2500, 8000, 1200, 600, 3500, 15000]
otr_ratios = [o / t for o, t in zip(orders_submitted, trades_executed)]
# Regulatory threshold (hypothetical)
otr_threshold = 20
The HFT desk and market-making desk have the highest OTR ratios, which is expected given their frequent quote updates. A market maker must continuously update its bid and ask quotes to reflect changing market conditions, and each update involves canceling the old quote and submitting a new one. This legitimate activity naturally produces a high OTR ratio. Similarly, an HFT desk that reacts to microstructure signals may cancel and replace orders many times before a single trade executes. However, the HFT desk exceeds the hypothetical threshold, triggering enhanced surveillance. A high OTR ratio is not proof of manipulation: market makers legitimately update quotes thousands of times per second. But it warrants investigation and documentation of the economic rationale for the order activity. The compliance team would then examine whether the high ratio is explained by the desk's strategy type or whether it masks patterns consistent with spoofing or quote stuffing.
Key Parameters
The key parameter for the order-to-trade analysis is:
- otr_threshold: The regulatory or internal limit for the order-to-trade ratio. Ratios above this level trigger enhanced surveillance. The appropriate threshold varies by market and strategy type. Some European exchanges have set explicit OTR limits, while in the U.S. the threshold is more often determined by the firm's internal compliance policies and the exchange's fee schedules, which may impose surcharges for excessive order-to-trade ratios. The threshold should be calibrated with an awareness of what is normal for each strategy type: a threshold that is appropriate for a momentum strategy would generate constant false positives if applied to a market-making desk.