---
title: "7.4.5 Market abuse: spoofing, layering and bots"
description: "EU MAR and its manipulation indicators from primary sources, a spoofing detector on an order log and the well-meaning bot that triggers it."
url: "https://pythonparatrading.com/en/python-trading-course/level-7/market-abuse-spoofing-layering-bots/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-7/market-abuse-spoofing-layering-bots.png"
---

# 7.4.5 · 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.

- Course: [Level 7: Production, MLOps, regulation, career and capstone](https://pythonparatrading.com/en/python-trading-course/level-7/)
- Module: 7.4 · Regulation, ethics and taxation
- Duration: 65 min
- Prerequisites: [1.1.3 · Order book: bid, ask, spread and depth](https://pythonparatrading.com/en/python-trading-course/level-1/order-book-bid-ask-spread-depth/), [1.1.4 · Order types](https://pythonparatrading.com/en/python-trading-course/level-1/order-types-explained/), [4.5.2 · Order management: states, partial fills, cancellations and reconnections](https://pythonparatrading.com/en/python-trading-course/level-4/order-management-states-fills-and-reconnections/), [6.4.5 · Guardrails for agents: the agent proposes, the engine decides](https://pythonparatrading.com/en/python-trading-course/level-6/guardrails-for-ai-agents/)

## By the end of this lesson

- Place spoofing, layering, quote stuffing and fictitious trades within Articles 12 and 15 of MAR and Annex II of Delegated Regulation (EU) 2016/522, including the 2026 amendment.
- Build a synthetic order log with market makers, a manipulator and two bots, and compute submissions, cancellations and fills per trader.
- Code a spoofing detector in the style of market surveillance (large orders near the best price, a small fill on the opposite side and an immediate cancellation) and measure its sensitivity to the thresholds.
- Recognize the bot design patterns (cancel everything after a fill, chase the price with cancel-and-replace) that produce the same footprint as manipulation, and fix them.
- Compute the RTS 9 ratio of unexecuted orders to transactions and know who monitors, what gets reported and with what consequences.

<aside class="callout" data-kind="aviso">

This lesson explains an EU regulation, as applied in Spain, from its primary sources so that a trading system does not
breach it by design. It is not legal advice: the lesson's detector is educational, its thresholds are examples and an
alert is not a legal assessment. For a specific case, read the text in force and, if needed, consult a professional.
Texts consulted on October 23, 2026, in their Spanish versions; quotations from legal texts are unofficial English
translations, except where the source is in English.

</aside>

## 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)[^1]. 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
transactions[^1]. 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 determined[^1]. 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*)[^2].

## 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)[^3].
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”[^4]. 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
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:

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

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

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

```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)
```
```output
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:

```python
cols = ["ts", "oid", "evento", "lado", "precio", "qty", "bid", "ask"]
log.loc[log["quien"] == "S1", cols].head(8)
```
```output
         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 $q$ at instant $t$ and the opposite orders $C$ that meet the time and price conditions:

$$
\text{alert} \iff \sum_{o \in C,\ t < t^{\text{cancel}}_o \le t + 1\,\text{s}} q_o \ \ge\ 5\,q
$$

```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
```
```output
       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.* Horizontal bars: S1, the manipulator, 100% of 20 executions; BOT_A, the bot with cancel-all, 96.5% of 86 (highlighted); market makers MM1 and MM2 and bot BOT_B, 0%.

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:

```python
pd.DataFrame({r: episodios(log, ratio=r).groupby("quien")["alerta"]
              .sum() for r in [1, 2, 5, 10]}).rename_axis(
    columns="ratio")
```
```output
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 period[^4]) and alerts go to an analyst, who looks
at the context: the impact on the book, the position, the trader's history.

<aside class="callout" data-kind="nota">

A detector like this is the one the UK's FCA describes in its case against a hedge fund manager fined £100,000 in
December 2020: between January and May 2017 he entered large CFD orders he did not intend to execute while executing
small orders on the opposite side, and the trading was identified by the FCA's own “surveillance algorithms” on order
book data from the main UK equity venues[^5].

</aside>

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

