Lesson 4.4.1 · 60 min

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.

Fig. — EEA investment firms allowed to operate in Spain under the freedom to provide services, by home state (CNMV list, 809 firms, retrieved 22/10/2026)
Level 4 lessons
  1. 4.1 · Portfolio theory and optimisation

    1. 4.1.1 Markowitz: efficient frontier and real constraints
    2. 4.1.2 Covariance estimation: shrinkage, factor models and denoising
    3. 4.1.3 Black-Litterman: from market equilibrium to views
    4. 4.1.4 Risk parity, minimum variance, HRP and HERC
    5. 4.1.5 Optimization with CVaR, drawdown, turnover and costs
    6. 4.1.6 Factor models: CAPM, Fama-French, PCA and attribution
    7. 4.1.7 Rebalancing and strategy portfolios
    8. 4.1.8 Portfolio tools: Riskfolio-Lib, PyPortfolioOpt, skfolio and bt
  2. 4.2 · Risk management and position sizing

    1. 4.2.1 Risk per trade, stops and risk of ruin
    2. 4.2.2 Kelly criterion and volatility targeting: how much to risk
    3. 4.2.3 VaR, Expected Shortfall and VaR backtesting
    4. 4.2.4 Stress testing and correlation, liquidity, gap and counterparty risk
    5. 4.2.5 Drawdown control and kill switch
    6. 4.2.6 Derivatives risk: portfolio Greeks, margin and liquidations
  3. 4.3 · Microstructure and execution

    1. 4.3.1 Implementation shortfall, VWAP, TWAP and Almgren-Chriss
    2. 4.3.2 Order flow imbalance, microstructure signals and latency
  4. 4.4 · Brokers and APIs

    1. 4.4.1 Choosing a broker from Spain or the EU: regulation, assets, costs and API
    2. 4.4.2 Interactive Brokers: TWS and IB Gateway, ib_async, contracts, data and orders
    3. 4.4.3 Alpaca, Tradier, MetaTrader 5 with Python and OANDA v20
    4. 4.4.4 Crypto exchange APIs: ccxt, Binance, Bybit, Kraken and Coinbase
    5. 4.4.5 Secure credentials: environment variables, secret managers and least privilege
  5. 4.5 · Live trading system

    1. 4.5.1 Live system architecture: from data to reconciliation
    2. 4.5.2 Order management: states, partial fills, cancellations and reconnections
    3. 4.5.3 Paper trading, shadow trading and slippage monitoring
    4. 4.5.4 Task scheduling and alerts: cron, APScheduler, Prefect, Telegram, Slack and email
    5. 4.5.5 Fault tolerance: idempotency, persistent state and reconciliation at startup
    6. 4.5.6 NautilusTrader or LEAN live: from backtest to production with the same code

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 register1Note 1 · CNMV, “¿Cuáles son las entidades autorizadas?” (“Which entities are authorized?”, in Spanish), cnmv.es, and the list of EEA investment firms under freedom to provide services (809 entities) and the other investment firm lists, accessed on October 22, 2026; register of entities of the Banco de España..

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

Authorized in IrelandNotifies its supervisorFreedom to provide servicesBranch in SpainCNMV listThird countryClient's own initiative
Mermaid source
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)2Note 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, accessed on October 22, 2026.. 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 firms92
EEA investment firms with a branch in Spain46
EEA investment firms under freedom to provide services809
Non-EEA investment firms with a branch1
Non-EEA investment firms under freedom to provide services11

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:

IN [01]cnmv_libre_prestacion.pypython
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.

IN [02]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"]]
OUT [02]An extract in the format of the CNMV list
                             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 activities3Note 3 · ESMA, Databases and Registers, entry “MiFID investment firms”, and ESMA Registers, accessed on October 22, 2026.. 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:

IN [03]esma_registro_mifid.pypython
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 ones2. The fictitious extract with those fields:

IN [04]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
OUT [04]An extract in the format of the ESMA register
                             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.

