---
title: "7.4.1 MiFID II: clients, suitability and algorithmic trading"
description: "Client categorization, opting up to professional, suitability and appropriateness, and the RTS 6 controls for algorithmic trading, with code."
url: "https://pythonparatrading.com/en/python-trading-course/level-7/mifid-ii-suitability-algorithmic-trading/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-7/mifid-ii-suitability-algorithmic-trading.png"
---

# 7.4.1 · MiFID II: clients, suitability and algorithmic trading

> MiFID II does not regulate the individual who trades their own money, but it decides what their broker can sell them and how. This lesson turns into code the rules that categorize and assess that client under the EU and Spanish framework, and reviews, as background culture, what RTS 6 requires of a firm that trades with algorithms.

- 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: 55 min
- Prerequisites: [4.2.5 · Drawdown control and kill switch](https://pythonparatrading.com/en/python-trading-course/level-4/drawdown-control-and-kill-switch/), [4.4.1 · Choosing a broker from Spain or the EU: regulation, assets, costs and API](https://pythonparatrading.com/en/python-trading-course/level-4/choosing-a-broker-in-the-eu/), [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

- Distinguish retail client, professional client and eligible counterparty under Directive 2014/65/EU, Spain's Law 6/2023 and Royal Decree 813/2023.
- Code the assessment of a request to be treated as a professional: two of the three Annex II criteria and the written procedure.
- Separate suitability (advice and portfolio management), appropriateness (other services) and execution-only, and write an appropriateness assessor with its record.
- List what Delegated Regulation (EU) 2017/589 (RTS 6) requires of a firm engaged in algorithmic trading and apply its pre-trade controls to an order log.

## A rule for firms that affects you as a client

Directive 2014/65/EU (MiFID II) regulates investment firms and markets. Someone who only trades their own money is
outside it: Article 2(1)(d) excludes those who deal on own account without providing investment services, unless
they are a market maker, a member of a regulated market or of an MTF, have direct electronic access, apply a
high-frequency algorithmic trading technique or execute client orders by dealing on own account. An individual with a
bot that sends orders through their broker's API is not in any of those cases.

But that individual is a **client** of a firm that is subject to it, and what the rules require of the firm decides
what the client can buy: whether it asks for a test before letting them trade derivatives, whether it applies the CFD
leverage limits (lesson 7.4.2), whether it can sell them an ETF without a key information document (lesson 7.4.3).
Everything depends on two decisions the firm makes with written rules: **which category** the client is in and
**which assessment** applies.

The second half of the lesson looks at the other side: what Delegated Regulation (EU) 2017/589, RTS 6, requires of a
firm engaged in algorithmic trading. It does not bind an individual, but it is the best list of controls there is for
a system that sends orders on its own, and it is what anyone who works on a trading desk will find (lesson 7.5.1).

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

**Not advice.** This lesson summarizes EU and Spanish rules as of October 23, 2026, with links to the texts in the
BOE (Spain's Official State Gazette, in Spanish). The framework is that of the EU as applied in Spain; other countries
have their own rules. The profiles, thresholds and questionnaires in the code are synthetic and serve to understand
the logic, not to categorize anyone: each firm applies its written policy, and the binding one is the one in the
contract.

</aside>

## Three client categories

MiFID II defines a professional client as one who meets the criteria of Annex II, and a retail client as “any client
who is not a professional client” (Article 4(1), points 10 and 11; unofficial translation of the Spanish text). The
eligible counterparty is in Article 30. In Spain, Law 6/2023 on Securities Markets and Investment Services (*Ley
6/2023 de los Mercados de Valores y de los Servicios de Inversión*), the Spanish act that transposes MiFID II, requires
firms to categorize all their clients into those three classes (Article 192), and Royal Decree 813/2023 (*Real Decreto
813/2023*) specifies who is a professional (Articles 112 to 114).

| Category | Who | Protection |
|---|---|---|
| Retail | Everyone who is not a professional: almost all individuals | Maximum: appropriateness, information, KID, product limits |
| Per se professional | Regulated financial institutions; large undertakings meeting two of: balance sheet total of 20 M EUR, net turnover of 40 M EUR, own funds of 2 M EUR; governments, central banks, supranational bodies; institutional investors | Lower; can request retail treatment |
| Professional on request | A retail client who requests it, meets two of three criteria and follows the procedure | The protection lost is notified in writing |
| Eligible counterparty | Investment firms, credit institutions, insurance companies, UCITS, pension funds, other regulated financial institutions, governments, central banks | Minimum; only in reception and transmission of orders, execution and dealing on own account |

Two details that are often confused. The eligible counterparty is not a fourth client class for everything: Article
192 of Law 6/2023 limits it to reception and transmission of orders, execution and dealing on own account, and makes
clear that it does not apply to portfolio management or advice. And the categorization can be changed in both
directions: a professional can request more protection (Annex II, Section I) and the firm can offer it on its own
initiative (Article 45(3) of Delegated Regulation (EU) 2017/565).

```mermaid
flowchart TB
  C[New client] --> F{Regulated entity?}
  F -- yes --> S{Service?}
  S -- orders --> CE[Eligible counterp.]
  S -- management or advice --> P[Professional]
  F -- no --> G{2 of 3 size tests?}
  G -- yes --> P
  G -- no --> M[Retail]
  M --> O{Requests prof. treatment?}
  O -- yes, and meets criteria --> PP[Prof. on request]
```

## Opting up to professional: two of three criteria

A retail client can waive part of their protection. Section II.1 of Annex II, which Article 113 of Royal Decree
813/2023 reproduces, requires the firm to verify **at least two** of these criteria:

- **Frequency.** Transactions of significant size on the relevant market for the instrument, or for similar
  instruments, at an average frequency of 10 per quarter over the previous four quarters.
- **Portfolio.** Financial instruments and cash deposits worth more than 500,000 EUR.
- **Professional experience.** A position of at least one year in the financial sector that requires knowledge of the
  transactions or services envisaged.

And a procedure (Section II.2 of the Annex; Article 114 of the Royal Decree): the client requests the change in
writing, in general or for a particular service or type of product; the firm warns them in writing of the protections
and investor compensation rights they may lose; and the client states in writing, **in a document separate from the
contract**, that they are aware of the consequences. If they later stop meeting the requirements, Article 114.3 of
the Royal Decree requires the firm to treat them as retail immediately.

The rules do not define “significant size”. Each firm's policy sets it, and that is why it is a parameter in the code.
Six synthetic profiles with their transactions over the last four quarters:

```python
import numpy as np
import pandas as pd

rng = np.random.default_rng(741)
perfiles = pd.DataFrame([
    # trades/quarter, median amount (EUR), portfolio, years in the sector
    ("A", 4, 3_000, 120_000, 0),
    ("B", 30, 1_500, 650_000, 0),
    ("C", 14, 9_000, 300_000, 2),
    ("D", 45, 12_000, 2_400_000, 0),
    ("E", 12, 6_000, 480_000, 0),
    ("F", 2, 50_000, 900_000, 6),
], columns=["cliente", "ops_trim", "importe_med", "cartera_eur",
            "anios_sector"]).set_index("cliente")

filas = []
for c in perfiles.itertuples():
    for trim in range(1, 5):
        n = rng.poisson(c.ops_trim)
        importes = rng.lognormal(np.log(c.importe_med), 0.6, n)
        filas += [(c.Index, trim, round(x, 2)) for x in importes]
ops = pd.DataFrame(filas, columns=["cliente", "trimestre", "importe"])
print(len(ops), "transactions")
ops.groupby("cliente").size().to_dict()
```
```output
413 transactions
{'A': 12, 'B': 113, 'C': 67, 'D': 162, 'E': 54, 'F': 5}
```

The assessor separates the criteria, which are measurable facts, from the procedure, which is documents. A request
that meets the criteria but lacks the separate declaration is not accepted:

```python
UMBRAL_SIGNIFICATIVO = 5_000  # EUR per transaction: policy (assumption)

def criterios(cliente, ops, perfiles, umbral=UMBRAL_SIGNIFICATIVO):
    o = ops[(ops["cliente"] == cliente) & (ops["importe"] >= umbral)]
    por_trim = o.groupby("trimestre").size().reindex(
        range(1, 5), fill_value=0)
    p = perfiles.loc[cliente]
    return {
        "media_trim": por_trim.mean(),
        "frecuencia": bool(por_trim.mean() >= 10),
        "cartera": bool(p["cartera_eur"] > 500_000),
        "sector": bool(p["anios_sector"] >= 1),
    }

def evaluar(cliente, documentos):
    c = criterios(cliente, ops, perfiles)
    n = c["frecuencia"] + c["cartera"] + c["sector"]
    falta = [d for d in ("solicitud_escrita", "advertencia_entregada",
                         "declaracion_separada")
             if not documentos.get(d)]
    if n < 2:
        decision = "rejected: criteria"
    elif falta:
        decision = "pending: " + ", ".join(falta)
    else:
        decision = "accepted"
    return {**c, "cumple": int(n), "decision": decision}

completos = dict(solicitud_escrita=True, advertencia_entregada=True,
                 declaracion_separada=True)
docs = {k: completos for k in perfiles.index}
docs["D"] = {**completos, "declaracion_separada": False}
tabla = pd.DataFrame({k: evaluar(k, v) for k, v in docs.items()}).T
tabla["media_trim"] = tabla["media_trim"].astype(float).round(2)
tabla
```
```output
   media_trim frecuencia cartera sector cumple                       decision
A        0.25      False   False  False      0             rejected: criteria
B        1.50      False    True  False      1             rejected: criteria
C       14.25       True   False   True      2                       accepted
D       37.50       True    True  False      2  pending: declaracion_separada
E        7.00      False   False  False      0             rejected: criteria
F        1.25      False    True   True      2                       accepted
```

The figure at the top summarizes the `cumple` column (criteria met). The most instructive case is **B**: about 30
transactions per quarter, which at first sight are well over ten, but of about 1,500 EUR each; above the firm's
threshold only 1.5 per quarter remain. The 650,000 EUR portfolio is their only criterion. **E** falls short on two
(seven significant transactions per quarter and a portfolio of 480,000 EUR). **C**, **D** and **F** meet two criteria
by different routes, and D stays pending because the separate declaration (`declaracion_separada`) is missing:
without it the waiver is not valid, however much wealth they have.

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

What is lost by being professional is not symbolic. The firm can presume that the client has the necessary knowledge
and experience for the products for which they are professional (Articles 54(3) and 56(1) of Delegated Regulation
2017/565); the intervention measures for retail clients stop applying, such as the leverage limits and the negative
balance protection of CFDs (lesson 7.4.2), and so does the obligation to provide the KID (lesson 7.4.3). Ads saying
“become a professional to get more leverage” are selling exactly that waiver.

</aside>

## Suitability, appropriateness and execution-only

Once the client is categorized, Article 25 of MiFID II decides what has to be assessed, and it depends on the
**service**, not on the product:

| Assessment | When | What is asked | Rule |
|---|---|---|---|
| Suitability | Investment advice and portfolio management | Knowledge and experience, financial situation and ability to bear losses, objectives and risk tolerance | Art. 25(2); Arts. 54-55 of Delegated Reg. 2017/565; Art. 204 of Law 6/2023 |
| Appropriateness | Other services (reception, transmission and execution of orders) | Only knowledge and experience with that type of product or service | Art. 25(3); Arts. 55-56 of Delegated Reg. 2017/565; Art. 205 of Law 6/2023 |
| Execution-only | Non-complex instruments, at the client's initiative, with a warning | Nothing | Art. 25(4); Art. 207 of Law 6/2023 |

Suitability ends in a recommendation and a written statement of why it fits the client (Article 25(6)). ESMA sets out
how to carry it out in its guidelines ESMA35-43-3172 (2023), which also apply when the advice is given by an
algorithm: Article 54(1) of the Delegated Regulation says that responsibility for the assessment lies with the firm
and is not reduced by the use of an automated system.

For a trader who uses an execution broker, what matters is **appropriateness**. If the product is not appropriate,
the firm must warn the client; if the client does not provide information, it must warn them that it cannot assess.
In both cases the client can go ahead, and the firm has to record the result, the warnings and whether the client
went ahead (Article 56(2)). The execution-only route, without an assessment, is limited to the instruments listed in
Article 25(4): shares admitted to trading on a regulated market or equivalent (without embedded derivatives), bonds
without derivatives or structures that are hard to understand, money market instruments, units in non-structured
UCITS, simple structured deposits and other non-complex instruments.

An appropriateness assessor that keeps its record. The experience criterion (five transactions in three years or a
related profession) is an example of a policy, not a figure from the rules:

```python
NO_COMPLEJOS = {"accion_cotizada", "etf_ucits", "bono_simple"}
PRODUCTOS = NO_COMPLEJOS | {"cfd", "opcion_cotizada", "futuro"}

def conveniencia(cliente, producto, test, profesional=False,
                 iniciativa_cliente=True):
    if producto not in PRODUCTOS:
        raise ValueError(f"unknown product: {producto}")
    if profesional:
        return "appropriate (presumption Art. 56.1)"
    if producto in NO_COMPLEJOS and iniciativa_cliente:
        return "execution-only (warning Art. 25.4.c)"
    if test is None:
        return "no information: warn (Art. 25.3)"
    conoce = producto in test["conoce"]
    experiencia = (test["ops_3a"].get(producto, 0) >= 5
                   or test["profesion_relacionada"])
    if conoce and experiencia:
        return "appropriate"
    return "not appropriate: warn (Art. 25.3)"

tests = {
    "A": dict(conoce={"accion_cotizada"}, ops_3a={},
              profesion_relacionada=False),
    "C": dict(conoce={"accion_cotizada", "futuro", "cfd"},
              ops_3a={"futuro": 12, "cfd": 2},
              profesion_relacionada=True),
    "E": dict(conoce={"cfd"}, ops_3a={"cfd": 3},
              profesion_relacionada=False),
}
registro = []
for cli, prod in [("A", "etf_ucits"), ("A", "cfd"), ("C", "cfd"),
                  ("E", "cfd"), ("E", "opcion_cotizada"),
                  ("B", "futuro"), ("F", "futuro")]:
    res = conveniencia(cli, prod, tests.get(cli),
                       profesional=(cli == "F"))
    registro.append({"cliente": cli, "producto": prod,
                     "resultado": res})
pd.DataFrame(registro)
```
```output
  cliente         producto                             resultado
0       A        etf_ucits  execution-only (warning Art. 25.4.c)
1       A              cfd     not appropriate: warn (Art. 25.3)
2       C              cfd                           appropriate
3       E              cfd     not appropriate: warn (Art. 25.3)
4       E  opcion_cotizada     not appropriate: warn (Art. 25.3)
5       B           futuro      no information: warn (Art. 25.3)
6       F           futuro   appropriate (presumption Art. 56.1)
```

The record tells the story that Article 56(2) asks for. A has no experience with CFDs and receives a warning; E knows
the product but has only made three transactions, fewer than the policy requires; B has not answered the test; C
qualifies through experience and through profession. F appears as appropriate without a test because in the example
they are professional for futures: the presumption of Article 56(1) is exactly what stops protecting the client who
asked for that treatment.

## RTS 6: what is required of a firm with algorithms

MiFID II defines algorithmic trading as trading in which an algorithm automatically determines the parameters of
orders (whether to send them, when, at what price, in what quantity, how they are managed) with limited or no human
intervention; routing orders or confirming trades is not enough (Article 4(1)(39)). A firm that engages in it must
have resilient systems, with thresholds and limits, that prevent erroneous orders, full testing and continuity
(Article 17(1)), and must notify its competent authority, which can ask it for a description of its strategies,
limits and testing (Article 17(2)).

RTS 6 details those obligations. Grouped:

| Block | Articles | What it requires |
|---|---|---|
| Governance | 1-4 | Lines of responsibility, separation between the trading desk and control, a compliance function that understands the algorithms, qualified staff; outsourcing does not transfer responsibility |
| Testing and deployment | 5-8 | Development and testing methodologies; each deployment or substantial change authorized by a designated person; a change record (who, when, who approves, what); conformance testing with the venue; a separate testing environment; limits set before deployment |
| After deployment | 9-11 | Annual self-assessment and validation with the criteria of Annex I; stress testing; analysis of each material change |
| Resilience | 12-18 | Kill functionality; automated surveillance for market abuse; business continuity; pre-trade controls; real-time monitoring with alerts within five seconds or less; post-trade controls and reconciliation; security and annual penetration tests |

The kill functionality (Article 12) and the disabling after repeated executions (Article 15(3)) were already coded in
lesson 4.2.5, and lesson 6.4.5 applied rate limits to an agent's proposals. Here the whole of Article 15 is completed,
with two details that tend to be forgotten: the limits count **all** orders sent, including modifications and
cancellations, at the moment they go out (Article 15(2)), and there must be a procedure to let a blocked order
through, exceptionally and with authorization (Article 15(6)).

```mermaid
flowchart TB
  O[Message from the algo] --> D{Algorithm active?}
  D -- no --> R[Reject and log]
  D -- yes --> M{Messages/sec?}
  M -- excess --> R
  M -- ok --> B{Band and size?}
  B -- outside --> R
  B -- inside --> E[To the venue]
  E --> X{Repeated exec.?}
  X -- yes --> P[Disable]
```

The order log is synthetic: an hour of normal messages from an algorithm `alfa` on two fictitious stocks, plus three
planted incidents: a price 10% away from the market, an order for 15,000 NOVA shares (797,250 EUR in value) and
another for 250,000 ORBE shares. An algorithm `beta` gets into a modification loop (250 messages in one second) and
another one, `gamma`, buys 100 shares every two seconds without stopping. In the log, `lado` is the side (`C` buy,
`V` sell) and `tipo` the message type (`nueva` new, `modifica` modify, `cancela` cancel):

```python
rng = np.random.default_rng(15)
REF = {"NOVA": 53.15, "ORBE": 12.10}  # reference price
n = 1200
base = pd.DataFrame({
    "ts": np.sort(rng.integers(0, 3_600_000, n)),  # ms within one hour
    "algo": "alfa",
    "instr": rng.choice(list(REF), n),
    "lado": rng.choice(["C", "V"], n),
    "cant": rng.integers(1, 20, n) * 100,
    "tipo": rng.choice(["nueva", "modifica", "cancela"], n,
                       p=[0.6, 0.25, 0.15]),
})
base["precio"] = (base["instr"].map(REF)
                  * (1 + rng.normal(0, 0.002, n))).round(2)

def msg(ts, algo, instr, lado, cant, tipo, precio):
    return dict(ts=ts, algo=algo, instr=instr, lado=lado, cant=cant,
                tipo=tipo, precio=precio)

incidentes = [
    msg(600_000, "alfa", "NOVA", "C", 300, "nueva", 58.47),
    msg(900_000, "alfa", "NOVA", "V", 15_000, "nueva", 53.15),
    msg(1_200_000, "alfa", "ORBE", "V", 250_000, "nueva", 12.08),
]
incidentes += [msg(2_000_000 + 4 * k, "beta", "NOVA", "C", 100,
                   "modifica", 53.10) for k in range(250)]
incidentes += [msg(3_000_000 + 2_000 * k, "gamma", "ORBE", "C", 100,
                   "nueva", 12.11) for k in range(40)]
ordenes = (pd.concat([base, pd.DataFrame(incidentes)])
           .sort_values("ts", kind="stable").reset_index(drop=True))
ordenes.groupby("algo").size().to_dict()
```
```output
{'alfa': 1203, 'beta': 250, 'gamma': 40}
```

The controls, with the limits as data and not as constants hidden in the code (Article 8 requires them to be set
before the algorithm is deployed). The simplification of the simulator is that every accepted new order is executed:

```python
from collections import Counter, deque
from dataclasses import dataclass

@dataclass(frozen=True)
class Limites:
    banda: float = 0.05          # maximum deviation from the reference
    valor_max: float = 250_000   # EUR per order
    cant_max: int = 20_000       # shares per order
    msg_seg: int = 50            # messages per second and algorithm
    repeticiones: int = 20       # identical executions within 60 s

class Controles:
    def __init__(self, lim, ref):
        self.lim, self.ref = lim, ref
        self.ventanas = {}                # algo -> ts of its messages
        self.ejecuciones = {}             # (algo, instr, lado) -> ts
        self.desactivados = set()
        self.registro = []

    def evaluar(self, m, excepcion=None):
        w = self.ventanas.setdefault(m.algo, deque())
        w.append(m.ts)                    # Art. 15.2: everything counts now
        while w[0] <= m.ts - 1_000:
            w.popleft()
        motivo = self._motivo(m, len(w))
        if motivo and excepcion and motivo != "disabled":
            motivo = None                 # Art. 15.6, authorized
        if motivo is None and m.tipo == "nueva":
            self._ejecutada(m)
        self.registro.append(dict(ts=m.ts, algo=m.algo,
                                  resultado=motivo or "accepted",
                                  excepcion=excepcion))
        return motivo

    def _motivo(self, m, en_ventana):
        if m.algo in self.desactivados:
            return "disabled"
        if en_ventana > self.lim.msg_seg:
            return "messages"
        if m.tipo == "cancela":
            return None                   # cancelling is always allowed
        if abs(m.precio / self.ref[m.instr] - 1) > self.lim.banda:
            return "band"
        if m.cant > self.lim.cant_max:
            return "volume"
        if m.precio * m.cant > self.lim.valor_max:
            return "value"
        return None

    def _ejecutada(self, m):
        e = self.ejecuciones.setdefault((m.algo, m.instr, m.lado),
                                        deque())
        e.append(m.ts)
        while e[0] <= m.ts - 60_000:
            e.popleft()
        if len(e) >= self.lim.repeticiones:
            self.desactivados.add(m.algo)  # Art. 15.3

ctl = Controles(Limites(), REF)
for m in ordenes.itertuples():
    ctl.evaluar(m)
reg = pd.DataFrame(ctl.registro)
print(reg.groupby(["algo", "resultado"]).size().to_string())
print("disabled:", sorted(ctl.desactivados))
```
```output
algo   resultado
alfa   accepted     1200
       band            1
       value           1
       volume          1
beta   accepted       50
       messages      200
gamma  accepted       20
       disabled       20
disabled: ['gamma']
```

Each control triggers where it should. The band stops the price that is 10% too high; the maximum volume, the order
for 250,000 ORBE, and the maximum value, the one for 15,000 NOVA, which is not large in shares but is in euros. The
message limit cuts off `beta`'s loop at message 51 of that second, even though they are modifications that would
never execute anything: if the limit only looked at executions, the whole loop would have reached the venue. `gamma`
never exceeds the message rate, one purchase every two seconds is normal traffic, but at the twentieth identical
purchase within a minute it is disabled and its next 20 orders are rejected. No normal `alfa` message has been
blocked: limits that reject legitimate traffic end up being switched off by the desk itself.

Disabling does not undo itself. A re-arm, like an exception, is an act of a designated person that is recorded:

```python
bloqueada = ordenes.iloc[reg.index[reg["resultado"] == "value"][0]]
motivo = ctl.evaluar(bloqueada, excepcion="risk: R. Gil, block "
                     "agreed with the client")
print("retry with exception:", motivo or "accepted")

def rearmar(ctl, algo, persona, causa):
    ctl.desactivados.discard(algo)
    ctl.registro.append(dict(ts=None, algo=algo, resultado="re-arm",
                             excepcion=f"{persona}: {causa}"))

rearmar(ctl, "gamma", "L. Mar", "loop fixed (v1.4.2)")
pd.DataFrame(ctl.registro).tail(2)[["algo", "resultado", "excepcion"]]
```
```output
retry with exception: accepted
       algo resultado                                   excepcion
1493   alfa  accepted  risk: R. Gil, block agreed with the client
1494  gamma    re-arm                 L. Mar: loop fixed (v1.4.2)
```

## Stress tests and annual self-assessment

Article 10 sets the minimum load of the stress tests: the highest number of messages sent and received over the
previous six months, **multiplied by two**, and the highest trading volume over the same period, also times two,
without touching the production environment. With the daily peaks of messages per second over six months
(synthetic):

```python
rng = np.random.default_rng(10)
sesiones = pd.bdate_range("2026-04-01", periods=126)
pico = pd.Series(rng.lognormal(np.log(22), 0.25, 126).round(),
                 index=sesiones, name="msg_seg")
pico.iloc[87] = 61              # earnings day and volatility
prueba = 2 * pico.max()
print(f"six-month peak: {pico.max():.0f} msg/s on "
      f"{pico.idxmax().date()}; test: {prueba:.0f} msg/s")
print(f"days above the 50 msg/s limit: "
      f"{(pico > Limites().msg_seg).sum()}")
```
```output
six-month peak: 61 msg/s on 2026-07-31; test: 122 msg/s
days above the 50 msg/s limit: 1
```

*Fig. — Daily peak of messages per second of an algorithm over six months (synthetic): the RTS 6 stress test is run at twice the maximum.* Bars for 126 sessions, from April to September 2026, with the daily peak of messages per second, between 13 and 40 except one day. The maximum, highlighted, is 61 on 31 July. A dashed line marks the pre-trade limit of 50 messages per second, exceeded that day, and another the test load, 122.

The test teaches two things at once. The system has to withstand 122 messages per second without degrading, and the
pre-trade limit of 50 per second, which seemed comfortable in the previous section, would have rejected legitimate
traffic on the day of the peak. Limits are calibrated with the data of the six months, not once at deployment.

The annual self-assessment (Article 9) reviews the algorithms, governance, business continuity and compliance with
Article 17, using the criteria of Annex I: nature of the business (role in the market, automation, strategies,
latency sensitivity), scale (algorithms running in parallel, instruments, messages, positions) and complexity. The
risk management function draws it up, internal audit audits it and senior management approves it. As an executable
checklist:

```python
REQUISITOS = {
    "Art. 5.2 deployment authorized by a designated person": "ok",
    "Art. 5.7 change record (who, when, approves, what)": "ok",
    "Art. 7 testing environment separate from production": "ok",
    "Art. 10 test at twice the six-month maximum": "fail",
    "Art. 12 kill functionality tested": "ok",
    "Art. 14.4 continuity reviewed and tested this year": None,
    "Art. 15.1 bands, value, volume and messages": "ok",
    "Art. 16.5 alerts within 5 s or less": "ok",
    "Art. 18.4 annual penetration tests": None,
}
for req, estado in REQUISITOS.items():
    if estado != "ok":
        print(f"{'NO EVIDENCE' if estado is None else 'FAIL':13}"
              f" {req}")
```
```output
FAIL          Art. 10 test at twice the six-month maximum
NO EVIDENCE   Art. 14.4 continuity reviewed and tested this year
NO EVIDENCE   Art. 18.4 annual penetration tests
```

An individual does not have to hand this report to anyone, but the questions are the same for a system that trades
only its owner's money: who approved the latest version, where the tests are, what happens if the bot gets into a
loop at three in the morning and who disables it. Lessons 7.2.4 (business continuity) and 7.3.3 (automatic model
shutdown) develop those answers for your own infrastructure.

## What this changes in your system

- **Your category decides your product.** Before designing a strategy with CFDs, US ETFs or options, check what your
  broker will let you do as a retail client; opting up to professional has requirements and a price in protection.
- **The appropriateness test is not a formality.** Answering whatever “unlocks” a product makes a warning that the
  firm has already given you, and recorded, your own responsibility.
- **Copy Article 15 even if nobody makes you.** Band, value, volume, messages and repeated executions cost a few
  lines and stop the failures that cost the most money.
- **Limits are calibrated with data.** Twice the six-month maximum is a good rule for your own infrastructure.

## Exercises

1. Change the significant-size threshold to 1,000 EUR and assess the requests again. Which clients change decision?
   What does that tell you about the importance of the firm's written policy?

**Solution.**

```python
def evaluar_con(umbral):
    out = {}
    for k in perfiles.index:
        c = criterios(k, ops, perfiles, umbral=umbral)
        out[k] = int(c["frecuencia"] + c["cartera"] + c["sector"])
    return out

print("5,000 EUR:", evaluar_con(5_000))
print("1,000 EUR:", evaluar_con(1_000))
```
```output
5,000 EUR: {'A': 0, 'B': 1, 'C': 2, 'D': 2, 'E': 0, 'F': 2}
1,000 EUR: {'A': 0, 'B': 2, 'C': 2, 'D': 2, 'E': 1, 'F': 2}
```

With a threshold of 1,000 EUR, most of B's transactions count and B now meets two criteria; E gains the frequency
criterion, but stays at one. With the same activity, one firm would accept B as a professional and another would
not. That is why Annex II requires written categorization policies, and why it is worth reading your broker's before
requesting the change.

2. Add to the controls engine a **position** limit per algorithm and instrument (for example, 5,000 net shares) that
   uses the accepted new orders. Which article of RTS 6 requires it and for which instruments does it require it
   explicitly?

**Solution.**

```python
class ControlesConPosicion(Controles):
    def __init__(self, lim, ref, pos_max=5_000):
        super().__init__(lim, ref)
        self.pos_max, self.pos = pos_max, Counter()

    def _motivo(self, m, en_ventana):
        motivo = super()._motivo(m, en_ventana)
        if motivo or m.tipo != "nueva":
            return motivo
        signo = 1 if m.lado == "C" else -1
        if abs(self.pos[(m.algo, m.instr)] + signo * m.cant) \
                > self.pos_max:
            return "position"
        return None

    def _ejecutada(self, m):
        signo = 1 if m.lado == "C" else -1
        self.pos[(m.algo, m.instr)] += signo * m.cant
        super()._ejecutada(m)

ctl2 = ControlesConPosicion(Limites(), REF)
for m in ordenes.itertuples():
    ctl2.evaluar(m)
r2 = pd.DataFrame(ctl2.registro)
r2.groupby(["algo", "resultado"]).size().to_dict()
```
```output
{('alfa', 'accepted'): 1132, ('alfa', 'band'): 1, ('alfa', 'position'): 68, ('alfa', 'value'): 1, ('alfa', 'volume'): 1, ('beta', 'accepted'): 50, ('beta', 'messages'): 200, ('gamma', 'accepted'): 20, ('gamma', 'disabled'): 20}
```

The position limit trims the `alfa` orders that would accumulate more than 5,000 net shares: with an algorithm that
buys and sells at random, the position drifts and the limit ends up stopping part of the normal traffic, so in
practice it is set from the position history. Article 15(4) requires credit and market risk limits based on capital
and strategy, and Article 17(4) requires, for derivatives, controls on maximum long, short and overall positions.

3. A friend shows you an ad: “Become a professional client and trade with 1:500 leverage”. Explain, with the articles
   in this lesson, what they would have to prove, what documents they would sign and what protections they would
   lose.

**Solution.**

They would have to meet two of the three criteria of Annex II (Article 113 of Spain's Royal Decree 813/2023): ten
transactions of significant size per quarter on average over the last year, a portfolio of more than 500,000 EUR, or
one year in a related position in the financial sector. They would sign a written request, receive a written warning
of the protections they lose and sign a declaration in a document separate from the contract (Article 114). They
would lose, among other things, an effective appropriateness assessment (the firm can presume their knowledge), the
leverage limits, the close-out at 50% of margin and the negative balance protection of CFDs, which only apply to
retail clients (lesson 7.4.2). The firm, moreover, must take reasonable steps to check that they meet the requirements
before accepting: an ad that offers it with no further ado is a red flag.

## Self-assessment

1. A client makes about 30 transactions per quarter averaging 1,500 EUR and has a portfolio of 650,000 EUR. They have never worked in the financial sector. Can their broker treat them as a professional if they ask?

   - A. Yes: they trade more than 10 times per quarter
   - B. Yes: the portfolio exceeds 500,000 EUR and that is enough
   - C. No: they only meet one criterion (the portfolio); transactions count if they are of significant size, and theirs are not according to the firm's policy
   - D. No: individuals can never be professionals

   *Answer C: No: they only meet one criterion (the portfolio); transactions count if they are of significant size, and theirs are not according to the firm's policy.* Annex II requires at least two of three criteria. Frequency is measured in transactions “of significant size”, not in transactions alone: in the lesson, client B only has 1.5 per quarter above the firm's threshold.

2. What assessment does a broker that only receives and executes orders carry out when a retail client wants to buy a CFD?

   - A. Appropriateness: whether the client has the knowledge and experience to understand the product (Art. 25(3))
   - B. Suitability: whether the product fits their objectives and their ability to bear losses
   - C. None: it is an order at the client's initiative
   - D. It only asks for the KID

   *Answer A: Appropriateness: whether the client has the knowledge and experience to understand the product (Art. 25(3)).* Suitability (Art. 25(2)) is for investment advice and portfolio management. In the other services, appropriateness is assessed (Art. 25(3)). The execution-only route (Art. 25(4)) is only available for non-complex instruments, and a CFD is not one.

3. Under Article 15 of RTS 6, what counts towards the pre-trade limits?

   - A. Only executed orders
   - B. All orders sent to the trading venue, included immediately in the calculation
   - C. Only new orders, not modifications
   - D. The previous day's orders

   *Answer B: All orders sent to the trading venue, included immediately in the calculation.* Article 15(2) requires all orders sent to be included immediately. A control that only looked at executions would not stop a loop of modifications, which is exactly what the message limit cuts off in the lesson.

4. With what load must the stress tests of Article 10 of RTS 6 be run?

   - A. With the average of the last six months
   - B. With the volume of a normal day
   - C. With twice the highest number of messages and twice the highest trading volume of the previous six months
   - D. With whatever load each firm freely sets

   *Answer C: With twice the highest number of messages and twice the highest trading volume of the previous six months.* Article 10 sets the two minimum tests: messages and trading volume, each at twice the maximum of the previous six months, and without affecting the production environment.

5. When is an entity an “eligible counterparty” under Spain's Law 6/2023?

   - A. Whenever it is a financial institution, for any service
   - B. When an individual with more than 500,000 EUR requests it
   - C. When it trades with algorithms
   - D. Only in reception and transmission of orders, execution and dealing on own account; not in portfolio management or advice

   *Answer D: Only in reception and transmission of orders, execution and dealing on own account; not in portfolio management or advice.* Article 192 of Law 6/2023 limits the category to those services and their ancillary services. For portfolio management or advice, the same entity is a professional client.

## Key points

- Every investment firm categorizes its clients as retail, professional or eligible counterparties; an individual is retail unless they request and obtain professional treatment.
- Opting up to professional requires two of three criteria (an average of 10 transactions of significant size per quarter over the previous four, a portfolio of more than 500,000 EUR, one year in a position in the financial sector) and a written procedure with a separate declaration.
- Suitability for advice and portfolio management; appropriateness for the rest; execution-only only with non-complex instruments, at the client's initiative and with a warning.
- Being professional removes protections: the firm can presume knowledge and experience, and the measures designed for retail clients (CFD limits, KID) stop applying.
- RTS 6 requires firms engaged in algorithmic trading to have governance, testing, an annual self-assessment, a kill functionality, pre-trade controls on all orders and stress tests at twice their six-month maximum.

## Resources

### Research

- [Finite-state machines for trading in Python](https://pythonparatrading.com/en/finite-state-machines-trading-python/) · Research-0029 · 47 min

### Documentation

- [Directive 2014/65/EU (MiFID II), in the BOE (Spain's official gazette, in Spanish): Articles 4, 17, 25 and 30 and Annex II](https://www.boe.es/buscar/doc.php?id=DOUE-L-2014-81285) · boe.es
- [Delegated Regulation (EU) 2017/589 (RTS 6), in the BOE (in Spanish)](https://www.boe.es/buscar/doc.php?id=DOUE-L-2017-80584) · boe.es
- [Delegated Regulation (EU) 2017/565: categorization (Art. 45), suitability and appropriateness (Arts. 54-56), in the BOE (in Spanish)](https://www.boe.es/buscar/doc.php?id=DOUE-L-2017-80560) · boe.es
- [Spain's Law 6/2023 on Securities Markets and Investment Services (Arts. 192-208), consolidated text (in Spanish)](https://www.boe.es/buscar/act.php?id=BOE-A-2023-7053) · boe.es
- [Spain's Royal Decree 813/2023 (Arts. 112-114: professional clients), consolidated text (in Spanish)](https://www.boe.es/buscar/act.php?id=BOE-A-2023-22763) · boe.es
- [ESMA (2023): Guidelines on certain aspects of the MiFID II suitability requirements (ESMA35-43-3172)](https://www.esma.europa.eu/sites/default/files/2023-04/ESMA35-43-3172_Guidelines_on_certain_aspects_of_the_MiFID_II_suitability_requirements.pdf) · esma.europa.eu

Previous lesson: [7.3.4 · Model serving: ONNX, CPU or GPU inference and the cost of LLMs in production](https://pythonparatrading.com/en/python-trading-course/level-7/model-serving-onnx-llm-costs/)

Next lesson: [7.4.2 · ESMA and CFDs: leverage, close-out and negative balance](https://pythonparatrading.com/en/python-trading-course/level-7/esma-cfd-leverage-investor-protection/)