```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
```
```output
               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 transactions[^6], 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 volume[^7]:

$$
\text{OTR}_N = \frac{N_O}{N_E} - 1
\qquad
\text{OTR}_V = \frac{V_O}{V_E} - 1
$$

where $N_O$ and $V_O$ are the number and volume of orders, and $N_E$ and $V_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:

```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())
```
```output
{'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
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)[^8].
- **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)[^9].

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

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

```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())
```
```output
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.

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

```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))
```
```output
(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.

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

[^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](https://www.boe.es/doue/2014/173/L00001-00061.pdf) (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]: Law 6/2023 of March 17 on Securities Markets and Investment Services, Article 297.1.c,
    [BOE-A-2023-7053](https://www.boe.es/buscar/act.php?id=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](https://www.boe.es/doue/2016/088/L00001-00018.pdf) (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](https://www.boe.es/doue/2026/788/L00001-00006.pdf) (Spanish version; quotations are unofficial
    translations).
[^5]: FCA, “FCA fines and prohibits hedge fund Chief Investment Officer for market abuse”, December 15, 2020,
    [fca.org.uk](https://www.fca.org.uk/news/press-releases/fca-fines-and-prohibits-hedge-fund-chief-investment-officer-market-abuse).
[^6]: Directive 2014/65/EU (MiFID II), Article 48(6),
    [EUR-Lex](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32014L0065).
[^7]: Commission Delegated Regulation (EU) 2017/566 (RTS 9), Articles 2 and 3 and Annex,
    [EUR-Lex](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32017R0566).
[^8]: Commission Delegated Regulation (EU) 2016/957 of 9 March 2016, Articles 3 and 7,
    [EUR-Lex](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32016R0957).
[^9]: Commission Delegated Regulation (EU) 2017/589 (RTS 6), Articles 5 and 13,
    [EUR-Lex](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32017R0589).

## 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?

   - A. Nothing: without intent there is no manipulation
   - B. That it is manipulation only if the bot makes money
   - C. That it only applies to natural persons
   - 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

   *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?

   - 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)
   - B. Because their orders are far from the best price
   - C. Because the detector excludes market makers by name
   - D. Because they never cancel

   *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?

   - A. It prohibits retail algorithmic trading
   - 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
   - C. It repeals Annex II of Delegated Regulation 2016/522
   - D. It sets a numerical cancellation threshold above which there is manipulation

   *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?

   - A. It triggers the spoofing detector, because it cancels a lot
   - B. It triggers nothing, because it only buys
   - 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
   - D. It triggers a fictitious trades alert

   *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?

   - A. Nobody: only the CNMV, the Spanish regulator, monitors
   - B. Only the client themselves
   - 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
   - D. The data provider

   *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

- [Limit Order Book - Aggregated Market Liquidity](https://pythonparatrading.com/en/limit-order-book-and-market-liquidity/) · Research-0008 · 11 min
- [Finite-State Machines for Quantitative Trading Execution in Python](https://pythonparatrading.com/en/finite-state-machines-trading-python/) · Research-0029 · 47 min

### 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)](https://www.boe.es/doue/2014/173/L00001-00061.pdf) · boe.es
- [Delegated Regulation (EU) 2016/522: indicators of manipulation (Annex II), OJ L 88, 5.4.2016 (copy in the BOE; Spanish version)](https://www.boe.es/doue/2016/088/L00001-00018.pdf) · boe.es
- [Delegated Regulation (EU) 2026/788, amending the indicators of manipulation, OJ L, 16.7.2026 (copy in the BOE; Spanish version)](https://www.boe.es/doue/2026/788/L00001-00006.pdf) · boe.es
- [Delegated Regulation (EU) 2017/566 (RTS 9): ratio of unexecuted orders to transactions](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32017R0566) · eur-lex.europa.eu
- [FCA: fine and prohibition for a fund manager over misleading CFD orders (December 15, 2020)](https://www.fca.org.uk/news/press-releases/fca-fines-and-prohibits-hedge-fund-chief-investment-officer-market-abuse) · fca.org.uk

Previous lesson: [7.4.4 · MiCA after the transitional period: checking a provider in the registers](https://pythonparatrading.com/en/python-trading-course/level-7/mica-authorized-casp-registers/)

Next lesson: [7.4.6 · Trading taxes in Spain](https://pythonparatrading.com/en/python-trading-course/level-7/trading-taxes-in-spain/)