IN [05]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}")
OUT [05]Checking a broker against the three sources
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.

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)4Note 4 · Directive 97/9/EC on investor-compensation schemes, Articles 4(1) and 7(1), English version on EUR-Lex, accessed on October 22, 2026.. 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)5Note 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 (in Spanish); Directive 2014/49/EU, Article 6(1), on EUR-Lex; accessed on October 22, 2026..
  • 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.
yespart missingThe broker failsSecurities segregated?They are returnedClaimCompensation up to thelimitLoss above it
Mermaid source
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):

IN [06]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)
OUT [06]Segregation, compensation and final loss
                                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

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:

CriterionWhat to look atHow to check it
AssetsMarkets 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
CommissionsScheme (per share, fixed, percentage, tiered), minimums and maximumsPublished fee schedule; the CNMV registers the maximum fees of Spanish entities
CurrencyMulti-currency account or automatic conversion on each trade; spread appliedFee schedule and the statement of a test trade
Custody and otherCustody, inactivity, market data, withdrawals, securities transfersThe full fee schedule, not the sales page
CashInterest on cash, rate on margin loansFee schedule; month-end statement
APIType (local socket, REST, websockets, FIX), test account, order types, client identifier, rate limits, data over the APIAPI documentation and a week on the test account (lessons 4.4.2 to 4.4.4)
OperationsRestarts and maintenance, support hours, procedure during outagesTechnical 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.

IN [07]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}")
OUT [07]Two trading profiles
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):

IN [08]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)
OUT [08]Total annual cost with explicit assumptions
                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

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:

IN [09]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"]]
OUT [09]System requirements versus the API
                   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 year6Note 6 · Royal Decree 1065/2007, Articles 42 ter and 42 quater, consolidated text in the BOE; Order HFP/886/2023 (Form 721) in the BOE; AEAT pages for Forms 720 and 721; all in Spanish, accessed on October 22, 2026.. 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 increase7Note 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 (in Spanish), accessed on October 22, 2026..
  • 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 7206.

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

IN [10]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})
OUT [10]Is Form 720 due for the securities block?
   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
IN [11]python
for fallo in [c for c, ok in comprobar("Dorado Trade LLC").items()
              if not ok]:
    print("NO:", fallo)
OUT [11]An entity from a third country
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).

  1. 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
IN [12]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
OUT [12]Brio with a multi-currency account
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.

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

Footnotes

  1. CNMV, “¿Cuáles son las entidades autorizadas?” (“Which entities are authorized?”, in Spanish), cnmv.es, and the list of EEA investment firms under freedom to provide services (809 entities) and the other investment firm lists, accessed on October 22, 2026; register of entities of the Banco de España. ↩

  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, accessed on October 22, 2026. ↩ ↩2 ↩3

  3. ESMA, Databases and Registers, entry “MiFID investment firms”, and ESMA Registers, 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, 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 (in Spanish); Directive 2014/49/EU, Article 6(1), on EUR-Lex; accessed on October 22, 2026. ↩

  6. Royal Decree 1065/2007, Articles 42 ter and 42 quater, consolidated text in the BOE; Order HFP/886/2023 (Form 721) in the BOE; AEAT pages for Forms 720 and 721; all in Spanish, accessed on October 22, 2026. ↩ ↩2

  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 (in Spanish), accessed on October 22, 2026. ↩

Each answer is folded under its question

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?

    • AA new authorization from the CNMV
    • BNothing: any EU firm can operate in any country without notice
    • CTo 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
    • DTo open a branch, necessarily
    Show answer

    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?

    • AThe 25% fall, up to EUR 100,000
    • BNothing: segregated securities are returned to the client, and no compensation scheme covers market losses
    • CEUR 100,000 in any case
    • DThat year's custody fee
    Show answer

    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?

    • AAncillary 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
    • BNothing, execution includes custody
    • CPortfolio management (A.4)
    • DDealing on own account (A.3)
    Show answer

    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?

    • AYes, every year
    • BYes, because it exceeds 50,000
    • CNo, never again
    • DNo: 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
    Show answer

    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?

    • AThe commission per order
    • BThe data fee
    • CCurrency conversion: 25 bp on about 53 million of traded value per year, around 13% of capital
    • DCustody
    Show answer

    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