Lesson 7.4.6 · 65 min
Trading taxes in Spain
A year of trades becomes a figure on the Spanish income tax return by following rules that can be programmed: which lot is sold, when a loss does not count, how balances are offset and how much of the tax paid abroad is recovered. This lesson reads those rules of Spain's personal income tax (IRPF) in the BOE and in the rulings of the DGT and turns them into an engine tested against hand-calculated cases. It is not tax advice.
- Module
- 7.4 · Regulation, ethics and taxation
- Duration
- 65 min
- Prerequisites
- 4.4.1 · Choosing a broker from Spain or the EU: regulation, assets, costs and API, 1.2.1 · Stocks: splits, dividends and corporate actions, 0.3.1 · Variables, types and operators; floating point and decimal for prices and ticks
- Author
- Jesús Cuesta
- Updated
Level 7 lessons
7.1 · Software engineering for trading
7.2 · Infrastructure and deployment
7.3 · MLOps for trading models
7.4 · Regulation, ethics and taxation
- 7.4.1 MiFID II: clients, suitability and algorithmic trading
- 7.4.2 ESMA and CFDs: leverage, close-out and negative balance
- 7.4.3 PRIIPs, the KID and US ETFs
- 7.4.4 MiCA after the transitional period: checking a provider in the registers
- 7.4.5 Market abuse: spoofing, layering and the bot that imitates them by accident
- 7.4.6 Trading taxes in Spain
- 7.4.7 Ethics: personal data, artificial intelligence and selling signals
7.5 · Quant career
7.6 · Final capstone
- 7.6.1 Capstone: common requirements, the panel's rubric and preparing the defense
- 7.6.2 Capstone C1: a multi-asset futures system, from forecast to paper account
- 7.6.3 Capstone C2: systematic options book, volatility surface and real-time Greeks
- 7.6.4 Capstone C3: stock and ETF portfolio with ML, HRP and Spanish taxation
- 7.6.5 Capstone C4: a research platform with LLMs and alternative data
- 7.6.6 Capstone C5: crypto market making or arbitrage with counterparty control
The savings base and its scale
The IRPF separates two tax bases. The general base covers employment, business activities and other income; the savings base covers, among others, income from movable capital (dividends and interest, Art. 25) and capital gains and losses arising from the transfer of assets (Art. 46)1Note 1 · Law 35/2006 on Personal Income Tax (IRPF), Arts. 25, 33.5, 35, 37, 46, 49, 66, 76, 80 and 101.4; Arts. 66 and 76 as worded by Law 7/2024 (effective from January 1, 2025). Consolidated text in the BOE (in Spanish; quotations are unofficial translations), last update published on October 7, 2026, accessed on October 23, 2026.. An individual who buys and sells shares, ETFs or cryptocurrencies, and who receives dividends, is taxed on them in the savings base.
The scale has two equal halves: the state half (Art. 66.1) and the regional half (Art. 76), both set in the state law. Since January 1, 2025, after Law 7/2024, the top bracket went from 28% to 30%, and the BOE consolidated text shows no later changes to those two articles: the 2025 scale is the 2026 scale1.
| Taxable savings base | State | Regional | Total | Cumulative tax at the top |
|---|---|---|---|---|
| Up to €6,000 | 9.5% | 9.5% | 19% | €1,140 |
| €6,000 to €50,000 | 10.5% | 10.5% | 21% | €10,380 |
| €50,000 to €200,000 | 11.5% | 11.5% | 23% | €44,880 |
| €200,000 to €300,000 | 13.5% | 13.5% | 27% | €71,880 |
| Over €300,000 | 15% | 15% | 30% | — |
The cumulative tax column is not a calculation of our own: it comes from the scale in Art. 66.2, which applies the full rates to the taxpayers resident abroad of Arts. 8.2 and 10.1, and it is useful for checking the code.
The scale applies to the part of the savings base that exceeds the personal and family allowance (Art. 66.1). Here we
assume the general base absorbs it entirely, which is the usual case with a salary; otherwise, the tax on savings goes
down. With amounts in Decimal (lesson 0.3.1), so that the cents add up:
from decimal import Decimal as D, ROUND_HALF_UP
# 2026 savings scale: (up to, state Art. 66, regional Art. 76)
TRAMOS = [
(D(6000), D("0.095"), D("0.095")),
(D(50000), D("0.105"), D("0.105")),
(D(200000), D("0.115"), D("0.115")),
(D(300000), D("0.135"), D("0.135")),
(None, D("0.15"), D("0.15")),
]
def c2(x):
"""Rounds to cents, as on the tax return."""
return D(x).quantize(D("0.01"), rounding=ROUND_HALF_UP)
def cuota_ahorro(base):
"""Gross tax on the savings base and its breakdown by bracket."""
base, desde, filas = D(base), D(0), []
for hasta, estatal, autonomico in TRAMOS:
if base <= desde:
break
tope = base if hasta is None else min(base, hasta)
tipo = estatal + autonomico
filas.append((desde, tope, tipo, c2((tope - desde) * tipo)))
if hasta is None:
break
desde = hasta
return sum((f[3] for f in filas), D(0)), filas
total, filas = cuota_ahorro(75000)
for desde, hasta, tipo, cuota in filas:
print(f"{desde:>7,.0f} to {hasta:>7,.0f} {tipo:.0%}"
f" {cuota:>9,.2f}")
print(f"{'Gross tax':>28} {total:>9,.2f}") 0 to 6,000 19% 1,140.00
6,000 to 50,000 21% 9,240.00
50,000 to 75,000 23% 5,750.00
Gross tax 16,130.00The tax on €75,000 is not 23% of 75,000, but each bracket at its rate. The difference between the rate on the last euro and the rate on the whole base matters when estimating what a strategy returns after tax:
print(" base marginal average")
for base in (6000, 20000, 50000, 120000, 300000, 1000000):
cuota, filas = cuota_ahorro(base)
print(f"{base:>9,} {filas[-1][2]:>8.0%} {cuota / base:>7.2%}") base marginal average
6,000 19% 19.00%
20,000 21% 20.40%
50,000 21% 20.76%
120,000 23% 22.07%
300,000 27% 23.96%
1,000,000 30% 28.19%The figure at the top draws both curves. To decide whether to realize a gain this year or the next, the marginal rate is what counts; to estimate a year's total tax, the average rate. With a base of €300,000, the average rate is 23.96%, and every additional euro is already taxed at 30%.
Acquisition value, FIFO and platforms
A capital gain or loss is the difference between the transfer value and the acquisition value, and both include the expenses and taxes inherent to the transaction: the purchase commission adds to the cost and the sale commission is subtracted from the proceeds (Art. 35)1. The practical problem is which shares are sold when they were bought in several purchases at different prices. For homogeneous securities, Art. 37.2 settles it: “the securities transferred by the taxpayer shall be deemed to be those acquired first”. It is FIFO, and it is mandatory: you cannot pick the most expensive lot to declare less.
For cryptocurrencies the law says nothing, but the DGT applies the same criterion. In ruling V1604-18 it concludes that in partial sales of bitcoin “the ‘bitcoin’ transferred must be considered to be those acquired first”, and that buying and selling them on different exchanges “is not a circumstance that alters homogeneity”. Rulings V0975-22 and V1550-25 repeat it; the latter, from September 2025, adds that the criterion is limited to cryptocurrencies, not other crypto-assets, whose homogeneity would require its own analysis2Note 2 · DGT, binding rulings V1604-18 (June 11, 2018), V0975-22 (May 4, 2022) and V1550-25 (September 3, 2025), in Spanish, read on October 23, 2026..
The lesson's trade log is synthetic and built so that every rule has at least one case that tests it. The assets are
fictitious except BTC and ETH, and the amounts are euros already net of commissions (in a dollar account, each purchase
and each sale is converted at the exchange rate of its date). The trade types in the log keep their Spanish keys:
compra (purchase), venta (sale), permuta (swap) and dividendo (dividend):
from dataclasses import dataclass, field
from datetime import date
@dataclass
class Op:
fecha: date
tipo: str # compra, venta, permuta or dividendo
activo: str
cantidad: D
importe: D # euros net of commissions
sitio: str # broker or exchange
clase: str # "valor" (listed security) or "cripto"
datos: dict = field(default_factory=dict)
def op(f, tipo, activo, cant, imp, sitio="B1", clase="valor", **k):
return Op(date.fromisoformat(f), tipo, activo, D(cant), D(imp),
sitio, clase, k)
def registro_sintetico():
"""Fictitious trades with the cases that must come out right."""
return [
op("2019-05-06", "compra", "ETFX", 1000, "40005"),
op("2025-01-15", "compra", "GAMMA", 300, "6005"),
op("2025-02-10", "compra", "BTC", "0.1", "9009", "EX1",
"cripto"),
op("2025-03-10", "compra", "ACME", 100, "5005"),
op("2025-06-02", "compra", "BETA", 200, "8005"),
op("2025-09-15", "compra", "ACME", 100, "6005"),
op("2025-11-03", "compra", "DELTA", 100, "8005"),
op("2025-12-01", "compra", "BTC", "0.1", "8008", "EX2",
"cripto"),
op("2026-02-20", "venta", "ACME", 150, "13495"),
op("2026-03-02", "venta", "DELTA", 100, "5995"),
op("2026-03-15", "venta", "BTC", "0.1", "7492.5", "EX2",
"cripto"),
op("2026-03-20", "compra", "DELTA", 100, "5805"),
op("2026-04-01", "compra", "EPSI", 400, "4005"),
op("2026-04-15", "dividendo", "ACME", 0, "500",
retencion_es=D(95)),
op("2026-05-11", "venta", "BETA", 200, "5995"),
op("2026-05-20", "venta", "EPSI", 200, "1795"),
op("2026-06-10", "venta", "ETFX", 1000, "98995"),
op("2026-06-25", "compra", "BETA", 200, "6205"),
op("2026-06-30", "dividendo", "USAA", 0, "800", "B2",
retencion_ext=D(120), tipo_convenio=D("0.15")),
op("2026-07-15", "venta", "DELTA", 100, "6595"),
op("2026-08-10", "permuta", "BTC", "0.05", "4600", "EX1",
"cripto", recibe="ETH", cantidad_recibida=D("1.5"),
valor_recibido=D(4575)),
op("2026-09-01", "venta", "GAMMA", 300, "4495"),
op("2026-09-30", "dividendo", "USBB", 0, "400", "B2",
retencion_ext=D(120), tipo_convenio=D("0.15")),
op("2026-10-20", "compra", "GAMMA", 100, "1405"),
op("2026-11-05", "venta", "ETH", "0.5", "1648.35", "EX1",
"cripto"),
]
REG = registro_sintetico()
print(len(REG), "trades;",
sum(o.tipo == "venta" for o in REG), "sales")25 trades; 9 salesThe engine walks through the log by date and keeps a queue of lots per asset. Each sale consumes the oldest lots; each lot stores, besides its cost, the deferred loss it carries under the two-month rule, which is explained further down:
from collections import defaultdict, deque
from dateutil.relativedelta import relativedelta
DOS_MESES = relativedelta(months=2)
@dataclass
class Lote:
origen: int # id of the purchase that created it
cantidad: D
coste: D # acquisition value of what is left
diferida: D = D(0) # deferred loss it carries (Art. 33.5)
class Motor:
"""Capital gains and losses for the tax year with FIFO and repurchases."""
def __init__(self, registro, ejercicio, por_sitio=False):
self.reg = sorted(registro, key=lambda o: o.fecha)
self.ejercicio, self.por_sitio = ejercicio, por_sitio
self.lotes = defaultdict(deque)
self.libre = {id(o): o.cantidad for o in self.reg
if o.tipo == "compra"} # not yet a repurchase
self.por_llegar = defaultdict(D) # deferred loss of future purchases
self.gp, self.rcm, self.diferidas = [], [], []
def clave(self, o, activo=None):
activo = activo or o.activo
return (activo, o.sitio) if self.por_sitio else activo
def comprar(self, o, activo, cantidad, coste):
lote = Lote(id(o), cantidad, coste,
self.por_llegar.pop(id(o), D(0)))
self.lotes[self.clave(o, activo)].append(lote)
def sacar_fifo(self, o):
"""Consumes the oldest lots (cost and deferred loss)."""
cola, falta = self.lotes[self.clave(o)], o.cantidad
coste = diferida = D(0)
while falta > 0:
if not cola:
raise ValueError(f"{o.fecha} {o.activo}: no shares")
lote = cola[0]
u = min(falta, lote.cantidad)
parte = u / lote.cantidad
coste += lote.coste * parte
diferida += lote.diferida * parte
lote.coste -= lote.coste * parte
lote.diferida -= lote.diferida * parte
lote.cantidad -= u
falta -= u
if lote.cantidad == 0:
cola.popleft()
return coste, diferida
def compras(self, o, desde, hasta):
return [c for c in self.reg if c.tipo == "compra"
and c.activo == o.activo and desde <= c.fecha <= hasta
and c is not o]
def saldo(self, activo, antes_de):
s = D(0)
for c in self.reg:
if c.activo == activo and c.fecha < antes_de:
s += c.cantidad if c.tipo == "compra" else -c.cantidad
return s
def recompras(self, o):
"""Units of homogeneous purchases that count as a repurchase."""
d = o.fecha
antes = [c for c in self.compras(o, d - DOS_MESES, d)
if c.fecha < d]
despues = [c for c in self.compras(o, d, d + DOS_MESES)
if c.fecha > d]
quedan = sum(l.cantidad for l in self.lotes[self.clave(o)])
inicio = self.saldo(o.activo, o.fecha - DOS_MESES)
if len(antes) == 1 and inicio == 0:
antes = [] # single purchase and no shares: not a repurchase
usadas, falta, tope = [], o.cantidad, quedan
previas = {id(c) for c in antes}
for c in antes + despues:
u = min(self.libre[id(c)], falta)
if id(c) in previas:
u = min(u, tope)
tope -= u
if u > 0:
usadas.append((c, u))
self.libre[id(c)] -= u
falta -= u
return usadas
def vender(self, o, valor, nota):
coste, liberada = self.sacar_fifo(o)
resultado = valor - coste
if liberada and self.compras(o, o.fecha - DOS_MESES,
o.fecha + DOS_MESES):
raise NotImplementedError(
f"{o.activo} {o.fecha}: non-final sale of shares"
" with a deferred loss; review by hand")
if resultado < 0 and o.clase == "valor":
usadas = self.recompras(o)
n = sum((u for _, u in usadas), D(0))
diferida = resultado * n / o.cantidad
for c, u in usadas:
self.asignar(c, resultado * u / o.cantidad)
if n:
self.diferidas.append(
(o.fecha, o.activo, c2(-diferida)))
nota += f" (defers {n} of {o.cantidad})"
resultado -= diferida
self.anotar(o, resultado, nota)
if liberada:
self.anotar(o, -liberada, "deferred loss now included")
def asignar(self, compra, perdida):
for cola in self.lotes.values():
for lote in cola:
if lote.origen == id(compra):
lote.diferida += -perdida
return
self.por_llegar[id(compra)] += -perdida
def anotar(self, o, importe, nota):
if o.fecha.year == self.ejercicio:
self.gp.append((o.fecha, o.activo, c2(importe), nota))
def procesar(self):
for o in self.reg:
if o.tipo == "compra":
self.comprar(o, o.activo, o.cantidad, o.importe)
elif o.tipo == "venta":
self.vender(o, o.importe, "sale")
elif o.tipo == "permuta":
d = o.datos
valor = max(o.importe, d["valor_recibido"]) # 37.1.h
self.vender(o, valor, f"swap for {d['recibe']}")
self.comprar(o, d["recibe"], d["cantidad_recibida"],
valor)
elif o.tipo == "dividendo":
if o.fecha.year == self.ejercicio:
self.rcm.append(o)
return self
m = Motor(REG, 2026).procesar()
for fecha, activo, importe, nota in m.gp:
print(f"{fecha} {activo:<6}{importe:>10,.2f} {nota}")2026-02-20 ACME 5,487.50 sale
2026-03-02 DELTA 0.00 sale (defers 100 of 100)
2026-03-15 BTC -1,516.50 sale
2026-05-11 BETA 0.00 sale (defers 200 of 200)
2026-05-20 EPSI -207.50 sale
2026-06-10 ETFX 58,990.00 sale
2026-07-15 DELTA 790.00 sale
2026-07-15 DELTA -2,010.00 deferred loss now included
2026-08-10 BTC 596.00 swap for ETH
2026-09-01 GAMMA -1,006.67 sale (defers 100 of 300)
2026-11-05 ETH 115.02 saleThe ACME gains come from FIFO: the 150 shares sold are the 100 from March 2025 (€5,005) and half of the September lot (€3,002.50), so the gain is 13,495 − 8,007.50 = €5,487.50. The ETFX gain, €58,990, comes from an ETF bought in 2019: the holding period does not reduce the gain.
The mistake of keeping a FIFO per exchange, which is what someone does when adding up each platform's reports separately:
def resultado(motor, activo):
return sum(i for _, a, i, _ in motor.gp if a == activo)
bien = Motor(REG, 2026).procesar()
mal = Motor(REG, 2026, por_sitio=True).procesar()
print("BTC, FIFO over all holdings:", resultado(bien, "BTC"))
print("BTC, FIFO per exchange: ", resultado(mal, "BTC"))BTC, FIFO over all holdings: -920.50
BTC, FIFO per exchange: -420.00The March sale was made on the second exchange, but the units sold for tax purposes are the first ones bought, on the first exchange. Counting per exchange declares €500.50 less loss in 2026 and leaves a wrong cost in the portfolio for the following years.
Cryptocurrencies: every swap is a sale
Exchanging one crypto-asset for another is not neutral. The DGT, in ruling V0999-18, classifies the exchange between different virtual currencies as a swap (permuta) that generates a capital gain or loss in the savings base, valued under Art. 37.1 h): the difference between the acquisition value of what is delivered and the higher of the market value of what is delivered and of what is received. That same value becomes the acquisition value of what is received3Note 3 · DGT, binding ruling V0999-18 (April 18, 2018), in Spanish, read on October 23, 2026..
In the log, on August 10, 0.05 BTC (market value €4,600) are delivered in exchange for 1.5 ETH (€4,575). The FIFO cost of those 0.05 BTC is half of the December lot, €4,004, so the gain is 4,600 − 4,004 = €596 and the ETH enter the portfolio with a cost of €4,600. The later sale of 0.5 ETH for €1,648.35 gives 1,648.35 − 4,600/3 = €115.02. The swap never went through euros and is taxed just like a sale.
Two limits of the engine are worth stating. The first: the two-month rule in the next section speaks of “securities or
units admitted to trading”, and a cryptocurrency is not one; point e) of the same Art. 33.5 deals with the repurchase of
“assets” within one year. The rulings read for this lesson do not settle how it applies to cryptocurrencies, so the
engine applies no repurchase rule to the cripto class and leaves it as an explicit decision. The second: staking,
airdrops, lending or DeFi have their own treatments that the engine does not model.
The two-month rule
Art. 33.5 f) says that losses from transferring securities admitted to trading are not computed “when the taxpayer has acquired homogeneous securities within the two months before or after those transfers”, and that those losses “shall be included as the securities or units that remain in the taxpayer's assets are transferred”. For unlisted securities, point g) extends the period to one year1. The purpose, in the DGT's words, is not to allow the loss “as long as the taxpayer's assets remain constant”.
Mermaid source
flowchart TB
V[Sale at a loss] --> C{Listed security?}
C -- no --> G[Unlisted: 1 year]
C -- yes --> R{Purchase ±2 months?}
R -- no --> I[Included]
R -- yes --> D[Deferred]
D --> T[Final sale]
T --> IThe detail lies in the DGT's doctrine, which ruling V5316-26 (July 2026) summarizes as a procedure4Note 4 · DGT, binding rulings V5316-26 (July 28, 2026), V0754-26 (April 6, 2026) and V0089-26 (January 20, 2026), in Spanish, read on October 23, 2026.:
- Purchases after the sale, within the two months: they prevent computing the loss on the repurchased units.
- Purchases before, within the two months: they only count as a repurchase up to the balance of shares left after the sale. And they do not count if there was a single purchase in that period and no shares were held when it began.
- The same purchase cannot block two losses: purchases already used as a repurchase do not count again.
- The deferred loss is included when the repurchased shares are sold in a final sale, with no repurchase within the two months. Ruling V0754-26 applies it by units: it includes the loss on 185 of the 207 shares of a sale, and V0089-26 does not allow a loss to be computed because more shares than those sold had been bought in the previous two months.
The engine codes those four rules in recompras and vender, and the log has one case for each: BETA (full later
repurchase), GAMMA (partial later repurchase, 100 of 300), EPSI (a single prior purchase with no shares at the start of
the period, the same pattern as V5316-26) and DELTA (deferred loss released by selling in July with no repurchase). The
result:
print("Deferred losses in 2026:")
for fecha, activo, importe in m.diferidas:
print(f" {fecha} {activo:<6}{importe:>9,.2f}")
print("Still held with a deferred loss:")
for activo, cola in m.lotes.items():
for lote in cola:
if lote.diferida:
print(f" {activo:<6}{lote.cantidad:>5} shares"
f"{c2(lote.diferida):>9,.2f}")Deferred losses in 2026:
2026-03-02 DELTA 2,010.00
2026-05-11 BETA 2,010.00
2026-09-01 GAMMA 503.33
Still held with a deferred loss:
GAMMA 100 shares 503.33
BETA 200 shares 2,010.00Of the three losses deferred in 2026, DELTA's has already been included in the same year: the line “deferred loss now included” in the previous table. Those of BETA and GAMMA remain inside the repurchased lots and will be included in the year in which they are finally sold. An engine without this rule would have declared €2,513.33 more in losses in 2026 (BETA's €2,010 and GAMMA's €503.33).
Dividends, withholdings and tax treaties
Dividends are income from movable capital (Art. 25.1) and go to the savings base at their gross amount, before withholdings. A Spanish payer withholds 19% on account (Art. 101.4), which is deducted at the end as a payment on account1. A foreign dividend usually arrives with a withholding from the source country, and two pieces come into play:
- The tax treaty. The one between Spain and the US, as amended by the 2013 Protocol (published in the BOE on October 23, 2019), allows the US to tax a dividend received by a resident of Spain at up to 15% of the gross amount; with lower rates only for companies with significant holdings and pension funds (Art. 10)5Note 5 · Convention between the Kingdom of Spain and the United States of America for the avoidance of double taxation, of February 22, 1990, Arts. 10 and 24, consolidated text with the Protocol of January 14, 2013 in the BOE (Protocol: BOE-A-2019-15166), in Spanish, accessed on October 23, 2026.. To apply that rate, the broker needs the IRS form W-8BEN, which certifies the beneficial owner's residence; without it, the general withholding on those payments is 30%. The form is generally valid until the last day of the third calendar year after the year it was signed6Note 6 · IRS, Instructions for Form W-8BEN (October 2021 revision) and NRA withholding, accessed on October 23, 2026..
- The international double taxation deduction (Art. 80). The lower of two amounts is deducted: what was actually paid abroad and the result of applying the average effective savings rate to the part of the base obtained abroad. The AEAT specifies that this average rate is that of the undivided savings base and is expressed with two decimals7Note 7 · AEAT, 2025 income tax practical guide (Manual práctico de Renta 2025), chapter 18, international double taxation deduction (in Spanish), accessed on October 23, 2026.. And the DGT understands as the amount paid the one the other State may levy under the treaty (V5381-26)8Note 8 · DGT, binding ruling V5381-26 (July 28, 2026), in Spanish, read on October 23, 2026. Its case involves a different treaty (Panama); the criterion cited is the general one on Art. 80.: whatever was over-withheld is not deducted in Spain and can only be recovered by claiming it from the source country.
The log has a Spanish dividend (€500, €95 withheld) and two US dividends of €800 and €400, with €120 withheld on each: the first at 15%, with a W-8BEN; the second at 30%, without one.
def rendimientos(motor):
bruto = sum((o.importe for o in motor.rcm), D(0))
ext = [o for o in motor.rcm if "retencion_ext" in o.datos]
base_ext = sum((o.importe for o in ext), D(0))
retenido = sum((o.datos["retencion_ext"] for o in ext), D(0))
admisible = sum((min(o.datos["retencion_ext"],
c2(o.importe * o.datos["tipo_convenio"]))
for o in ext), D(0))
ret_es = sum((o.datos.get("retencion_es", D(0))
for o in motor.rcm), D(0))
return dict(bruto=bruto, base_ext=base_ext, retenido_ext=retenido,
admisible=admisible, retencion_es=ret_es)
for k, v in rendimientos(m).items():
print(f"{k:<13}{v:>8,.2f}")bruto 1,700.00
base_ext 1,200.00
retenido_ext 240.00
admisible 180.00
retencion_es 95.00The keys are the gross dividends (bruto), the foreign-source base (base_ext), the tax withheld abroad
(retenido_ext), the part of it that is deductible (admisible) and the Spanish withholding (retencion_es). Of the
€240 withheld in the US, only €180 are deductible: €120 from the first dividend and €60 (15% of 400) from the second.
Since the treaty rate (15%) is lower than the lowest average rate of the savings base (19%), with US dividends the
binding limit is almost always the treaty's, not the average rate's.
Offsetting losses and the final assessment
Art. 49 builds the savings base from two balances calculated separately: income from movable capital (RCM) and capital gains and losses (GP). If one is negative, it offsets the positive balance of the other up to 25% of that positive balance, and whatever is left is offset “in the following four years in the same order”1. The AEAT guide details the order with balances from previous years: first each pending item against its own balance for the year, with no limit; then the rest, crosswise, with a 25% that is shared with the offsetting of the year itself9Note 9 · AEAT, 2025 income tax practical guide, chapter 12: inclusion and offsetting in the savings base and worked example (in Spanish), accessed on October 23, 2026..
Mermaid source
flowchart TB
A[RCM balance] --> X{25% cross-offset}
B[GP balance] --> X
X --> P[Own carryforwards]
P --> Q[Carryforwards, crosswise]
Q --> BA[Savings base]
BA --> E[Scale 19 to 30%]The best test for a function like this is a case solved by the tax administration itself. The 2025 income tax guide includes one: income from movable capital of −€800, gains of €5,600, losses of €1,600 and pending items from 2021 and 2022. Its solution gives a savings base of €200:
def compensar(rcm, gp, pend_rcm, pend_gp, ejercicio):
"""Art. 49 of the IRPF Law, in the order of the AEAT guide."""
rcm, gp = D(rcm), D(gp)
pend_rcm, pend_gp = dict(pend_rcm), dict(pend_gp)
vivas = range(ejercicio - 4, ejercicio)
caducadas = {k: v for p in (pend_rcm, pend_gp)
for k, v in p.items() if k not in vivas}
pend_rcm = {k: D(v) for k, v in pend_rcm.items() if k in vivas}
pend_gp = {k: D(v) for k, v in pend_gp.items() if k in vivas}
lim_gp, lim_rcm = D("0.25") * max(gp, 0), D("0.25") * max(rcm, 0)
# Phase 1: balances of the year, crosswise with the 25%
if rcm < 0 < gp:
x = min(-rcm, lim_gp)
rcm, gp, lim_gp = rcm + x, gp - x, lim_gp - x
if gp < 0 < rcm:
x = min(-gp, lim_rcm)
gp, rcm, lim_rcm = gp + x, rcm - x, lim_rcm - x
nuevas_rcm, nuevas_gp = -min(rcm, 0), -min(gp, 0)
rcm, gp = max(rcm, 0), max(gp, 0)
def gastar(pend, saldo, tope=None):
for año in sorted(pend):
x = min(pend[año], saldo,
saldo if tope is None else tope)
pend[año] -= x
saldo -= x
if tope is not None:
tope -= x
return saldo, tope
# Phase 2, step 1: each pending item against its own balance
rcm, _ = gastar(pend_rcm, rcm)
gp, _ = gastar(pend_gp, gp)
# Phase 2, step 2: the rest, crosswise and within the remaining 25%
gp, lim_gp = gastar(pend_rcm, gp, lim_gp)
rcm, lim_rcm = gastar(pend_gp, rcm, lim_rcm)
if nuevas_rcm:
pend_rcm[ejercicio] = nuevas_rcm
if nuevas_gp:
pend_gp[ejercicio] = nuevas_gp
limpia = lambda p: {k: c2(v) for k, v in p.items() if v}
return dict(base=c2(rcm + gp), rcm=c2(rcm), gp=c2(gp),
pend_rcm=limpia(pend_rcm), pend_gp=limpia(pend_gp),
caducadas=limpia(caducadas))
# Worked example from the AEAT guide (2025 income tax, chapter 12)
aeat = compensar(rcm=-800, gp=5600 - 1600,
pend_rcm={2021: 500}, pend_gp={2021: 700, 2022: 2100},
ejercicio=2025)
print("Savings base:", aeat["base"])
print("Negative RCM from 2021 still pending:", aeat["pend_rcm"][2021])Savings base: 200.00
Negative RCM from 2021 still pending: 300.00It matches: €800 of negative income for the year offset within the limit of €1,000 (25% of 4,000), €2,800 of pending losses against the gains and only €200 of the €500 of negative income from 2021, because the 25% was almost used up. The remaining €300 carry over to the next year.
With all the pieces, the assessment of the savings base of the synthetic log, with pending losses from 2021 (€500) and 2022 (€1,000):
def liquidar(motor, pend_rcm, pend_gp):
r = rendimientos(motor)
gp = sum((i for _, _, i, _ in motor.gp), D(0))
comp = compensar(r["bruto"], gp, pend_rcm, pend_gp,
motor.ejercicio)
base = comp["base"]
cuota, filas = cuota_ahorro(base)
tme = (cuota / base * 100).quantize(D("0.01")) if base else D(0)
ddi = min(r["admisible"], c2(r["base_ext"] * tme / 100))
liquida = cuota - ddi
return dict(saldo_gp=gp, saldo_rcm=r["bruto"], base=base,
filas=filas, cuota=cuota, tme=tme, ddi=ddi,
liquida=liquida, retenciones=r["retencion_es"],
diferencial=liquida - r["retencion_es"],
pendientes=comp["pend_gp"], caducadas=comp["caducadas"])
liq = liquidar(m, pend_rcm={}, pend_gp={2021: 500, 2022: 1000})
for k in ("saldo_gp", "saldo_rcm", "base"):
print(f"{k:<12}{liq[k]:>11,.2f}")
for desde, hasta, tipo, cuota in liq["filas"]:
print(f" {desde:>7,.0f} to {hasta:>9,.2f}"
f" {tipo:.0%}{cuota:>10,.2f}")
print(f"{'cuota':<12}{liq['cuota']:>11,.2f}")
print(f"{'TME':<12}{liq['tme']:>10} %")
for k in ("ddi", "liquida", "retenciones", "diferencial"):
print(f"{k:<12}{liq[k]:>11,.2f}")
for k in ("caducadas", "pendientes"):
años = ", ".join(f"{a}: {v:,.2f}" for a, v in liq[k].items())
print(f"{k:<12}{años or 'none'}")saldo_gp 61,237.85
saldo_rcm 1,700.00
base 61,937.85
0 to 6,000.00 19% 1,140.00
6,000 to 50,000.00 21% 9,240.00
50,000 to 61,937.85 23% 2,745.71
cuota 13,125.71
TME 21.19 %
ddi 180.00
liquida 12,945.71
retenciones 95.00
diferencial 12,850.71
caducadas 2021: 500.00
pendientes noneThe labels are the keys of the result: GP and RCM balances, base, gross tax (cuota), average effective rate (TME),
double taxation deduction (ddi), net tax (liquida), Spanish withholdings (retenciones), the amount payable
(diferencial) and the losses that expire (caducadas) or remain pending (pendientes). The 2021 losses expire: their
period ended in 2025. The 2022 losses are offset in full against the 2026 gains, the last year in which they could be
used. The savings base, €61,937.85, spans three brackets; the average rate is 21.19%, and the double taxation
deduction, €180 (the treaty limit, lower than 21.19% of 1,200).
A tax engine without tests is an opinion. The ones that follow pin down every figure calculated by hand in the previous
sections, in pytest style (bare assert statements inside test_ functions), and run in a loop so as not to depend on
a separate file:
def gp_de(motor, activo):
return [i for _, a, i, _ in motor.gp if a == activo]
def test_escala_coincide_con_el_boe():
# cumulative tax from the table in Art. 66.2 (state + regional)
for base, cuota in [(6000, 1140), (50000, 10380),
(200000, 44880), (300000, 71880)]:
assert cuota_ahorro(base)[0] == cuota
def test_fifo_ignora_el_exchange():
assert gp_de(m, "BTC")[0] == D("-1516.50") # 7,492.5 - 9,009
assert gp_de(mal, "BTC")[0] == D("-515.50") # the mistake
def test_permuta_usa_el_mayor_valor():
assert gp_de(m, "BTC")[1] == D("596.00") # 4,600 - 8,008/2
assert gp_de(m, "ETH") == [D("115.02")] # 1,648.35 - 4,600/3
def test_recompra_en_dos_meses_difiere_la_perdida():
assert gp_de(m, "BETA") == [D("0.00")] # 2,010 deferred
assert gp_de(m, "GAMMA") == [D("-1006.67")] # 1/3 deferred
def test_unica_compra_previa_sin_titulos_no_es_recompra():
assert gp_de(m, "EPSI") == [D("-207.50")]
def test_venta_definitiva_libera_la_perdida():
assert gp_de(m, "DELTA") == [D("0.00"), D("790.00"),
D("-2010.00")]
def test_compensacion_como_el_manual_de_la_aeat():
assert aeat["base"] == 200
assert aeat["pend_rcm"] == {2021: 300}
def test_limite_del_25_por_ciento():
r = compensar(rcm=1000, gp=-600, pend_rcm={}, pend_gp={},
ejercicio=2026)
assert r["base"] == 750 and r["pend_gp"] == {2026: 350}
def test_dividendos_y_convenio():
assert rendimientos(m)["admisible"] == 180 # 120 + 15% of 400
assert liq["ddi"] == 180
def test_liquidacion_completa():
assert liq["base"] == D("61937.85")
# 1,140 + 9,240 + 23% of 11,937.85
assert liq["cuota"] == D("13125.71")
assert liq["diferencial"] == D("12850.71")
pruebas = [f for n, f in list(globals().items())
if n.startswith("test_")]
for prueba in pruebas:
prueba()
print(f"{len(pruebas)} tests passed")10 tests passedForms 720 and 721: reporting is not taxation
Lesson 4.4.1 explains when Forms 720 (Modelo 720, assets and rights located abroad) and 721 (Modelo 721, virtual currencies held abroad) must be filed: a threshold of €50,000 per block on December 31, a new return when the increase exceeds €20,000 and a filing window from January 1 to March 31 (Royal Decree 1065/2007, Arts. 42 ter and 42 quater; Orders HAP/72/2013 and HFP/886/2023; Form 721 was filed for the first time for tax year 2023)10Note 10 · Royal Decree 1065/2007, Arts. 42 ter and 42 quater, consolidated text in the BOE; Order HAP/72/2013 (BOE-A-2013-954); Order HFP/886/2023 (BOE-A-2023-17429); Law 5/2022, preamble and fourth and fifth final provisions (BOE-A-2022-3712). All in Spanish, accessed on October 23, 2026.. What matters here is their relationship with the tax:
- They assess no tax. They are information returns. The gains, losses and dividends from those assets are declared in the IRPF like any others, with the rules of this lesson.
- The 2012 regime was corrected in 2022. The judgment of the Court of Justice of the EU of January 27, 2022 (case C-788/19) declared aspects of the regime associated with Form 720 contrary to EU law; Law 5/2022 consequently amended the eighteenth additional provision of the General Tax Law (Ley General Tributaria) and Art. 39 of the IRPF Law10.
- What remains. Under the current Art. 39, assets whose ownership does not match the declared income or wealth are unjustified capital gains that are included in the general base of the period in which they are discovered, unless the taxpayer proves they held them since before the limitation period. A complete trade history, like the log that feeds the engine, is also the proof of origin.
What the lesson's engine does not do, so that nobody mistakes it for a tax return program: it does not compute the general base or the personal allowance, it does not model derivatives, securities lending, spin-offs, capital increases, subscription rights or investment funds (which have their own transfer regime), it does not convert currencies and it knows nothing about the foral regimes or non-residents.
Exercises
- Repeat the assessment assuming the broker also had the W-8BEN for the USBB dividend (15% withholding). Does what you pay in Spain change? How much does not having filed it cost?
Solution
con_w8 = [o if o.activo != "USBB" else
op("2026-09-30", "dividendo", "USBB", 0, "400", "B2",
retencion_ext=D(60), tipo_convenio=D("0.15"))
for o in REG]
m2 = Motor(con_w8, 2026).procesar()
liq2 = liquidar(m2, pend_rcm={}, pend_gp={2021: 500, 2022: 1000})
filas = [("Withheld in the US", rendimientos(m)["retenido_ext"],
rendimientos(m2)["retenido_ext"]),
("Deduction in Spain", liq["ddi"], liq2["ddi"]),
("Amount payable", liq["diferencial"], liq2["diferencial"])]
print(f"{'':<20}{'no W-8BEN':>12}{'with W-8BEN':>12}")
for nombre, sin, con in filas:
print(f"{nombre:<20}{sin:>12,.2f}{con:>12,.2f}") no W-8BEN with W-8BEN
Withheld in the US 240.00 180.00
Deduction in Spain 180.00 180.00
Amount payable 12,850.71 12,850.71What you pay in Spain does not change: in both cases €180 are deducted. What changes is the amount withheld in the US: €60 more without the form, which are not recovered on the Spanish return and can only be claimed from the US tax administration. It is 15% of every dividend received without a valid W-8BEN.
- Write a pre-trade check: before selling a listed security at a loss, list the purchases of the previous two months and the date until which it is best not to buy it back. Test it with GAMMA on November 10, 2026.
Solution
def aviso_recompra(registro, activo, fecha):
f = date.fromisoformat(fecha)
previas = [o for o in registro if o.tipo == "compra"
and o.activo == activo and f - DOS_MESES <= o.fecha < f]
return previas, f + DOS_MESES
previas, hasta = aviso_recompra(REG, "GAMMA", "2026-11-10")
for o in previas:
print(f"Prior purchase: {o.fecha} {o.cantidad} shares")
print("No repurchases until", hasta, "to avoid deferring the loss")Prior purchase: 2026-10-20 100 shares
No repurchases until 2027-01-10 to avoid deferring the lossThe October 20 purchase falls within the previous two months, so a sale at a loss on November 10 would be deferred in
the part that purchase covers (up to the balance left after the sale). It is a warning, not the full calculation: the
single-purchase exception and the purchases already used as a repurchase are decided by Motor.recompras. In a real
system, the check goes into the pre-trade risk control (lesson 4.5.1), next to the limits.
- You sell some listed shares at a loss on December 15, 2026 and buy them back on January 20, 2027. On which tax return does the loss appear, and what data does the engine need to know it?
Solution
It does not appear on the 2026 return: there is a homogeneous purchase within the following two months, so the loss is deferred and will be included in the year in which the shares repurchased in January are finally sold. To know it, the engine has to read trades up to February 15, 2027 when computing 2026. A year-end close that only looks at the calendar year would declare a loss that cannot be computed.
Footnotes
-
Law 35/2006 on Personal Income Tax (IRPF), Arts. 25, 33.5, 35, 37, 46, 49, 66, 76, 80 and 101.4; Arts. 66 and 76 as worded by Law 7/2024 (effective from January 1, 2025). Consolidated text in the BOE (in Spanish; quotations are unofficial translations), last update published on October 7, 2026, accessed on October 23, 2026. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
DGT, binding rulings V1604-18 (June 11, 2018), V0975-22 (May 4, 2022) and V1550-25 (September 3, 2025), in Spanish, read on October 23, 2026. ↩
-
DGT, binding ruling V0999-18 (April 18, 2018), in Spanish, read on October 23, 2026. ↩
-
DGT, binding rulings V5316-26 (July 28, 2026), V0754-26 (April 6, 2026) and V0089-26 (January 20, 2026), in Spanish, read on October 23, 2026. ↩
-
Convention between the Kingdom of Spain and the United States of America for the avoidance of double taxation, of February 22, 1990, Arts. 10 and 24, consolidated text with the Protocol of January 14, 2013 in the BOE (Protocol: BOE-A-2019-15166), in Spanish, accessed on October 23, 2026. ↩
-
IRS, Instructions for Form W-8BEN (October 2021 revision) and NRA withholding, accessed on October 23, 2026. ↩
-
AEAT, 2025 income tax practical guide (Manual práctico de Renta 2025), chapter 18, international double taxation deduction (in Spanish), accessed on October 23, 2026. ↩
-
DGT, binding ruling V5381-26 (July 28, 2026), in Spanish, read on October 23, 2026. Its case involves a different treaty (Panama); the criterion cited is the general one on Art. 80. ↩
-
AEAT, 2025 income tax practical guide, chapter 12: inclusion and offsetting in the savings base and worked example (in Spanish), accessed on October 23, 2026. ↩
-
Royal Decree 1065/2007, Arts. 42 ter and 42 quater, consolidated text in the BOE; Order HAP/72/2013 (BOE-A-2013-954); Order HFP/886/2023 (BOE-A-2023-17429); Law 5/2022, preamble and fourth and fifth final provisions (BOE-A-2022-3712). All in Spanish, accessed on October 23, 2026. ↩ ↩2
Each answer is folded under its question
Self-assessment
With a taxable savings income base of €75,000 in 2026, what is the gross tax liability (state plus regional)?
- A€14,250: 19% of the whole base
- B€16,130: 1,140 + 9,240 + 5,750, each bracket at its rate
- C€17,250: 23% of the whole base, because the last euro is taxed at 23%
- D€22,500: 30%, the top rate
Show answer
Answer B. €16,130: 1,140 + 9,240 + 5,750, each bracket at its rate
The scale is progressive by brackets: €6,000 at 19%, €44,000 at 21% and the remaining €25,000 at 23%. The marginal rate is 23%, but the average rate is 21.51%. Applying the marginal rate to the whole base is the most common mistake when estimating a strategy's tax.
On May 11, 2026 you sell at a loss listed shares you had held since 2025, and on June 25 you buy the same shares. What happens to the loss?
- AIt is included in 2026 because the sale came before the purchase
- BIt is lost forever
- CIt is included in 2026 if the repurchase is through another broker
- DIt is not computed in 2026: it will be included when you finally sell the repurchased shares
Show answer
Answer D. It is not computed in 2026: it will be included when you finally sell the repurchased shares
Art. 33.5 f) prevents computing the loss if homogeneous securities are acquired within the two months before or after the sale, and says it will be included as the securities that remain are transferred. That is what the engine does with BETA: €2,010 deferred that travel with the repurchased lot.
You bought 0.1 BTC on one exchange in February 2025 and another 0.1 BTC on another exchange in December. In March 2026 you sell 0.1 BTC on the second one. Which acquisition value do you use?
- AThe February 2025 one: FIFO over all BTC, without distinguishing the platform
- BThe December one, because the sale was made on that exchange
- CThe average of the two
- DWhichever you choose, as long as you are consistent
Show answer
Answer A. The February 2025 one: FIFO over all BTC, without distinguishing the platform
The DGT applies the criterion of Art. 37.2 to homogeneous cryptocurrencies: the units acquired first are the ones sold, and the platform does not break homogeneity (V1604-18, V0975-22, V1550-25). In the lesson's trade log, FIFO per exchange gives a loss of €515.50 instead of €1,516.50.
A €400 dividend from a US company arrives with 30% withheld because the broker did not have the W-8BEN form. How much can you deduct in Spain for double taxation?
- AThe €120 withheld
- BNothing, because you did not file the W-8BEN
- CAt most €60: the 15% the tax treaty allows; the excess is claimed from the US tax administration
- D19% of the dividend
Show answer
Answer C. At most €60: the 15% the tax treaty allows; the excess is claimed from the US tax administration
Art. 80 of the IRPF Law deducts what was actually paid abroad, capped at the average rate, and the DGT understands as paid the amount the other State may levy under the treaty (V5381-26). The treaty with the US caps the tax on an individual's dividends at 15%.
In 2026 the balance of capital gains and losses is –€600 and the balance of dividends and interest is +€1,000. What happens?
- AThe 600 are offset: the savings base is €400
- B€250 are offset (25% of 1,000); the remaining 350 are carried forward for up to four years
- CNothing is offset: gains and investment income never mix
- DThey are offset against the general base
Show answer
Answer B. €250 are offset (25% of 1,000); the remaining 350 are carried forward for up to four years
Art. 49 allows the negative balance of one block to be offset against the positive balance of the other, up to 25% of that positive balance. Whatever is left is offset in the following four years in the same order. It is the test
test_limite_del_25_por_ciento.
Key points
- Spain's 2026 savings income base is taxed by brackets at 19, 21, 23, 27 and 30% (half state, half regional, Arts. 66 and 76; common-regime territory). With a base of €300,000, the average rate is 23.96%.
- FIFO is the legal rule for homogeneous securities (Art. 37.2) and the one the DGT applies to cryptocurrencies, over all units and without separating by platform. A swap between cryptocurrencies is a transfer: it is valued at the higher of the two market values.
- A loss on listed securities is not computed if there are homogeneous purchases two months before or after; it is included when the repurchased shares are sold. An engine that does not model this brings forward losses that are not deductible.
- Dividends and interest on one side and capital gains and losses on the other: each negative balance offsets the other's positive balance only up to 25%, and what is pending expires after four years.
- The double taxation deduction is the lower of what was paid abroad (within what the treaty allows: 15% for US dividends) and the average rate applied to that income. Without a W-8BEN, the US withholds 30% and the difference is not recovered in Spain.
- Forms 720 and 721 (Modelo 720 and 721) report, they do not assess tax: they do not change the taxation of gains, but failing to file them leaves assets unaccounted for.
Resources
Documentation
- Law 35/2006 on Personal Income Tax (IRPF), consolidated text (BOE, in Spanish)
- AEAT (Spanish Tax Agency), 2025 income tax practical guide: inclusion and offsetting of income in the savings base (in Spanish)
- DGT, binding ruling V1550-25: FIFO for cryptocurrencies without separating by platform (in Spanish)
- DGT, binding ruling V5316-26: how the two-month rule is applied (in Spanish)
- Spain–US double taxation treaty, consolidated text with the 2013 Protocol (BOE, in Spanish)
- IRS: Instructions for Form W-8BEN