---
title: "4.4.1 Choosing a broker from Spain and the EU"
description: "CNMV and ESMA registers, MiFID II passport, segregation and compensation, total cost, API and Spanish Forms 720 and 721, with code and no recommendations."
url: "https://pythonparatrading.com/en/python-trading-course/level-4/choosing-a-broker-in-the-eu/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-4/choosing-a-broker-in-the-eu.png"
---

# 4.4.1 · Choosing a broker from Spain or the EU: regulation, assets, costs and API

> Before comparing commissions you need to know which entity you sign with, who supervises it and what happens to your securities if it fails. This lesson turns each criterion into a check: it reads, with code, extracts in the format of the CNMV and ESMA registers, works out what segregation and compensation schemes cover, estimates the total cost of two trading profiles with explicit assumptions and applies the rules of Spanish Forms 720 and 721. It recommends no broker and is not advice.

- Course: [Level 4: Portfolios, risk, execution and live trading](https://pythonparatrading.com/en/python-trading-course/level-4/)
- Module: 4.4 · Brokers and APIs
- Duration: 60 min
- Prerequisites: [1.1.1 · Market participants and trading venues](https://pythonparatrading.com/en/python-trading-course/level-1/market-participants-and-trading-venues/), [1.1.5 · Costs, settlement, leverage and margin](https://pythonparatrading.com/en/python-trading-course/level-1/trading-costs-settlement-leverage-and-margin/), [2.4.7 · Cost modeling: commissions, bid-ask spread, impact, financing and securities borrowing](https://pythonparatrading.com/en/python-trading-course/level-2/backtest-transaction-cost-modeling/), [3.7.5 · MiCA: the EU framework for crypto-assets and their service providers (CASPs)](https://pythonparatrading.com/en/python-trading-course/level-3/mica-casp-eu-crypto-regulation/)

## By the end of this lesson

- Identify the legal entity that provides the service and its regime: investment firm or credit institution, Spanish, from another EEA state (freedom to provide services or branch) or from a third country.
- Query and process with code the CNMV and ESMA registers: authorized services (Annex I of MiFID II), authorization status, passport and warnings about unauthorized or cloned entities.
- Distinguish what asset segregation protects from what a compensation scheme covers (EUR 100,000 at FOGAIN, a minimum of 20,000 in the EU) and from what nobody covers: market losses.
- Estimate the total annual cost of a trading profile (commissions, currency, custody, data) with explicit assumptions and see how what matters changes with the profile.
- Apply the threshold and increase rules of the Spanish information returns Form 720 and Form 721 as published by the BOE and the AEAT.

## Before the price, the entity

Choosing a broker usually starts with a commission table. This lesson starts with another question: **which legal
entity signs the contract**. It determines who supervises it, which services it may provide, where the securities are,
which compensation scheme covers the client if it fails and what reporting obligations a Spanish resident has. Lesson
1.1.1 presented the broker as one link in the chain of an order; here it is treated as what it also is, a
counterparty.

In the EU, whoever receives, transmits or executes client orders is an **investment firm** (in Spain, *sociedades* and
*agencias de valores*) or a **credit institution** (a bank) authorized for those services. The CNMV (Comisión Nacional
del Mercado de Valores, Spain's securities regulator) puts it this way: to contract an investment service you have to
go to authorized entities, and to find out which ones are, which services each can offer and their maximum fees, you
have to consult its official registers; for the agents of a credit institution, the Banco de España register[^1].

The entity can be Spanish or come from abroad under two different regimes:

```mermaid
flowchart TB
  A[Authorized in Ireland] --> C[Notifies its supervisor]
  C --> L[Freedom to provide services]
  C --> S[Branch in Spain]
  L --> R[CNMV list]
  S --> R
  T[Third country] --> I[Client's own initiative]
```

- **European passport.** An investment firm authorized in another member state can provide in Spain the services
  covered by its authorization, with no additional requirements on what MiFID II regulates, by notifying the authority
  of its home state beforehand (freedom to provide services, Article 34 of Directive 2014/65/EU) or by opening a branch
  (Article 35)[^2]. Prudential supervision stays with the home state, and so does the compensation scheme.
- **Third country.** An entity from outside the EU has no passport. Each state may require it to have a branch
  authorized by its supervisor (Article 39), and the authorization requirement does not apply when the client initiates
  the service at their own exclusive initiative (Article 42). That exception does not make the entity supervised in the
  EU: the client is left outside the European protections.

The CNMV publishes the list of EEA investment firms operating in Spain under freedom to provide services with the data
the home authorities send it (who are “responsible for ensuring that the information submitted is accurate”). On
October 22, 2026 it had 809 entities from 29 states; the figure at the top counts them by country:

| CNMV list (query of October 22, 2026) | Entities |
|---|---|
| Spanish broker-dealers (*sociedades* and *agencias de valores*) | 114 |
| Financial advisory firms | 92 |
| EEA investment firms with a branch in Spain | 46 |
| EEA investment firms under freedom to provide services | 809 |
| Non-EEA investment firms with a branch | 1 |
| Non-EEA investment firms under freedom to provide services | 11 |

An entity appearing on a list says nothing about its quality: it says that it is authorized and by whom. What follows
turns that check, and the others, into code.

## Reading the registers with code

Three primary sources are enough for the regulatory part: the CNMV lists, the ESMA register and the CNMV warnings
section (unauthorized entities, including those that imitate the name or website of an authorized one). The CNMV list
is an HTML page with a four-line block per entity. The real code to download and count it:

```python skip title="cnmv_libre_prestacion.py"
import collections
import html
import re
from urllib.request import Request, urlopen

URL = ("https://www.cnmv.es/portal/consultas/listadoentidad.aspx"
       "?id=1&tipoent=4")             # EEA firms, freedom to provide services
req = Request(URL, headers={"User-Agent": "Mozilla/5.0"})
with urlopen(req, timeout=30) as r:
    pagina = r.read().decode("utf-8", errors="replace")
texto = html.unescape(re.sub(r"<[^>]+>", "\n", pagina))
paises = re.findall(r"País:\s*([^\n]+)", texto)
print(len(paises), collections.Counter(paises).most_common(3))
```

With the page of October 22, 2026 it prints 809 entities, with Cyprus (220), Germany (100) and the Netherlands (94) in
the lead. To run it without a network, the next cell uses a **fictitious** extract in the same format (the page is in
Spanish, so the field labels are too): three made-up brokers (Alcor, Brio and Cenit, those of lesson 2.4.7) and one
more entity.

```python
import re
import pandas as pd

LISTA = """
ALCOR SECURITIES IRELAND LIMITED
 Número y fecha de registro oficial: 9101 - 14/03/2021
 Dirección: 1 Quay Street - D01 DUBLIN
 País: IRLANDA
BRIO BROKERS B.V.
 Número y fecha de registro oficial: 9102 - 02/06/2017
 Dirección: Kade 2 - 1000 AMSTERDAM
 País: PAISES BAJOS
CENIT MARKETS (CY) LTD
 Número y fecha de registro oficial: 9103 - 10/04/2012
 Dirección: Odos 3 - 3035 LIMASSOL
 País: CHIPRE
DELTA FUTURES GMBH
 Número y fecha de registro oficial: 9104 - 01/09/2026
 Dirección: Platz 4 - 60311 FRANKFURT AM MAIN
 País: ALEMANIA
"""
patron = re.compile(
    r"^(?P<nombre>\S[^\n]*)\n"
    r"\s*Número y fecha de registro oficial: (?P<num>\d+)"
    r" - (?P<fecha>\d\d/\d\d/\d{4})\n"
    r"\s*Dirección: (?P<direccion>[^\n]*)\n"
    r"\s*País: (?P<pais>[^\n]+)", re.M)
cnmv = pd.DataFrame([m.groupdict() for m in patron.finditer(LISTA)])
cnmv["fecha"] = pd.to_datetime(cnmv["fecha"], format="%d/%m/%Y")
cnmv[["nombre", "num", "fecha", "pais"]]
```
```output
                             nombre   num      fecha          pais
0  ALCOR SECURITIES IRELAND LIMITED  9101 2021-03-14       IRLANDA
1                 BRIO BROKERS B.V.  9102 2017-06-02  PAISES BAJOS
2            CENIT MARKETS (CY) LTD  9103 2012-04-10        CHIPRE
3                DELTA FUTURES GMBH  9104 2026-09-01      ALEMANIA
```

The CNMV list carries neither LEI nor services. That is what the ESMA register of investment firms (“MiFID investment
firms”) is for: it collects the firms authorized by national authorities with their services and activities[^3]. Its
web interface queries a search index that returns JSON; the query below worked on October 22, 2026, but it is not a
documented API and can change without notice:

```python skip title="esma_registro_mifid.py"
import json
from urllib.parse import urlencode
from urllib.request import Request, urlopen

BASE = ("https://registers.esma.europa.eu/solr/"
        "esma_registers_upreg/select?")
def consulta(q, **extra):
    url = BASE + urlencode({"q": q, "wt": "json", "rows": 50, **extra})
    req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
    with urlopen(req, timeout=30) as r:
        return json.load(r)["response"]["docs"]

firmas = consulta('ae_entityName:"Entity name"')
for f in firmas:                       # entity: status, member state, dates
    print(f["ae_entityName"], f["ae_lei"], f["ae_status"],
          f["ae_homeMemberState"],
          f["ae_authorisationNotificationDateStr"])
    hijos = consulta('{!child of="type_s:parent"}id:' + f["id"])
    print([h["ac_serviceName"] for h in hijos
           if h["entity_type"] == "aeActivity"])
```

Each entity is a “parent” document (`ae_entityName`, `ae_lei`, `ae_competentAuthority`, `ae_homeMemberState`,
`ae_status`, authorization and withdrawal dates) with “child” documents per service (`ac_serviceName`, `ac_status`).
The service names follow Annex I of MiFID II: section A is the investment services and section B the ancillary
ones[^2]. The fictitious extract with those fields:

```python
ANEXO_I = {
    "reception and transmission of orders": "A1",
    "execution of orders": "A2",
    "dealing on own account": "A3",
    "portfolio management": "A4",
    "investment advice": "A5",
    "safekeeping and administration": "B1",
    "granting credits or loans": "B2",
    "foreign exchange services": "B4",
}
def ent(nombre, lei, pais, estado, alta, baja, servicios):
    return {"ae_entityName": nombre, "ae_lei": lei,
            "ae_homeMemberState": pais, "ae_status": estado,
            "ae_authorisationNotificationDateStr": alta,
            "ae_authorisationWithdrawalEndDateStr": baja,
            "hijos": [{"ac_serviceName": s, "ac_status": "Active"}
                      for s in servicios]}

esma_json = [
    ent("Alcor Securities Ireland Limited", "5299009ALCOR0IE0001",
        "IRELAND", "Active", "22/12/2020", "",
        ["Reception and transmission of orders",
         "Execution of orders", "Dealing on own account",
         "Safekeeping and administration",
         "Granting credits or loans", "Foreign exchange services"]),
    ent("Brio Brokers B.V.", "5299009BRIO00NL0002", "NETHERLANDS",
        "Active", "02/03/2017", "",
        ["Reception and transmission of orders",
         "Execution of orders"]),
    ent("Cenit Markets (CY) Ltd", "5299009CENIT0CY0003", "CYPRUS",
        "Inactive", "05/03/2012", "30/09/2026",
        ["Reception and transmission of orders",
         "Execution of orders", "Dealing on own account",
         "Safekeeping and administration"]),
]
filas = []
for e in esma_json:
    servicios = {ANEXO_I.get(h["ac_serviceName"].lower(), "?")
                 for h in e["hijos"] if h["ac_status"] == "Active"}
    filas.append({
        "nombre": e["ae_entityName"], "pais": e["ae_homeMemberState"],
        "estado": e["ae_status"],
        "baja": pd.to_datetime(
            e["ae_authorisationWithdrawalEndDateStr"],
            format="%d/%m/%Y"),
        "servicios": "".join(sorted(servicios))})
esma = pd.DataFrame(filas)
esma
```
```output
                             nombre         pais    estado       baja     servicios
0  Alcor Securities Ireland Limited      IRELAND    Active        NaT  A1A2A3B1B2B4
1                 Brio Brokers B.V.  NETHERLANDS    Active        NaT          A1A2
2            Cenit Markets (CY) Ltd       CYPRUS  Inactive 2026-09-30      A1A2A3B1
```

Alcor has orders (A1, A2), own account (A3), custody (B1), credit for trading (B2) and foreign exchange (B4). Brio only
receives and executes orders: it is not authorized for custody, so its clients' securities will be at another entity,
and that is the one to identify and check. Cenit is listed as inactive, with its authorization withdrawn on September
30, 2026, even though the extract of the CNMV list still shows it: two registers may not be in sync on the same day,
and the home authority's register prevails.

## Cross-checking the sources and looking for clones

Names do not match across registers: capitalization, abbreviated legal forms (“LIMITED” and “Ltd”), periods and
parentheses. They have to be normalized before matching. The CNMV also warns about entities that impersonate authorized
ones with an almost identical name or website; a fuzzy comparison of names against the warnings list detects them. The
LEI, whose check digits are validated as in lesson 3.7.5, is the reliable key when it is available; the CNMV list does
not include it.

```python
import difflib

def norma(nombre):
    """Uppercase, no punctuation or spaces, abbreviated forms."""
    n = re.sub(r"[^A-Z0-9 ]", " ", nombre.upper())
    n = re.sub(r"\bLIMITED\b", "LTD", n)
    n = re.sub(r"\bB V\b", "BV", n)
    return re.sub(r"\s+", "", n)

ADVERTENCIAS = ["ALCOR SECURITIES LTD (alcor-securities.example)",
                "ORION CAPITAL FX"]
cnmv["clave"] = cnmv["nombre"].map(norma)
esma["clave"] = esma["nombre"].map(norma)
avisos = {norma(a.split(" (")[0]) for a in ADVERTENCIAS}
autorizadas = set(cnmv["clave"]) | set(esma["clave"])

def comprobar(nombre, necesito="A1A2B1", dia="2026-10-22"):
    k = norma(nombre)
    r = {"in the CNMV list": k in set(cnmv["clave"])}
    fila = esma[esma["clave"] == k]
    r["in the ESMA register"] = not fila.empty
    if not fila.empty:
        f = fila.iloc[0]
        r["authorization in force"] = (f["estado"] == "Active" and (
            pd.isna(f["baja"]) or f["baja"] > pd.Timestamp(dia)))
        pedidos = [necesito[i:i + 2]
                   for i in range(0, len(necesito), 2)]
        r["services " + necesito] = all(s in f["servicios"]
                                        for s in pedidos)
    r["no CNMV warning"] = k not in avisos
    if k not in autorizadas:          # does it imitate an authorized one?
        r["does not imitate an authorized firm"] = all(
            difflib.SequenceMatcher(None, k, a).ratio() < 0.8
            for a in autorizadas)
    return r

for nombre in ["Alcor Securities Ireland Limited", "Brio Brokers BV",
               "Cenit Markets (CY) Ltd", "Alcor Securities Ltd"]:
    fallos = [c for c, ok in comprobar(nombre).items() if not ok]
    print(f"{nombre:<33s} {'OK' if not fallos else 'NO'} {fallos}")
```
```output
Alcor Securities Ireland Limited  OK []
Brio Brokers BV                   NO ['services A1A2B1']
Cenit Markets (CY) Ltd            NO ['authorization in force']
Alcor Securities Ltd              NO ['in the CNMV list', 'in the ESMA register', 'no CNMV warning', 'does not imitate an authorized firm']
```

Alcor passes every check for an order service with custody. Brio is registered and active, but it lacks B.1. Cenit has
had its authorization withdrawn. And “Alcor Securities Ltd”, a name almost identical to Alcor's, is in no register and
is on the warnings list: it is the pattern of a clone, which copies the brand of an authorized entity with another
company and another website. The automatic check does not replace reading the contract, but it avoids the most
expensive mistake: trading with a different company from the one you think.

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

The entity that matters is the one on the contract and the statements, not the one in the advertising. Many groups
have companies in several countries (for example, one in the US and another in the EU) under the same brand and with
different protections. You have to look up that specific company, by its exact name and, if it has one, by its LEI.

</aside>

## What is protected and what is not

Two different mechanisms protect a broker's client, and it pays not to confuse them with a third one that does not
exist:

- **Segregation.** MiFID II requires an investment firm that holds clients' financial instruments to take measures to
  safeguard their ownership rights, “especially in the event of the investment firm's insolvency”, and not to use them
  on its own account except with the client's express consent; and, for funds, to safeguard them and, except for credit
  institutions, not to use them on its own account (Article 16, paragraphs 8 and 9)[^2]. If segregation works, in a
  failure the securities belong to the client and are returned. Express consent matters: some brokers lend out their
  clients' securities, and that lending creates counterparty exposure.
- **Compensation scheme.** If the entity cannot return the money or securities, the scheme pays what is missing up to
  a limit. Directive 97/9/EC requires cover of at least EUR 20,000 (ECU in its text) per investor, and the home state's
  scheme also covers the clients of its branches in other states (Articles 4 and 7)[^4]. In Spain, the Fondo General de
  Garantía de Inversiones (FOGAIN, Spain's investor compensation fund) covers the investor's “overall credit position”
  against the investment firm with a limit of EUR 100,000 (Royal Decree 948/2001, Article 6), when the firm has been
  declared insolvent or the CNMV declares that it cannot meet its obligations (Article 5); for credit institutions the
  scheme works through the deposit guarantee funds, and deposits are covered up to EUR 100,000 (Directive 2014/49/EU,
  Article 6)[^5].
- **What nobody covers.** The preamble of the royal decree says so expressly: the cover “will not extend to losses in
  the value of the investment or to any credit risk”. A market fall, a bad strategy or a bond that defaults are not the
  scheme's business.

```mermaid
flowchart TB
  Q[The broker fails] --> S{Securities segregated?}
  S -- yes --> D[They are returned]
  S -- part missing --> F[Claim]
  F --> I[Compensation up to the limit]
  I --> P[Loss above it]
```

The cell computes what an investor with EUR 150,000 in securities and 20,000 in cash recovers in four scenarios, with
the Spanish limit and with the European minimum (which one applies depends on the entity's home state):

```python
def recupera(valores, efectivo, falta, limite):
    """falta: fraction of the holdings that is missing."""
    reclamable = falta * (valores + efectivo)
    cubierto = min(reclamable, limite)
    return reclamable, cubierto, reclamable - cubierto

valores, efectivo = 150_000, 20_000
casos = {"correct segregation": 0.0,
         "30% missing": 0.3,
         "fraud: all missing": 1.0}
filas = []
for caso, falta in casos.items():
    for sistema, limite in [("FOGAIN", 100_000), ("EU minimum", 20_000)]:
        rec, cub, perd = recupera(valores, efectivo, falta, limite)
        filas.append({"caso": caso, "sistema": sistema,
                      "reclamable": rec, "cubierto": cub,
                      "pérdida": perd})
# no failure: the market falls 25%
filas.append({"caso": "market falls 25%", "sistema": "none",
              "reclamable": 0, "cubierto": 0,
              "pérdida": 0.25 * valores})
pd.DataFrame(filas).set_index(["caso", "sistema"]).astype(int)
```
```output
                                reclamable  cubierto  pérdida
caso                sistema
correct segregation FOGAIN               0         0        0
                    EU minimum           0         0        0
30% missing         FOGAIN           51000     51000        0
                    EU minimum       51000     20000    31000
fraud: all missing  FOGAIN          170000    100000    70000
                    EU minimum      170000     20000   150000
market falls 25%    none                 0         0    37500
```

*Fig. — Investor with 150,000 euros in securities and 20,000 in cash: what the compensation scheme covers (dark segment) and what is lost (light segment), in thousand euros, by shortfall and limit; a market fall is covered by no scheme.* Five stacked horizontal bars. 30% shortfall with FOGAIN: 51 thousand covered, no loss. With the EU minimum: 20 covered, 31 lost. Total fraud with FOGAIN: 100 covered, 70 lost. With the EU minimum: 20 covered, 150 lost. A 25% market fall with no failure, highlighted: 37.5 lost that nobody covers.

With correct segregation, the failure costs the client nothing but time: the securities are transferred to another
entity. With a 30% shortfall (EUR 51,000), FOGAIN covers it in full and the European minimum leaves a loss of EUR
31,000. In total fraud, the final loss is EUR 70,000 with the Spanish limit and 150,000 with the European minimum. And
the market fall, EUR 37,500, is covered by no scheme. Two practical consequences: the entity's home state matters (it
sets the scheme and its limit), and spreading large wealth across different entities reduces what is left above the
limit.

## Assets, costs and API: criteria and how to check them

With the entity checked, the remaining criteria are technical. Each has a way of checking it that does not depend on
the broker's advertising:

| Criterion | What to look at | How to check it |
|---|---|---|
| Assets | Markets and instrument types; UCITS ETFs versus US ones (lesson 7.4.3); derivatives and CFDs (lesson 7.4.2) | The broker's list of markets and contracts; for a specific ETF, look it up on its platform before opening the account |
| Commissions | Scheme (per share, fixed, percentage, tiered), minimums and maximums | Published fee schedule; the CNMV registers the maximum fees of Spanish entities |
| Currency | Multi-currency account or automatic conversion on each trade; spread applied | Fee schedule and the statement of a test trade |
| Custody and other | Custody, inactivity, market data, withdrawals, securities transfers | The full fee schedule, not the sales page |
| Cash | Interest on cash, rate on margin loans | Fee schedule; month-end statement |
| API | Type (local socket, REST, websockets, FIX), test account, order types, client identifier, rate limits, data over the API | API documentation and a week on the test account (lessons 4.4.2 to 4.4.4) |
| Operations | Restarts and maintenance, support hours, procedure during outages | Technical documentation and the broker's status page |

Each broker's prices change and are not reproduced here. Lesson 2.4.7 modeled the commissions of three fictitious
brokers (`costes2_brokers.csv`) and their crossover points by order size. This cell goes further: it applies those
fees to the real orders of the live system of module 4.5 (the live account, `vivo4_ordenes.csv`) and adds the costs
that are not commissions, with **fictitious, explicit assumptions**, for two profiles: that daily system and a
portfolio of ten ETFs rebalanced once a month.

```python
ops = pd.read_csv("vivo4_extracto_operaciones.csv")
ops["efectivo"] = ops["cantidad"] * ops["precio"]
por_orden = ops.groupby("id_orden_broker").agg(
    acciones=("cantidad", "sum"), efectivo=("efectivo", "sum"))
por_orden["precio"] = por_orden["efectivo"] / por_orden["acciones"]
import exchange_calendars as xcals

dias = ops["fecha_operacion"]
sesiones = len(xcals.get_calendar("XNYS").sessions_in_range(
    dias.min(), dias.max()))           # sessions in the period
anual = 252 / sesiones
acciones_mes = por_orden["acciones"].sum() / sesiones * 21
print(f"{sesiones} sessions ({dias.nunique()} with trades), "
      f"{len(por_orden)} orders; shares per month: {acciones_mes:,.0f}")
print(f"statement commissions, annualized: "
      f"{ops['comision'].sum() * anual:,.0f} USD")

capital = 1_000_000                      # USD, the module's live account
diario = pd.DataFrame({
    "acciones": por_orden["acciones"].to_numpy(),
    "precio": por_orden["precio"].to_numpy(),
    "veces": anual})                      # each order, annualized
mensual = pd.DataFrame({                  # assumption: 10 ETFs, 12 months
    "acciones": [17] * 10, "precio": [100.0] * 10, "veces": 12})
perfiles = {"daily": (diario, capital), "monthly": (mensual, 200_000)}
for p, (d, cap) in perfiles.items():
    ef = (d["acciones"] * d["precio"] * d["veces"]).sum()
    print(f"{p:8s} orders/year {d['veces'].sum():6.0f}  "
          f"value/year {ef:12,.0f}  turnover {ef / cap:5.1f}")
```
```output
56 sessions (35 with trades), 250 orders; shares per month: 42,919
statement commissions, annualized: 2,684 USD
daily    orders/year   1125  value/year   52,964,780  turnover  53.0
monthly  orders/year    120  value/year      204,000  turnover   1.0
```

The daily system executes 250 orders in the 56 sessions of the period (it trades on 35 of them): annualized, about
1,125 orders and 53 million dollars of traded value with a one-million account, a turnover of 53 times capital. The
statement commissions, annualized, are 2,684 dollars. The monthly portfolio, with 200,000 dollars, places 120 small
orders and turns over its capital once a year. The costs that are not commissions, by broker (all fictitious):

```python
import numpy as np

tarifas = pd.read_csv("costes2_brokers.csv")
def comision(broker, acciones, precio):
    """First tier of each plan from 2.4.7 (vectorized)."""
    t = tarifas[(tarifas["broker"] == broker)
                & (tarifas["tramo_desde"] == 0)].iloc[-1]
    ef = acciones * precio
    c = {"por_accion": t["tarifa"] * acciones,
         "escalonado": t["tarifa"] * acciones,
         "fijo_por_orden": t["tarifa"] + 0 * acciones,
         "porcentaje": t["tarifa"] / 100 * ef}[t["tipo"]]
    if pd.notna(t["minimo_orden"]):
        c = np.maximum(c, t["minimo_orden"])
    if pd.notna(t["maximo_pct_efectivo"]):
        c = np.minimum(c, t["maximo_pct_efectivo"] / 100 * ef)
    return c

SUPUESTOS = {   # FICTITIOUS: check the fee schedule of each real broker
    "Alcor": {"fx_pb": 2, "fx_auto": False, "custodia_pct": 0.0,
              "datos_mes": 10.0},
    "Brio": {"fx_pb": 25, "fx_auto": True, "custodia_pct": 0.0,
             "datos_mes": 0.0},
    "Cenit": {"fx_pb": 10, "fx_auto": False, "custodia_pct": 0.10,
              "datos_mes": 0.0},
}
filas = []
for p, (d, cap) in perfiles.items():
    ef = (d["acciones"] * d["precio"] * d["veces"]).sum()
    for b, s in SUPUESTOS.items():
        com = (comision(b, d["acciones"], d["precio"])
               * d["veces"]).sum()
        convertido = cap + (ef if s["fx_auto"] else 0)
        filas.append({"perfil": p, "broker": b, "comisiones": com,
                      "divisa": convertido * s["fx_pb"] / 1e4,
                      "custodia": cap * s["custodia_pct"] / 100,
                      "datos": 12 * s["datos_mes"]})
coste = pd.DataFrame(filas).set_index(["perfil", "broker"])
coste["total"] = coste.sum(axis=1)
cap_perfil = coste.index.get_level_values(0).map(
    {p: c for p, (_, c) in perfiles.items()})
coste["pct_capital"] = 100 * coste["total"] / cap_perfil.to_numpy()
coste.round(2)
```
```output
                comisiones     divisa  custodia  datos      total  pct_capital
perfil  broker
daily   Alcor      1865.43     200.00       0.0  120.0    2185.43         0.22
        Brio       5568.75  134911.95       0.0    0.0  140480.70        14.05
        Cenit     42379.20    1000.00    1000.0    0.0   44379.20         4.44
monthly Alcor        42.00      40.00       0.0  120.0     202.00         0.10
        Brio        594.00    1010.00       0.0    0.0    1604.00         0.80
        Cenit       240.00     200.00     200.0    0.0     640.00         0.32
```

*Fig. — Total annual cost under fictitious assumptions (lesson 2.4.7 commissions, FX, custody and data) as a percentage of capital: monthly portfolio of 200,000 USD and daily system of 1 million with 53x turnover.* Two bar panels with different scales. Monthly portfolio: Alcor 0.10%, Brio 0.80%, Cenit 0.32%. Daily system: Alcor 0.22%, Brio 14.0% (highlighted, almost all from automatic FX conversion) and Cenit 4.4% (almost all from the percentage commission).

With the same three brokers, what decides changes with the profile:

- **Monthly portfolio.** Commissions are small in absolute terms but the per-order minimums weigh (17 shares at 100
  dollars pay the minimum on all three plans), and fixed costs appear: Alcor's data fee, Cenit's annual custody, the
  initial conversion of the capital. All totals stay below 1% of capital, and the differences between brokers are
  hundreds of dollars.
- **Daily system.** With a turnover of 53, everything proportional to traded value is multiplied by 53. Cenit's 0.08%
  is 42,379 dollars a year, 4.2% of capital; Brio's automatic conversion at 25 bp, 134,912 dollars, 13.5%, much more
  than any commission. Under those assumptions, the strategy would not survive at Brio or at Cenit even if its gross
  edge were good.
- **Fee schedule versus statement.** Alcor's fee schedule applied order by order gives 1,865 dollars, less than the
  2,684 charged on the live account statement: the broker in the data applies the minimum per execution (an order with
  three executions can pay three minimums) and adds two per-trade fees on sales (the ones the data README calls SEC and
  TAF). A cost model is calibrated against the statement, not the fee page.

The lesson is not which broker is cheaper (they are fictitious), but the method: a measured profile (orders, sizes,
turnover, currency), the full fee schedule and the total cost as a percentage of capital, compared with the strategy's
expected return net of the other costs (lessons 2.4.7 and 4.3.1). An assumption that changes the ranking, such as
currency conversion, deserves a test trade and a reading of the statement before you rely on it.

For the API, the check is similar: system requirements against what each broker documents. With fictitious
attributes:

```python
API = pd.DataFrame({
    "tipo": ["local socket", "REST + websocket", "REST"],
    "cuenta_prueba": [True, True, False],
    "orden_al_cierre": [True, False, True],
    "id_cliente": [True, True, True],
    "datos_por_api": [True, True, False],
}, index=["Alcor", "Brio", "Cenit"])
REQUISITOS = ["cuenta_prueba", "orden_al_cierre", "id_cliente",
              "datos_por_api"]
API["no_cumple"] = API[REQUISITOS].apply(
    lambda r: [c for c in REQUISITOS if not r[c]], axis=1)
API[["tipo", "no_cumple"]]
```
```output
                   tipo                       no_cumple
Alcor      local socket                              []
Brio   REST + websocket               [orden_al_cierre]
Cenit              REST  [cuenta_prueba, datos_por_api]
```

The module 4.5 system executes 15% of its orders at the close (MOC), needs a client identifier so as not to duplicate
orders on reconnection (lesson 4.5.5) and wants a test account for weeks of paper trading. They are requirements, not
preferences: a broker that does not meet them does not enter the cost comparison. The public API documentation
(lessons 4.4.2 to 4.4.4) and a few days on the test account check them.

## Reporting obligations in Spain: Forms 720 and 721

A Spanish resident with securities or crypto-assets at entities abroad has **reporting** obligations that are separate
from the taxation of gains (covered in lesson 7.4.6). What follows are the facts from the official sources, not advice;
each specific case should be verified with the AEAT (Agencia Tributaria, the Spanish Tax Agency) or an adviser.

- **Form 720** (*modelo 720*, assets and rights located abroad). It has three blocks: accounts, securities and
  insurance, and real estate. For “securities, rights, insurance and income deposited, managed or obtained abroad”, the
  General Regulation on tax management and inspection (RD 1065/2007, Article 42 ter) exempts filing when the combined
  value does not exceed EUR 50,000; if it does, all of them have to be reported. In later years, the return only has
  to be filed again when the combined value of the block rises by more than EUR 20,000 over the value determined by the
  last return, and also in certain cases of loss of ownership. Filing window: from January 1 to March 31 of the
  following year[^6]. According to the AEAT, listed shares can be valued at their December 31 price, amounts in foreign
  currency are converted at that date's exchange rate and exchange rate changes count toward the EUR 20,000
  increase[^7].
- **Form 721** (*modelo 721*, virtual currencies located abroad, held by entities that safeguard keys on behalf of
  third parties). Same scheme: no obligation if the combined balance on December 31 does not exceed EUR 50,000, a new
  return with an increase above 20,000 and a filing window from January 1 to March 31 (RD 1065/2007, Article 42 quater;
  Order HFP/886/2023). Virtual currencies are not reported on Form 720[^6].

The threshold and increase rule, applied to a block of dollar securities at a foreign broker, with **assumed** values
and exchange rates:

```python
cartera = pd.DataFrame({          # illustrative ASSUMPTIONS
    "anio": [2022, 2023, 2024, 2025],
    "valor_usd": [48_000, 61_000, 70_000, 82_000],
    "eurusd_31dic": [1.07, 1.10, 1.04, 1.17],
})
cartera["valor_eur"] = cartera["valor_usd"] / cartera["eurusd_31dic"]
ultima = None                     # value of the last return
toca = []
for v in cartera["valor_eur"]:
    if ultima is None:
        presenta = v > 50_000
    else:
        presenta = v - ultima > 20_000
    toca.append(presenta)
    if presenta:
        ultima = v
cartera["presenta"] = toca
cartera.round({"valor_eur": 0})
```
```output
   anio  valor_usd  eurusd_31dic  valor_eur  presenta
0  2022      48000          1.07    44860.0     False
1  2023      61000          1.10    55455.0      True
2  2024      70000          1.04    67308.0     False
3  2025      82000          1.17    70085.0     False
```

In 2022 the block does not reach EUR 50,000 and there is no obligation. In 2023 it exceeds it and the return has to be
filed. In 2024 and 2025 the value in dollars rises, but the increase in euros over the last return also depends on the
exchange rate: with a dearer euro at the end of 2025, 82,000 dollars are fewer euros, and the cumulative increase does
not pass 20,000. The calculation is simple; what requires care is the value used (the December 31 price or the other
criteria the AEAT accepts) and that date's exchange rate.

## Checklist and typical mistakes

A minimal list before opening an account, in the order in which it is best done:

1. Exact name of the company that signs the contract, its home state and its LEI.
2. Authorization in force in its authority's register and in ESMA's, with services A.1/A.2 and B.1; entry in the
   corresponding CNMV list; absence from the warnings.
3. The home state's compensation scheme, its limit and whether securities are lent out.
4. The full fee schedule applied to your own profile: total cost as a percentage of capital.
5. API and test account against the system's requirements.
6. Reporting (720, 721) and tax (lesson 7.4.6) obligations that the account creates.

Typical mistakes:

- **Checking the brand and not the company.** The register is searched for the entity named on the contract.
- **Confusing compensation with a guarantee on the investment.** No scheme covers market losses.
- **Comparing only per-order commissions.** Currency, custody, minimums and data can weigh more, and their weight
  depends on turnover.
- **Accepting a fee schedule without a test trade.** The statement says what is really charged (lesson 4.5.3 compares
  it with the backtest).
- **Forgetting the reporting obligations** because the account is abroad: the threshold is per block and combined, not
  per entity.

## Exercises

1. Add to the ESMA extract an entity “Dorado Trade LLC”, with a home state outside the EU, and check what the
   `comprobar` function says. What would it need, under MiFID II, to be able to provide services to a client in
   Spain?

**Solution.**

```python
for fallo in [c for c, ok in comprobar("Dorado Trade LLC").items()
              if not ok]:
    print("NO:", fallo)
```
```output
NO: in the CNMV list
NO: in the ESMA register
```

It is neither in the CNMV list nor in ESMA's register of firms authorized in the EU (ESMA publishes separately the
list of branches of third-country firms authorized under Article 41(2)). Under Article 39 of MiFID II, a state may
require a branch and an authorization from a third-country firm; the exception in Article 42 only covers services that
the client initiates at their own exclusive initiative, and even then the client is left outside the EU protections
(segregation supervised in the EU, a European compensation scheme).

2. Repeat the total cost of the daily system assuming that Brio offers a multi-currency account (it converts only the
   capital, once). How much does its cost fall as a percentage of capital? Which cost dominates then?

**Solution.**

```python
SUPUESTOS["Brio"]["fx_auto"] = False
d, cap = perfiles["daily"]
com = (comision("Brio", d["acciones"], d["precio"]) * d["veces"]).sum()
fx = cap * SUPUESTOS["Brio"]["fx_pb"] / 1e4
print(f"commissions {com:,.0f}  currency {fx:,.0f}  "
      f"total {100 * (com + fx) / cap:.2f}% of capital")
SUPUESTOS["Brio"]["fx_auto"] = True
```
```output
commissions 5,569  currency 2,500  total 0.81% of capital
```

Without conversion on every trade, Brio's cost falls from 14.0% to 0.81% of capital, and what dominates is the fixed
commission per order (4.95 dollars) multiplied by about 1,125 orders a year. The currency assumption was the deciding
one; that is why it has to be checked with a test trade.

3. Under the Form 721 rules, a resident holds crypto-assets on a foreign exchange worth EUR 45,000 on 12/31/2024 and
   52,000 on 12/31/2025, and has never filed the form. Do they have to file it in 2025 and in 2026?

**Solution.**

Not for 2024: the combined balance on December 31 (EUR 45,000) does not exceed 50,000. Yes for 2025, in the window
from January 1 to March 31, 2026, because 52,000 exceeds the threshold and it is the first time. The EUR 20,000
increase only applies once a return has been filed. Balances are valued in euros on December 31 of each year (Article
42 quater of RD 1065/2007). It is a reporting obligation: the taxation of gains is another matter (lesson 7.4.6).

[^1]: CNMV, “¿Cuáles son las entidades autorizadas?” (“Which entities are authorized?”, in Spanish),
    [cnmv.es](https://www.cnmv.es/portal/inversor/Entidades-Autorizadas), and the
    [list of EEA investment firms under freedom to provide services](https://www.cnmv.es/portal/consultas/listadoentidad.aspx?id=1&tipoent=4)
    (809 entities) and the other investment firm lists, accessed on October 22, 2026; register of entities of the
    [Banco de España](https://app.bde.es/ren_www/InicioSesion).
[^2]: Directive 2014/65/EU (MiFID II), Articles 16(8) and 16(9) (safeguarding of client instruments and funds), 34
    (freedom to provide services), 35 (branches), 39 and 42 (third-country firms) and Annex I (services A.1 to A.9 and
    B.1 to B.7), English version on [EUR-Lex](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32014L0065),
    accessed on October 22, 2026.
[^3]: ESMA, [Databases and Registers](https://www.esma.europa.eu/publications-and-data/databases-and-registers),
    entry “MiFID investment firms”, and [ESMA Registers](https://registers.esma.europa.eu/publication/searchRegister?core=esma_registers_upreg),
    accessed on October 22, 2026.
[^4]: Directive 97/9/EC on investor-compensation schemes, Articles 4(1) and 7(1), English version on
    [EUR-Lex](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:31997L0009), accessed on October 22, 2026.
[^5]: Royal Decree 948/2001 on investor compensation schemes, preamble and Articles 2, 5 and 6, consolidated text (last
    update published on December 28, 2023) in the [BOE](https://www.boe.es/buscar/act.php?id=BOE-A-2001-15221) (in
    Spanish); Directive 2014/49/EU, Article 6(1), on [EUR-Lex](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32014L0049);
    accessed on October 22, 2026.
[^6]: Royal Decree 1065/2007, Articles 42 ter and 42 quater, consolidated text in the
    [BOE](https://www.boe.es/buscar/act.php?id=BOE-A-2007-15984); Order HFP/886/2023 (Form 721) in the
    [BOE](https://www.boe.es/buscar/act.php?id=BOE-A-2023-17429); AEAT pages for Forms
    [720](https://sede.agenciatributaria.gob.es/Sede/procedimientoini/GI34.shtml) and
    [721](https://sede.agenciatributaria.gob.es/Sede/procedimientoini/GI55.shtml); all in Spanish, accessed on October
    22, 2026.
[^7]: AEAT, Form 720 FAQ (updated to February 2025): “Valoración de los bienes y derechos” (“Valuation of assets and
    rights”) and “Frecuencia en la presentación de la declaración” (“How often the return must be filed”), on the
    [e-office](https://sede.agenciatributaria.gob.es/Sede/procedimientoini/GI34.shtml) (in Spanish), accessed on
    October 22, 2026.

## Self-assessment

1. An investment firm authorized in Ireland wants to serve clients in Spain without opening an office. What does it need under MiFID II?

   - A. A new authorization from the CNMV
   - B. Nothing: any EU firm can operate in any country without notice
   - C. To notify its home authority that it will provide services in Spain (freedom to provide services, Art. 34); the Irish authorization is valid throughout the EU for the services it covers
   - D. To open a branch, necessarily

   *Answer C: To notify its home authority that it will provide services in Spain (freedom to provide services, Art. 34); the Irish authorization is valid throughout the EU for the services it covers.* Article 34 allows firms to provide the services covered by their authorization freely in other states, after notifying the authority of the home state, which passes it on to the host authority. That is why the CNMV publishes a list of EEA investment firms under freedom to provide services with the information those authorities send it.

2. A broker fails. A client's securities were segregated and are all there; in addition, the portfolio had fallen 25% because of the market. What does FOGAIN cover?

   - A. The 25% fall, up to EUR 100,000
   - B. Nothing: segregated securities are returned to the client, and no compensation scheme covers market losses
   - C. EUR 100,000 in any case
   - D. That year's custody fee

   *Answer B: Nothing: segregated securities are returned to the client, and no compensation scheme covers market losses.* Compensation schemes pay when the entity cannot return the client's money or securities, with a limit of EUR 100,000 at FOGAIN (RD 948/2001, Art. 6). The royal decree itself makes clear that the cover does not extend to losses in the value of the investment.

3. In the ESMA register, an entity is listed as an active `Investment firm` with the services “Reception and transmission of orders” and “Execution of orders”, but without “Safekeeping and administration”. What is missing to use it as a broker with custody?

   - A. Ancillary service B.1 of Annex I of MiFID II (safekeeping and administration of financial instruments for the account of clients): you have to find out who holds the securities
   - B. Nothing, execution includes custody
   - C. Portfolio management (A.4)
   - D. Dealing on own account (A.3)

   *Answer A: Ancillary service B.1 of Annex I of MiFID II (safekeeping and administration of financial instruments for the account of clients): you have to find out who holds the securities.* Reception and transmission (A.1) and execution of orders (A.2) are investment services; holding the client's securities is ancillary service B.1. If the entity does not have it, the securities will be at another entity, and that is the one to check.

4. A Spanish resident holds securities worth EUR 60,000 at a foreign broker on 12/31/2024 and filed Form 720 for them. On 12/31/2025 they are worth EUR 75,000. Do they have to file it again for that block?

   - A. Yes, every year
   - B. Yes, because it exceeds 50,000
   - C. No, never again
   - D. No: it is only mandatory when the combined value of the block rises by more than EUR 20,000 over the value determined by the last return, and here it rises by 15,000

   *Answer D: No: it is only mandatory when the combined value of the block rises by more than EUR 20,000 over the value determined by the last return, and here it rises by 15,000.* Article 42 ter of the General Regulation (RD 1065/2007) sets the threshold at EUR 50,000 per block and requires filing in later years only with an increase above EUR 20,000. The AEAT adds that exchange rate changes count toward that increase. There are other cases (ceasing to be the holder) that the AEAT details in its FAQ.

5. A daily strategy with a turnover of about 53 times capital per year trades dollar ETFs from a euro account with automatic currency conversion on every trade at 25 bp. Which cost dominates?

   - A. The commission per order
   - B. The data fee
   - C. Currency conversion: 25 bp on about 53 million of traded value per year, around 13% of capital
   - D. Custody

   *Answer C: Currency conversion: 25 bp on about 53 million of traded value per year, around 13% of capital.* With high turnover, any cost proportional to traded value is multiplied by the turnover. In the cost cell, automatic conversion costs more than everything else put together. A multi-currency account that converts the capital once changes the result completely.

## Key points

- The first question is not the price but the entity: which company signs the contract, in which state it is authorized, for which services and with what status. A brand can group companies from several countries with different protections.
- With the MiFID II passport, an investment firm authorized in another EEA state can operate in Spain under freedom to provide services or through a branch: the CNMV lists 809 under freedom to provide services (query of October 22, 2026), 220 of them authorized in Cyprus.
- The registers can be read with code: ESMA's gives the status, the withdrawal date and the Annex I services (A.1 and A.2 for orders, B.1 for custody); the CNMV warnings list gives unauthorized entities and clones with similar names.
- Segregation protects the client's securities if the entity fails; the compensation scheme covers what is missing up to a limit (EUR 100,000 at FOGAIN, a minimum of 20,000 in the EU depending on the home state); nobody covers market losses.
- The relevant cost is the total and it depends on the profile: in a monthly portfolio, fixed costs and minimums weigh most; in a daily system with high turnover, everything proportional to traded value (percentages and currency conversion) is multiplied by the turnover.
- Forms 720 and 721: threshold of EUR 50,000 per block on December 31, a new return only with an increase above 20,000, filing window from January 1 to March 31. They are information returns; taxation is covered in lesson 7.4.6.

## Resources

### Documentation

- [CNMV (Spanish securities regulator): list of EEA investment firms under freedom to provide services (in Spanish)](https://www.cnmv.es/portal/consultas/listadoentidad.aspx?id=1&tipoent=4) · cnmv.es
- [CNMV: which entities are authorized? and warnings about unauthorized entities (in Spanish)](https://www.cnmv.es/portal/inversor/Entidades-Autorizadas) · cnmv.es
- [ESMA: Databases and Registers (MiFID investment firms)](https://www.esma.europa.eu/publications-and-data/databases-and-registers) · esma.europa.eu
- [EUR-Lex: Directive 2014/65/EU (MiFID II), Arts. 16, 34, 35, 42 and Annex I](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32014L0065) · eur-lex.europa.eu
- [BOE (Spain's official gazette): Royal Decree 948/2001 on investor compensation schemes (consolidated text, in Spanish)](https://www.boe.es/buscar/act.php?id=BOE-A-2001-15221) · boe.es
- [AEAT (Spanish Tax Agency): Form 720, return on assets and rights located abroad (in Spanish)](https://sede.agenciatributaria.gob.es/Sede/procedimientoini/GI34.shtml) · sede.agenciatributaria.gob.es

### Data

- [Level 4 data: live system orders, paper and live account (CSV)](https://pythonparatrading.com/curso/datos/nivel-4/vivo4_ordenes.csv) · Download · vivo4_ordenes.csv

Previous lesson: [4.3.2 · Order flow imbalance, microstructure signals and latency](https://pythonparatrading.com/en/python-trading-course/level-4/order-flow-imbalance-and-latency/)

Next lesson: [4.4.2 · Interactive Brokers: TWS and IB Gateway, ib_async, contracts, data and orders](https://pythonparatrading.com/en/python-trading-course/level-4/interactive-brokers-with-ib-async/)
