Lesson 7.4.5 · 65 min
Market abuse: spoofing, layering and the bot that imitates them by accident
The EU Market Abuse Regulation prohibits orders that give a false impression of supply or demand, and it does not ask whether the sender is a person or a program. This lesson reads Articles 12 and 15 of MAR and the indicators of Delegated Regulation 2016/522, builds a surveillance-style spoofing detector on an order log with five traders and checks that a bot with a careless fill handler leaves the same footprint as a manipulator. It is not legal advice.
- Module
- 7.4 · Regulation, ethics and taxation
- Duration
- 65 min
- Prerequisites
- 1.1.3 · Order book: bid, ask, spread and depth, 1.1.4 · Order types, 4.5.2 · Order management: states, partial fills, cancellations and reconnections, 6.4.5 · Guardrails for agents: the agent proposes, the engine decides
- Author
- Jesús Cuesta
- Updated
Level 7 lessons
7.1 · Software engineering for trading
7.2 · Infrastructure and deployment
7.3 · MLOps for trading models
7.4 · Regulation, ethics and taxation
- 7.4.1 MiFID II: clients, suitability and algorithmic trading
- 7.4.2 ESMA and CFDs: leverage, close-out and negative balance
- 7.4.3 PRIIPs, the KID and US ETFs
- 7.4.4 MiCA after the transitional period: checking a provider in the registers
- 7.4.5 Market abuse: spoofing, layering and the bot that imitates them by accident
- 7.4.6 Trading taxes in Spain
- 7.4.7 Ethics: personal data, artificial intelligence and selling signals
7.5 · Quant career
7.6 · Final capstone
- 7.6.1 Capstone: common requirements, the panel's rubric and preparing the defense
- 7.6.2 Capstone C1: a multi-asset futures system, from forecast to paper account
- 7.6.3 Capstone C2: systematic options book, volatility surface and real-time Greeks
- 7.6.4 Capstone C3: stock and ETF portfolio with ML, HRP and Spanish taxation
- 7.6.5 Capstone C4: a research platform with LLMs and alternative data
- 7.6.6 Capstone C5: crypto market making or arbitrage with counterparty control
What MAR prohibits
Regulation (EU) No 596/2014 on market abuse (MAR) fits in one sentence: “A person shall not engage in or attempt to engage in market manipulation” (Art. 15)1Note 1 · Regulation (EU) No 596/2014 of the European Parliament and of the Council of 16 April 2014 on market abuse, OJ L 173, 12.6.2014, Articles 12, 15, 16 and 30 and Annex I, copy in the BOE (Spanish version; quotations are unofficial translations). Articles 12, 15 and 16 have not been amended by Regulation (EU) 2024/2809; Article 30 has, and it keeps the amounts cited. Accessed on October 23, 2026.. Article 12 says what manipulating means. Its paragraph 1, point (a), includes entering into a transaction, placing an order to trade or any other behavior that “gives, or is likely to give, false or misleading signals as to the supply of, demand for, or price of” a financial instrument, or that secures or is likely to secure its price at an abnormal or artificial level, “unless the person […] establishes that such transaction, order or behaviour have been carried out for legitimate reasons, and conform with an accepted market practice”.
Three details of the text matter to anyone who codes a system:
- It talks about orders, not only transactions. An order that is never executed can be manipulation. Paragraph 2, point (c), spells it out for electronic trading: “the placing of orders to a trading venue, including any cancellation or modification thereof, […] such as algorithmic and high-frequency trading strategies”, when they disrupt the trading mechanism, make it harder for others to identify genuine orders (“by entering orders which result in the overloading or destabilisation of the order book”) or create a false signal about supply, demand or price.
- It is enough that the order is likely to give a false signal. The text does not require the price to move and does not mention intent; the way out it offers is to establish legitimate reasons. For a bot, “it was a programming bug” is an explanation, not proof that the order could not mislead.
- It applies to any person. Not only to banks or funds: also to an individual with a bot connected to their broker. For crypto-assets, Title VI of MiCA has an equivalent prohibition (Art. 91; lesson 3.7.5).
Annex I of MAR gives indicators “which shall not necessarily be considered, in and of themselves, to constitute market manipulation”, but which market participants and authorities must take into account when examining orders and transactions1. Two describe this lesson's footprint: point (d), orders or transactions that “include position reversals in a short period”, and point (f), orders that “change the representation of the best bid or offer prices […] or more generally the representation of the order book […], and are removed before they are executed”.
The consequences are serious. Article 30 requires Member States to provide for maximum fines of at least €5 million for a natural person and €15 million or 15% of turnover for a legal person for infringements of Articles 14 and 15, or three times the profit gained if it can be determined1. In Spain, breaching Article 15 of MAR is one of the infringements listed in Article 297 of Law 6/2023 on Securities Markets and Investment Services (Ley de los Mercados de Valores y de los Servicios de Inversión)2Note 2 · Law 6/2023 of March 17 on Securities Markets and Investment Services, Article 297.1.c, BOE-A-2023-7053 (in Spanish), consolidated text accessed on October 23, 2026..
Spoofing, layering and company in Delegated Regulation 2016/522
Delegated Regulation (EU) 2016/522 develops the Annex I indicators with a list of practices (its Annex II)3Note 3 · Commission Delegated Regulation (EU) 2016/522 of 17 December 2015, Annex II, Section 1, OJ L 88, 5.4.2016, copy in the BOE (Spanish version).. Delegated Regulation (EU) 2026/788, published in the Official Journal on July 16, 2026 and in force twenty days later, has amended it: it rewrites the description of layering and spoofing and adds a point 10 according to which the indicators “may also be assessed and calculated over a period of time longer or shorter than one day or one trading session”, something “particularly relevant […] in the context of algorithmic trading”4Note 4 · Commission Delegated Regulation (EU) 2026/788 of 8 April 2026 amending Delegated Regulation (EU) 2016/522, OJ L, 16.7.2026, Annex I (points 1a, 2a, 4a, 5(e), 5a and 10), copy in the BOE (Spanish version; quotations are unofficial translations).. The practices that most resemble what a program does:
| Practice (Annex II, Section 1) | What it describes | How a bot can fall into it |
|---|---|---|
| Layering and spoofing (point 5(e)) | Multiple or large orders on one side of the book to execute on the other; once the trade is done, they are removed | A handler that cancels everything after each fill while keeping large orders on the other side |
| Orders with no intention to execute them (point 6(a)) | Orders that are withdrawn before execution and give the impression of demand or supply at that price | “Support” or bait orders that the algorithm never intends to let execute |
| Quote stuffing (point 4(e)) | Large numbers of orders, cancellations or modifications that create uncertainty or slow others down | A loop that resubmits the order every few milliseconds even when nothing changes |
| Momentum ignition (point 4(f)) | Orders that start or exaggerate a trend to close or open a position at a good price; illustrated with a high ratio of canceled orders | A breakout strategy that buys aggressively and sells into the move it causes itself |
| Fictitious trades and improper matched orders (point 3(a) and (c)) | Buys and sells with no change in final beneficial ownership, or almost simultaneous, at the same price and size, by the same party | Two strategies on the same account that cross each other with no self-match prevention |
Mermaid source
flowchart TB A[Buy layers] --> B[Tilted book] B --> C[Small sell] C --> D[Fill] D --> E[Cancel layers] E --> F[Book reverts]
The diagram is the cycle of buy-side spoofing: the trader shows a lot of buying interest near the best price, other participants read the tilted book and buy or raise their prices, the trader's small sell order fills higher up and, right afterward, the layers disappear. The figure at the top is a concrete episode from this lesson's log: 12,000 shares to buy for less than a second in order to sell 300.
An order log with five traders
Trading venues and brokers monitor with order logs: every submission, cancellation and fill, with its time, trader,
side, price and size. To work without real data, the next cell builds a synthetic one for half an hour of trading. The
best bid follows a random walk in cents (10,000 cents are 100 euros) and the Registro class records the events with
the quote in force at each instant:
import numpy as np
import pandas as pd
DUR = 30 * 60 * 1000 # half an hour of trading, in ms
PASO = 250 # the best bid moves every 250 ms
rng = np.random.default_rng(7)
mov = rng.choice([-1, 0, 0, 0, 1], size=DUR // PASO)
BID = 10_000 + np.cumsum(mov) # best bid in cents
def touch(t):
"""Best bid and best ask (cents) at instant t."""
b = int(BID[min(t // PASO, len(BID) - 1)])
return b, b + 1
class Registro:
"""Order log in the style of a trading venue."""
def __init__(self):
self.filas, self.vivas, self.n = [], {}, 0
def _fila(self, t, quien, oid, tipo, o, qty):
b, a = touch(t)
self.filas.append((t, quien, oid, tipo, o["lado"],
o["precio"], qty, b, a))
def alta(self, t, quien, lado, precio, qty):
self.n += 1
o = {"quien": quien, "lado": lado, "precio": precio,
"qty": qty}
self.vivas[self.n] = o
self._fila(t, quien, self.n, "alta", o, qty)
return self.n
def cancela(self, t, oid):
o = self.vivas.pop(oid, None)
if o is not None:
self._fila(t, o["quien"], oid, "cancelada", o, o["qty"])
def ejecuta(self, t, oid):
o = self.vivas.pop(oid)
self._fila(t, o["quien"], oid, "ejecutada", o, o["qty"])
def de(self, quien):
return [i for i, o in self.vivas.items() if o["quien"] == quien]
def tabla(self):
cols = ["ts", "quien", "oid", "evento", "lado", "precio",
"qty", "bid", "ask"]
return (pd.DataFrame(self.filas, columns=cols)
.sort_values(["ts", "oid"], kind="stable")
.reset_index(drop=True))The log keeps its data keys in Spanish: quien is the trader, evento the event (alta for a new order, cancelada
for a cancellation, ejecutada for a fill) and lado the side (compra, buy; venta, sell).
There are five traders. Two market makers (MM1 and MM2) quote both sides with 300 shares and refresh the quote every 300 to 900 ms; sometimes one of the two sides gets filled. A manipulator (S1) runs the cycle in the diagram 20 times: three layers of 4,000 shares at the best price and the next two ticks, a real order for 300 on the opposite side and, as soon as it fills, the cancellation of the layers:
def creador(reg, nombre, rng, qty=300):
"""Quotes both sides with the same size and refreshes them."""
t, puntas = 0, []
while t < DUR - 1000:
if puntas and rng.random() < 0.12:
reg.ejecuta(t, puntas.pop(rng.integers(2)))
for oid in puntas:
reg.cancela(t + 1, oid)
b, a = touch(t + 2)
puntas = [reg.alta(t + 2, nombre, "compra", b, qty),
reg.alta(t + 2, nombre, "venta", a, qty)]
t += int(rng.integers(300, 900))
for oid in puntas:
reg.cancela(DUR - 1, oid)
def spoofer(reg, nombre, rng, n=20, espera=(30, 150)):
"""Large layers on one side; executes little on the other and cancels."""
inicios = np.sort(rng.choice(np.arange(5, DUR // 1000 - 10),
n, replace=False)) * 1000
for k, t0 in enumerate(inicios):
t0 = int(t0)
b, a = touch(t0)
if k % 2 == 0: # buy layers to sell higher
capas = [reg.alta(t0, nombre, "compra", b - d, 4000)
for d in range(3)]
lado, precio = "venta", a
else: # sell layers to buy lower
capas = [reg.alta(t0, nombre, "venta", a + d, 4000)
for d in range(3)]
lado, precio = "compra", b
t1 = t0 + int(rng.integers(400, 900))
real = reg.alta(t1, nombre, lado, precio, 300)
t2 = t1 + int(rng.integers(50, 400))
reg.ejecuta(t2, real)
t3 = t2 + int(rng.integers(*espera))
for oid in capas:
reg.cancela(t3, oid)And two bots written with no intention whatsoever of misleading anyone. BOT_A wants to accumulate: it keeps a buy order for 3,000 shares one tick below the best price and repositions it every time the price moves (“chasing the price”); from time to time it sells 200 of what it already held. Its fill handler has a rule that looks prudent: after any fill, cancel everything and wait two seconds. BOT_B wants to buy 100 shares at the best price and, so as not to lose its place in the queue, cancels and resubmits its order every 50 ms, whether or not the price changes:
def bot_acumulador(reg, nombre, rng, cancelar_todo=True,
persigue=True):
"""Buys 3000 one tick below the best price and sells 200 of
what it already held when it can. After each fill, “to be
safe”, it cancels everything and waits two seconds."""
t, grande, venta = 1000, None, None
while t < DUR - 3000:
b, a = touch(t)
lejos = (grande is not None and
abs(reg.vivas[grande]["precio"] - (b - 1))
> (0 if persigue else 5))
if grande is None or lejos:
if grande is not None:
reg.cancela(t, grande)
grande = reg.alta(t, nombre, "compra", b - 1, 3000)
if venta is None and rng.random() < 0.003:
venta = reg.alta(t, nombre, "venta", a, 200)
elif venta is not None and rng.random() < 0.03:
reg.ejecuta(t, venta)
venta = None
if cancelar_todo:
for oid in reg.de(nombre):
reg.cancela(t + 5, oid)
grande = None
t += 2000
continue
t += 50
for oid in reg.de(nombre):
reg.cancela(DUR - 1, oid)
def bot_bucle(reg, nombre, rng, cada=50, solo_si_cambia=False):
"""Buys 100 at the best price and resubmits the order every 50 ms."""
t, viva = 0, None
while t < DUR - 1000:
b, _ = touch(t)
if viva is not None and rng.random() < 0.002:
reg.ejecuta(t, viva)
viva = None
if viva is not None and solo_si_cambia and \
reg.vivas[viva]["precio"] == b:
t += cada
continue
if viva is not None:
reg.cancela(t, viva)
viva = reg.alta(t, nombre, "compra", b, 100)
t += cada
if viva is not None:
reg.cancela(DUR - 1, viva)The fills are a simplification: there is no matching engine, and each order fills with a fixed probability. To study the footprint of the orders in the log, it is enough. The full session:
def sesion(**kw):
reg = Registro()
g = [np.random.default_rng(s) for s in range(1, 6)]
creador(reg, "MM1", g[0])
creador(reg, "MM2", g[1])
spoofer(reg, "S1", g[2])
bot_acumulador(reg, "BOT_A", g[3], **kw)
bot_bucle(reg, "BOT_B", g[4])
return reg.tabla()
log = sesion()
print(len(log), "events")
log.groupby(["quien", "evento"]).size().unstack(fill_value=0)101652 events
evento alta cancelada ejecutada
quien
BOT_A 2762 2676 86
BOT_B 35980 35893 87
MM1 6026 5672 354
MM2 5978 5614 364
S1 80 60 20S1's first episode, with times in milliseconds from the start of the session and prices in cents:
cols = ["ts", "oid", "evento", "lado", "precio", "qty", "bid", "ask"]
log.loc[log["quien"] == "S1", cols].head(8) ts oid evento lado precio qty bid ask
3552 63000 12005 alta compra 10014 4000 10014 10015
3553 63000 12006 alta compra 10013 4000 10014 10015
3554 63000 12007 alta compra 10012 4000 10014 10015
3591 63546 12008 alta venta 10015 300 10014 10015
3616 63924 12008 ejecutada venta 10015 300 10014 10015
3619 63954 12005 cancelada compra 10014 4000 10014 10015
3620 63954 12006 cancelada compra 10013 4000 10014 10015
3621 63954 12007 cancelada compra 10012 4000 10014 10015Three buy orders for 4,000 at the best price and the two ticks below; 546 ms later, a sell order for 300 at the best ask; it fills 378 ms later and, 30 ms after that, the three buy orders disappear. That is what the figure at the top shows.
A detector in the style of market surveillance
A surveillance system does not read intentions: it looks for footprints. For layering, the footprint is the one in point 5(e) of Annex II: large orders on one side, a fill on the other, withdrawal afterward. The lesson's detector turns it into a rule that is evaluated on every fill of every trader:
- look for the trader's orders on the opposite side that were live at the time of the fill, had been entered at most 2 seconds earlier and were near the best price (less than 3 ticks away);
- of those, keep the ones canceled within the second following the fill;
- the fill is an alert if that canceled volume is at least 5 times the executed volume.
In notation, for a fill of size at instant and the opposite orders that meet the time and price conditions:
def ordenes(log):
"""One row per order: submission, end and how it ended."""
alta = log[log["evento"] == "alta"].set_index("oid")
fin = log[log["evento"] != "alta"].set_index("oid")
o = alta[["quien", "lado", "precio", "qty", "ts"]]
o = o.rename(columns={"ts": "t_alta"})
o["t_fin"] = fin["ts"]
o["fin"] = fin["evento"]
return o
def episodios(log, ventana=2000, tras=1000, ticks=3, ratio=5):
o = ordenes(log)
grupos = dict(tuple(o.groupby(["quien", "lado"])))
filas = []
for f in log[log["evento"] == "ejecutada"].itertuples():
otro = "compra" if f.lado == "venta" else "venta"
m = grupos.get((f.quien, otro))
vol, lat = 0, np.nan
if m is not None:
viva = m[(m["t_alta"] <= f.ts) & (m["t_fin"] > f.ts)
& (f.ts - m["t_alta"] <= ventana)]
if otro == "compra":
viva = viva[f.bid - viva["precio"] < ticks]
else:
viva = viva[viva["precio"] - f.ask < ticks]
c = viva[(viva["fin"] == "cancelada")
& (viva["t_fin"] - f.ts <= tras)]
vol = int(c["qty"].sum())
if len(c):
lat = float((c["t_fin"] - f.ts).max())
filas.append((f.ts, f.quien, f.lado, f.qty, vol, lat))
ep = pd.DataFrame(filas, columns=["ts", "quien", "lado", "qty",
"vol_cancelado", "ms_cancel"])
ep["alerta"] = ep["vol_cancelado"] >= ratio * ep["qty"]
return ep
ep = episodios(log)
res = ep.groupby("quien").agg(ejecuciones=("qty", "size"),
alertas=("alerta", "sum"))
res["pct"] = (100 * res["alertas"] / res["ejecuciones"]).round(1)
res["ms_cancel"] = ep[ep["alerta"]].groupby("quien")["ms_cancel"] \
.median()
res ejecuciones alertas pct ms_cancel
quien
BOT_A 86 83 96.5 5.0
BOT_B 87 0 0.0 NaN
MM1 354 0 0.0 NaN
MM2 364 0 0.0 NaN
S1 20 20 100.0 87.0The detector flags all 20 of S1's fills, none of the market makers' or BOT_B's, and 83 of BOT_A's 86 (the columns
ejecuciones and alertas are fills and alerts). The cancellation latency also gives the bot away: S1 takes a median
of 87 ms to withdraw the layers (a person or a program that waits a little); BOT_A, exactly 5 ms, those of its handler.
For the surveillance system, BOT_A is the clearer case of the two.
The market makers also cancel their opposite quote right after each fill: what separates them from layering is size. They quote 300 on each side, so the canceled volume never reaches five times the executed volume. With a lower threshold, the rule would confuse them:
pd.DataFrame({r: episodios(log, ratio=r).groupby("quien")["alerta"]
.sum() for r in [1, 2, 5, 10]}).rename_axis(
columns="ratio")ratio 1 2 5 10
quien
BOT_A 83 83 83 83
BOT_B 0 0 0 0
MM1 352 0 0 0
MM2 362 0 0 0
S1 20 20 20 20With ratio 1, every quote refresh after a fill is an alert: more than 350 per market maker, noise that no analyst can review. From 2 upward they disappear, and between 2 and 10 the result does not change because S1's layers are 40 times its fill and BOT_A's buy order, 15 times its sell order. In a real market, thresholds are calibrated by instrument and liquidity (the new point 5a of Annex II says so for the price reversal period4) and alerts go to an analyst, who looks at the context: the impact on the book, the position, the trader's history.
The bot that looks like a spoofer
BOT_A does not intend to mislead anyone. But its log says the same as S1's: a large order near the best price, a small fill on the opposite side and an immediate cancellation. The origin lies in two common design decisions:
- Cancel everything after a fill. It is a common reflex (“when anything happens, I get out of the market”) that in lesson 4.2.5 makes sense as a kill switch, not as a routine response to every fill.
- Chase the price. Repositioning the large buy order as soon as the best price moves makes the order short-lived and often canceled right after a sale, even if the handler cancels nothing.
Test the two fixes separately and together:
variantes = {
"original": {},
"no cancel-all": dict(cancelar_todo=False),
"no chasing": dict(persigue=False),
"both fixes": dict(cancelar_todo=False, persigue=False),
}
filas = {}
for nombre, kw in variantes.items():
e = episodios(sesion(**kw))
a = e[e["quien"] == "BOT_A"]
filas[nombre] = {"ejecuciones": len(a),
"alertas": int(a["alerta"].sum())}
pd.DataFrame(filas).T ejecuciones alertas
original 86 83
no cancel-all 89 74
no chasing 86 8
both fixes 89 0Removing only the “cancel everything” barely changes anything: 74 alerts out of 89, because the chasing still cancels the large buy order within the second after many sales. Removing only the chasing leaves 8: those of the sales that arrive right after the bot has replaced the buy order. With both fixes, none. The large buy order stays put as long as the price does not move more than five ticks away, and a sale does not trigger its cancellation.
There is a deeper fix that the code does not solve: holding at the same time a buy order for 3,000 and a sell order for 200 in the same stock and the same account means showing the market two opposite strategies at once. If they really are two strategies, it is worth separating them (and enabling the broker's or venue's self-match prevention, so as not to fall into the fictitious trades of point 3); if not, the bot should pick one side at a time. It is the same principle as in lesson 6.4.5: a pre-trade risk engine can reject, for example, a sell order when the account has live buy orders near the best price for more than five times its size.
The cancel-and-replace loop
BOT_B has no orders on the opposite side and the spoofing detector does not see it. Its problem is a different one: message volume. Article 48(6) of MiFID II requires regulated markets to be able to limit each member's ratio of unexecuted orders to transactions6Note 6 · Directive 2014/65/EU (MiFID II), Article 48(6), EUR-Lex., and Delegated Regulation (EU) 2017/566 (RTS 9) sets out how it is calculated, at least at the end of each trading session, by number and by volume7Note 7 · Commission Delegated Regulation (EU) 2017/566 (RTS 9), Articles 2 and 3 and Annex, EUR-Lex.:
where and are the number and volume of orders, and and the number and volume of executed transactions. According to its annex, “each submission, modification or cancellation shall be counted as one order”. The specific limit is set by each venue:
def otr(log):
ev = log.groupby(["quien", "evento"]).size().unstack(fill_value=0)
return ((ev["alta"] + ev["cancelada"]) / ev["ejecutada"] - 1) \
.round(1)
reg = Registro()
bot_bucle(reg, "BOT_B", np.random.default_rng(5),
solo_si_cambia=True)
corregido = reg.tabla()["evento"].value_counts()
print(otr(log).to_dict())
print("BOT_B only when the price changes:", corregido.to_dict(),
otr(reg.tabla()).to_dict()){'BOT_A': 62.2, 'BOT_B': 825.1, 'MM1': 32.0, 'MM2': 30.8, 'S1': 6.0}
BOT_B only when the price changes: {'alta': 2938, 'cancelada': 2851, 'ejecutada': 87} {'BOT_B': 65.5}BOT_B sends 35,980 submissions and 35,893 cancellations for 87 fills: a ratio of 825.1, more than twenty times that of the market makers, who quote all the time. Resubmitting the order only when the best price changes, the fills are the same and the ratio drops to 65.5. Besides the venue's limit, a volume like that comes close to the description of quote stuffing (point 4(e) of Annex II) and, in a real market, losing queue priority with every resubmission makes the order execute worse, not better (lesson 2.4.8).
Who monitors and what happens next
Mermaid source
flowchart TB L[Order log] --> V[Surveillance] V --> AL[Alert] AL --> AN[Analyst] AN --> S[STOR to the CNMV] AN --> X[Reasoned closure]
In Spain, the competent authority that receives these reports is the CNMV (Comisión Nacional del Mercado de Valores, Spain's securities regulator).
- Trading venues must prevent and detect manipulation and report to their authority without delay the orders and transactions, “including any cancellation or modification thereof”, that could constitute it (Art. 16(1) of MAR)1.
- Any person professionally arranging or executing transactions, including an individual's broker, must have systems to detect and report suspicious orders and transactions (Art. 16(2)). Delegated Regulation (EU) 2016/957 details those systems: they must analyze “each and every transaction executed and order placed, modified, cancelled or rejected” and generate alerts (Art. 3), and it sets the content of the suspicious transaction and order report (STOR)8Note 8 · Commission Delegated Regulation (EU) 2016/957 of 9 March 2016, Articles 3 and 7, EUR-Lex..
- Firms engaged in algorithmic trading must monitor all the activity of their systems, “including that of its clients”, with an automated system that generates alerts, and review it at least once a year (Art. 13 of RTS 6), and test their algorithms so that they do not contribute to disorderly trading conditions (Art. 5)9Note 9 · Commission Delegated Regulation (EU) 2017/589 (RTS 6), Articles 5 and 13, EUR-Lex..
For an individual, the most likely consequence of a bot like BOT_A is not an immediate fine, but a report from their broker to the authority, questions about their trading and, often, the closure of the account. To avoid it, the useful thing is to treat market abuse risk as one more requirement of the system:
| Control | What for |
|---|---|
| Keep the complete log of your own orders (submissions, cancellations, modifications) | Being able to run on it the same kind of detector the broker uses and explain every order |
| Run this lesson's detector in paper trading before going live (lesson 4.5.3) | Find the footprint before surveillance does |
| Do not cancel everything as a routine after each fill; keep “cancel everything” for the kill switch | Avoid the pattern of layers withdrawn after a fill |
| Proportionate sizes on both sides and a single direction per stock and account | What is shown is what you want to execute |
| Reposition only when needed, with a maximum message rate | Keep the RTS 9 ratio far from the venue's limits |
| Self-match prevention | Do not trade against yourself (fictitious trades) |
| Document the strategy's logic and its changes (lesson 2.2.4) | Have the “legitimate reasons” in writing when someone asks |
Exercises
- A more cautious manipulator waits between 1.5 and 3 seconds before withdrawing the layers. Simulate a session with
that spoofer and check whether the detector finds it with
tras=1000and withtras=5000. Do the market makers' alerts change?
Solution
reg = Registro()
creador(reg, "MM1", np.random.default_rng(1))
spoofer(reg, "S2", np.random.default_rng(3), espera=(1500, 3000))
lento = reg.tabla()
for tras in [1000, 5000]:
e = episodios(lento, tras=tras)
print(tras, e.groupby("quien")["alerta"].sum().to_dict())1000 {'MM1': 0, 'S2': 0}
5000 {'MM1': 0, 'S2': 20}With a one-second margin, the detector does not see S2: its layers are canceled later. With five seconds it sees all of it and the market makers still have no alerts, because their problem was never timing but size. That is why real systems evaluate several windows at once, and point 10 added by Delegated Regulation 2026/788 insists that the indicators can be calculated over periods other than a session: whoever manipulates adjusts their timing to the thresholds they assume.
- Write a function
permitir(reg, quien, lado, precio, qty, t)that a bot calls before each submission: it rejects the order if the account has live opposite-side orders less than 3 ticks from the best price for more than five timesqty. Test it with a sell order for 200 when BOT_A has its buy order for 3,000 live.
Solution
def permitir(reg, quien, lado, precio, qty, t, ticks=3, ratio=5):
b, a = touch(t)
otro = "compra" if lado == "venta" else "venta"
vol = 0
for o in reg.vivas.values():
if o["quien"] != quien or o["lado"] != otro:
continue
cerca = (b - o["precio"] < ticks if otro == "compra"
else o["precio"] - a < ticks)
vol += o["qty"] if cerca else 0
return vol <= ratio * qty, vol
reg = Registro()
t = 60_000
b, a = touch(t)
reg.alta(t, "BOT_A", "compra", b - 1, 3000)
print(permitir(reg, "BOT_A", "venta", a, 200, t))
print(permitir(reg, "BOT_A", "venta", a, 800, t))
print(permitir(reg, "BOT_A", "compra", b, 200, t))(False, 3000)
(True, 3000)
(True, 0)The sell order for 200 is rejected (3,000 in buy orders near the best price are 15 times its size); the one for 800 passes, and a buy order has nothing against it. The check does not decide whether the strategy is legitimate: it prevents the bot from showing a large intention on one side while executing on the other, which is exactly the footprint detectors look for. A rejection should be logged and, if it repeats, alert a person, like the risk engine in lesson 6.4.5.
- A momentum system buys aggressively when a high is broken and sells a few seconds later into the move. Explain which Annex II indicator it could trigger and what data you would keep to show that the strategy has legitimate reasons.
Solution
Point 4(f) of Annex II describes “momentum ignition”: orders or transactions likely to start or exacerbate a trend in order to close or open a position at a favorable price, illustrated with a high ratio of canceled orders. If the system's buy orders are a significant share of the volume in those seconds and its subsequent sale coincides with the move they have caused, the footprint is similar. Defending the strategy requires its documentation (hypothesis, backtest and out-of-sample results dated before trading), the complete order log with the signal behind each order, the system's participation in the volume of each episode and a participation limit that is respected. If the profit depends on the system itself moving the price, it is not a signal: it is the problem.
Footnotes
-
Regulation (EU) No 596/2014 of the European Parliament and of the Council of 16 April 2014 on market abuse, OJ L 173, 12.6.2014, Articles 12, 15, 16 and 30 and Annex I, copy in the BOE (Spanish version; quotations are unofficial translations). Articles 12, 15 and 16 have not been amended by Regulation (EU) 2024/2809; Article 30 has, and it keeps the amounts cited. Accessed on October 23, 2026. ↩ ↩2 ↩3 ↩4
-
Law 6/2023 of March 17 on Securities Markets and Investment Services, Article 297.1.c, BOE-A-2023-7053 (in Spanish), consolidated text accessed on October 23, 2026. ↩
-
Commission Delegated Regulation (EU) 2016/522 of 17 December 2015, Annex II, Section 1, OJ L 88, 5.4.2016, copy in the BOE (Spanish version). ↩
-
Commission Delegated Regulation (EU) 2026/788 of 8 April 2026 amending Delegated Regulation (EU) 2016/522, OJ L, 16.7.2026, Annex I (points 1a, 2a, 4a, 5(e), 5a and 10), copy in the BOE (Spanish version; quotations are unofficial translations). ↩ ↩2
-
FCA, “FCA fines and prohibits hedge fund Chief Investment Officer for market abuse”, December 15, 2020, fca.org.uk. ↩
-
Commission Delegated Regulation (EU) 2017/566 (RTS 9), Articles 2 and 3 and Annex, EUR-Lex. ↩
-
Commission Delegated Regulation (EU) 2016/957 of 9 March 2016, Articles 3 and 7, EUR-Lex. ↩
-
Commission Delegated Regulation (EU) 2017/589 (RTS 6), Articles 5 and 13, EUR-Lex. ↩
Each answer is folded under its question
Self-assessment
A bot sells 200 shares and, in the same millisecond in which it receives the fill, cancels a buy order for 3,000 that it had one tick below the best price. The programmer did not intend to mislead anyone. What does MAR say?
- ANothing: without intent there is no manipulation
- BThat it is manipulation only if the bot makes money
- CThat it only applies to natural persons
- DArticle 12(1)(a) speaks of orders that give or are likely to give false or misleading signals; the way out it offers is to establish legitimate reasons and conformity with an accepted market practice. The bot's footprint is that of a manipulation indicator and it will have to be explained
Show answer
Answer D. Article 12(1)(a) speaks of orders that give or are likely to give false or misleading signals; the way out it offers is to establish legitimate reasons and conformity with an accepted market practice. The bot's footprint is that of a manipulation indicator and it will have to be explained
The text of Article 12(1)(a) focuses on the possible effect of the order and leaves it to whoever placed it to establish legitimate reasons. The Annex I indicators are not manipulation in and of themselves, but a surveillance alert requires explanations, and “it was a programming error” does not make the footprint go away.
In the lesson, why do the two market makers not generate alerts even though they cancel their opposite quote right after each fill?
- ABecause they quote both sides with the same size: the volume canceled on the opposite side (300) is not five times the executed volume (300)
- BBecause their orders are far from the best price
- CBecause the detector excludes market makers by name
- DBecause they never cancel
Show answer
Answer A. Because they quote both sides with the same size: the volume canceled on the opposite side (300) is not five times the executed volume (300)
With a ratio threshold of 1, the two market makers would get more than 350 alerts each; with 2 or more, none. The size asymmetry between what is shown and what is executed is what separates layering from two-sided quoting.
What does Delegated Regulation (EU) 2026/788 change in the manipulation indicators?
- AIt prohibits retail algorithmic trading
- BIt specifies that the indicators can be assessed over periods longer or shorter than a trading session, especially in algorithmic trading, and rewrites the description of layering and spoofing
- CIt repeals Annex II of Delegated Regulation 2016/522
- DIt sets a numerical cancellation threshold above which there is manipulation
Show answer
Answer B. It specifies that the indicators can be assessed over periods longer or shorter than a trading session, especially in algorithmic trading, and rewrites the description of layering and spoofing
Published in the OJ L of July 16, 2026, it adds point 10 to Annex II (indicators that can be assessed over periods other than one day, especially in illiquid instruments and in algorithmic trading), replaces point 5(e) and takes into account significant changes in volume, not only in price. It does not set any numerical threshold.
The bot BOT_B resubmits its buy order every 50 ms even if the price does not change. Which indicator does it trigger and which does it not?
- AIt triggers the spoofing detector, because it cancels a lot
- BIt triggers nothing, because it only buys
- CIt has no orders on the opposite side, so there is no spoofing pattern; but its ratio of unexecuted orders to transactions is above 800, close to the “quote stuffing” practice and to the limits that venues set
- DIt triggers a fictitious trades alert
Show answer
Answer C. It has no orders on the opposite side, so there is no spoofing pattern; but its ratio of unexecuted orders to transactions is above 800, close to the “quote stuffing” practice and to the limits that venues set
RTS 9 counts each submission, modification or cancellation as an order. With 35,980 submissions and 35,893 cancellations for 87 fills, the ratio by number is 825.1; resubmitting only when the price changes brings it down to 65.5.
Who is obliged to detect and report suspicious orders from a client who trades with a bot through a broker?
- ANobody: only the CNMV, the Spanish regulator, monitors
- BOnly the client themselves
- CThe broker, as a person professionally executing transactions (Art. 16(2) of MAR), and the trading venue (Art. 16(1)); both must report without delay to the competent authority
- DThe data provider
Show answer
Answer C. The broker, as a person professionally executing transactions (Art. 16(2) of MAR), and the trading venue (Art. 16(1)); both must report without delay to the competent authority
Article 16 requires venues and any person professionally arranging or executing transactions to have systems to detect suspicious orders and transactions and to report them (STOR), as detailed in Delegated Regulation (EU) 2016/957. Firms engaged in algorithmic trading must also monitor all activity automatically, including that of their clients (Art. 13 of RTS 6).
Key points
- MAR prohibits engaging or attempting to engage in market manipulation (Art. 15). Article 12(2)(c) expressly includes orders, cancellations and modifications sent with algorithmic and high-frequency strategies that give false signals or destabilize the order book.
- Annex II of Delegated Regulation 2016/522 describes layering and spoofing: multiple or large orders on one side to execute on the other and then remove them. Since August 2026 (Delegated Regulation 2026/788), the indicators can be assessed over periods shorter than a session.
- The lesson's detector flags a fill when the same trader cancels, in less than a second, at least five times its size in opposite-side orders near the best price. It flags all 20 fills of the manipulator, none of the market makers' and 83 of the 86 of the bot with the “cancel everything” handler.
- A bot with no intent to mislead can leave a spoofer's footprint: cancel everything after each fill and chase the price with large orders on the other side. Removing the “cancel everything” is not enough (74 alerts out of 89); removing the chasing as well leaves 0.
- RTS 9 counts each submission, modification and cancellation as an order. A loop that resubmits every 50 ms gives a ratio of 825; resubmitting only when the price changes brings it down to 65.5.
- Venues, brokers and firms with algorithms monitor and report (Art. 16 of MAR, RTS 6). The maximum fines that MAR requires for manipulation are at least €5 million for natural persons. It is not legal advice.
Resources
Research
Documentation
- Regulation (EU) No 596/2014 on market abuse (MAR), OJ L 173, 12.6.2014 (copy in the BOE, Spain's official gazette; Spanish version)
- Delegated Regulation (EU) 2016/522: indicators of manipulation (Annex II), OJ L 88, 5.4.2016 (copy in the BOE; Spanish version)
- Delegated Regulation (EU) 2026/788, amending the indicators of manipulation, OJ L, 16.7.2026 (copy in the BOE; Spanish version)
- Delegated Regulation (EU) 2017/566 (RTS 9): ratio of unexecuted orders to transactions
- FCA: fine and prohibition for a fund manager over misleading CFD orders (December 15, 2020)