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.

Fig. — A layering episode from the lesson's synthetic log: trader S1's buy volume near the best price and its 300-share sell order, in milliseconds relative to the sale's execution (schematic)
Level 7 lessons
  1. 7.1 · Software engineering for trading

    1. 7.1.1 Project structure, configuration and quality: from notebook to repository
    2. 7.1.2 Performance: measure, profile and compile only what is needed
    3. 7.1.3 Patterns: events, message queues and hexagonal architecture
  2. 7.2 · Infrastructure and deployment

    1. 7.2.1 Docker and docker-compose: reproducible images with uv
    2. 7.2.2 VPS, cloud and latency; CI/CD with GitHub Actions
    3. 7.2.3 Orchestration and monitoring: task graphs, structured logs, metrics, alerts and heartbeats
    4. 7.2.4 Security and continuity: 2FA, IP allowlists, backups and failure procedures
  3. 7.3 · MLOps for trading models

    1. 7.3.1 Tracking experiments, models and data: MLflow, DVC and a run reproduced end to end
    2. 7.3.2 Scheduled retraining, automatic validation and champion/challenger
    3. 7.3.3 Live model monitoring and automatic shutdown
    4. 7.3.4 Model serving: ONNX, CPU or GPU inference and the cost of LLMs in production
  4. 7.4 · Regulation, ethics and taxation

    1. 7.4.1 MiFID II: clients, suitability and algorithmic trading
    2. 7.4.2 ESMA and CFDs: leverage, close-out and negative balance
    3. 7.4.3 PRIIPs, the KID and US ETFs
    4. 7.4.4 MiCA after the transitional period: checking a provider in the registers
    5. 7.4.5 Market abuse: spoofing, layering and the bot that imitates them by accident
    6. 7.4.6 Trading taxes in Spain
    7. 7.4.7 Ethics: personal data, artificial intelligence and selling signals
  5. 7.5 · Quant career

    1. 7.5.1 Quant roles: six jobs around the same system
    2. 7.5.2 Where to work: what each firm does and what the registers say
    3. 7.5.3 Public portfolio and interviews: show judgment, not returns
    4. 7.5.4 Certifications, training and prop firms: what each one certifies and what a challenge measures
  6. 7.6 · Final capstone

    1. 7.6.1 Capstone: common requirements, the panel's rubric and preparing the defense
    2. 7.6.2 Capstone C1: a multi-asset futures system, from forecast to paper account
    3. 7.6.3 Capstone C2: systematic options book, volatility surface and real-time Greeks
    4. 7.6.4 Capstone C3: stock and ETF portfolio with ML, HRP and Spanish taxation
    5. 7.6.5 Capstone C4: a research platform with LLMs and alternative data
    6. 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 describesHow 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 removedA 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 downA 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 ordersA 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 partyTwo strategies on the same account that cross each other with no self-match prevention
Buy layersTilted bookSmall sellFillCancel layersBook reverts
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:

IN [01]python
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:

IN [02]python
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:

IN [03]python
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:

IN [04]python
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)
OUT [04]Half an hour of trading
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         20

S1's first episode, with times in milliseconds from the start of the session and prices in cents:

IN [05]python
cols = ["ts", "oid", "evento", "lado", "precio", "qty", "bid", "ask"]
log.loc[log["quien"] == "S1", cols].head(8)
OUT [05]A layering episode in the log
         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  10015

Three 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 qq at instant tt and the opposite orders CC that meet the time and price conditions:

alert  ⟺  ∑o∈C, t<tocancel≤t+1 sqo ≥ 5 q\text{alert} \iff \sum_{o \in C,\ t < t^{\text{cancel}}_o \le t + 1\,\text{s}} q_o \ \ge\ 5\,q
IN [06]python
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
OUT [06]Spoofing detector
       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.0
Fig. — Each trader's executions flagged by the lesson's spoofing detector (percentage, threshold of 5 times the executed size), over the half-hour synthetic session

The 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:

IN [07]python
pd.DataFrame({r: episodios(log, ratio=r).groupby("quien")["alerta"]
              .sum() for r in [1, 2, 5, 10]}).rename_axis(
    columns="ratio")
OUT [07]Sensitivity to the size threshold
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  20

With 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:

  1. 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.
  2. 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:

IN [08]python
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
OUT [08]Fixing the bot
               ejecuciones  alertas
original                86       83
no cancel-all           89       74
no chasing              86        8
both fixes              89        0

Removing 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.:

OTRN=NONE−1OTRV=VOVE−1\text{OTR}_N = \frac{N_O}{N_E} - 1 \qquad \text{OTR}_V = \frac{V_O}{V_E} - 1

where NON_O and VOV_O are the number and volume of orders, and NEN_E and VEV_E 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:

IN [09]python
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())
OUT [09]Ratio of unexecuted orders to transactions
{'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

Order logSurveillanceAlertAnalystSTOR to the CNMVReasoned closure
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:

ControlWhat 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 switchAvoid the pattern of layers withdrawn after a fill
Proportionate sizes on both sides and a single direction per stock and accountWhat is shown is what you want to execute
Reposition only when needed, with a maximum message rateKeep the RTS 9 ratio far from the venue's limits
Self-match preventionDo 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

  1. 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=1000 and with tras=5000. Do the market makers' alerts change?
Solution
IN [10]python
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())
OUT [10]A spoofer that waits
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.

  1. 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 times qty. Test it with a sell order for 200 when BOT_A has its buy order for 3,000 live.
Solution
IN [11]python
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))
OUT [11]A pre-trade check
(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.

  1. 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

  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. ↩ ↩2 ↩3 ↩4

  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. ↩

  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). ↩

  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). ↩ ↩2

  5. FCA, “FCA fines and prohibits hedge fund Chief Investment Officer for market abuse”, December 15, 2020, fca.org.uk. ↩

  6. Directive 2014/65/EU (MiFID II), Article 48(6), EUR-Lex. ↩

  7. Commission Delegated Regulation (EU) 2017/566 (RTS 9), Articles 2 and 3 and Annex, EUR-Lex. ↩

  8. Commission Delegated Regulation (EU) 2016/957 of 9 March 2016, Articles 3 and 7, EUR-Lex. ↩

  9. Commission Delegated Regulation (EU) 2017/589 (RTS 6), Articles 5 and 13, EUR-Lex. ↩

Each answer is folded under its question

Self-assessment

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  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