---
title: "0.4.1 POO: clases, herencia, composición, dataclasses"
description: "Órdenes, posiciones y carteras como clases de Python: dataclasses inmutables, orden de prioridad, herencia para tipos de orden y composición."
url: "https://pythonparatrading.com/curso-python-trading/nivel-0/clases-herencia-y-dataclasses/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-0/clases-herencia-y-dataclasses.png"
---

# 0.4.1 · Programación orientada a objetos: clases, herencia, composición y dataclasses

> Un sistema de trading maneja objetos con estado y reglas: órdenes que no deben cambiar, posiciones que sí cambian y carteras que agrupan posiciones. Esta lección los modela con clases y dataclasses, y explica cuándo heredar y cuándo componer.

- Curso: [Nivel 0: Python desde cero y mentalidad de datos](https://pythonparatrading.com/curso-python-trading/nivel-0/)
- Módulo: 0.4 · Python intermedio
- Duración: 45 min
- Requisitos: [0.3.5 · Funciones: argumentos, *args y **kwargs, lambdas y ámbito](https://pythonparatrading.com/curso-python-trading/nivel-0/funciones-argumentos-y-ambito/), [0.3.6 · Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15](https://pythonparatrading.com/curso-python-trading/nivel-0/excepciones-modulos-y-paquetes/)

## Al terminar esta lección

- Escribir una clase con `__init__`, métodos, `__repr__` y `__eq__`, y reconocer el código repetitivo que ahorra una dataclass.
- Modelar una orden inmutable con `@dataclass(frozen=True)`, validarla en `__post_init__` y ordenar órdenes por prioridad precio-tiempo.
- Construir una cartera por composición que calcule su valor y su P&L realizado y no realizado.
- Decidir entre herencia y composición con la regla «es un» frente a «tiene un».

## Por qué clases

Hasta ahora los datos de una orden cabían en un diccionario: `{"simbolo": "SINT", "lado": "compra", "cantidad": 100}`.
Funciona, pero no protege de nada. Un error de tecleo (`"cantdad"`) crea una clave nueva en silencio, nada impide una
cantidad negativa y el cálculo del importe vive en una función suelta que hay que acordarse de llamar.

Una **clase** junta en un mismo sitio los datos (atributos) y lo que se puede hacer con ellos (métodos). Cada objeto
creado a partir de la clase es una **instancia**. En un sistema de trading hay tres candidatas evidentes:

| Objeto | Qué guarda | ¿Cambia? | Qué hace |
|---|---|---|---|
| Orden | símbolo, lado, cantidad, precio | No: una vez enviada, se cancela o se sustituye | Calcula su importe, se valida |
| Posición | símbolo, cantidad, precio medio, P&L realizado | Sí: con cada ejecución | Aplica ejecuciones, calcula su valor |
| Cartera | efectivo y posiciones | Sí | Ejecuta órdenes, calcula valor y P&L |

La columna «¿Cambia?» decide buena parte del diseño. Las órdenes serán inmutables; las posiciones y la cartera, no.

## Una clase escrita a mano

La forma básica de una clase: `__init__` recibe los datos y los guarda en `self`, la propia instancia. Los métodos
son funciones que reciben `self` como primer argumento. Los que empiezan y terminan con doble guion bajo son
**métodos especiales**: Python los llama por su cuenta. `__repr__` decide cómo se muestra el objeto y `__eq__`, cuándo
dos objetos son iguales.

```python
class OrdenManual:
    def __init__(self, simbolo, lado, cantidad, precio):
        self.simbolo = simbolo
        self.lado = lado
        self.cantidad = cantidad
        self.precio = precio

    def importe(self):
        return self.cantidad * self.precio

    def __repr__(self):
        return (f"OrdenManual({self.simbolo!r}, {self.lado!r}, "
                f"{self.cantidad}, {self.precio})")

    def __eq__(self, otra):
        if not isinstance(otra, OrdenManual):
            return NotImplemented
        return ((self.simbolo, self.lado, self.cantidad, self.precio)
                == (otra.simbolo, otra.lado, otra.cantidad,
                    otra.precio))

a = OrdenManual("SINT", "compra", 100, 142.22)
b = OrdenManual("SINT", "compra", 100, 142.22)
print(a)
print(f"Importe: {a.importe():.2f}")
print(a == b, a is b)
```
```output
OrdenManual('SINT', 'compra', 100, 142.22)
Importe: 14222.00
True False
```

Sin `__repr__`, `print(a)` mostraría algo como `<OrdenManual object at 0x…>`, inútil en un registro de operaciones.
Sin `__eq__`, `a == b` compararía la identidad (si son el mismo objeto en memoria) y daría `False` para dos órdenes
idénticas. Con los dos métodos, `a == b` es `True` aunque `a is b` siga siendo `False`: son dos objetos con el mismo
contenido.

El importe se imprime con dos decimales por precaución. `100 * 142.22` da `14222.0` justo, pero `3 * 142.22` da
`426.65999999999997`: es el problema de coma flotante de la lección 0.3.1, y la razón de que en contabilidad de
precios se use `decimal`.

Hay mucho código repetido: cada campo aparece cuatro veces en `__init__`, otra en `__repr__` y dos en `__eq__`. Si
mañana se añade un campo y se olvida en `__eq__`, dos órdenes distintas serán «iguales».

## Dataclasses: el mismo resultado sin repetir

El decorador `@dataclass` (módulo `dataclasses`, en la biblioteca estándar) lee las anotaciones de la clase y escribe
`__init__`, `__repr__` y `__eq__` por ti. Las anotaciones (`simbolo: str`) se ven a fondo en la lección 0.4.2; aquí
basta con saber que cada línea `nombre: tipo` declara un campo.

Dos opciones encajan con una orden:

- `frozen=True`: la instancia no se puede modificar después de crearla. Una orden enviada no cambia; si se quiere
  otra cantidad, se crea otra orden.
- `slots=True`: los atributos se guardan en huecos fijos en lugar de en un diccionario. Ocupa menos memoria y hace
  imposible crear atributos por error.

La validación va en `__post_init__`, que la dataclass llama al final de su `__init__`. El importe es una
**propiedad** (`@property`): se calcula al leerla, sin paréntesis, y no se guarda.

```python
from dataclasses import dataclass, field, replace

LADOS = ("compra", "venta")

@dataclass(frozen=True, slots=True)
class Orden:
    simbolo: str
    lado: str
    cantidad: int
    precio: float

    def __post_init__(self):
        if self.lado not in LADOS:
            raise ValueError(f"lado no válido: {self.lado!r}")
        if self.cantidad <= 0:
            raise ValueError(f"cantidad no positiva: {self.cantidad}")

    @property
    def importe(self) -> float:
        return self.cantidad * self.precio

orden = Orden("SINT", "compra", 100, 142.22)
print(orden)
print(orden == Orden("SINT", "compra", 100, 142.22))
print(f"{orden.importe:.2f}")
```
```output
Orden(simbolo='SINT', lado='compra', cantidad=100, precio=142.22)
True
14222.00
```

Las garantías se comprueban en cuanto se intenta saltárselas. Modificar un campo de una orden congelada lanza
`FrozenInstanceError`:

```python raises
orden.cantidad = 200
```
```output
dataclasses.FrozenInstanceError: cannot assign to field 'cantidad'
```

Y una orden mal construida no llega a existir:

```python raises
Orden("SINT", "comprar", 100, 142.22)
```
```output
ValueError: lado no válido: 'comprar'
```

Para cambiar algo se crea una orden nueva con `replace`, que copia todos los campos menos los indicados y vuelve a
pasar por `__post_init__`. Como las órdenes congeladas tienen `__hash__`, también se pueden meter en un conjunto, lo
que sirve, por ejemplo, para descartar órdenes duplicadas que una señal haya generado dos veces:

```python
reducida = replace(orden, cantidad=50)
print(reducida)
print(orden.cantidad)

pendientes = {orden, Orden("SINT", "compra", 100, 142.22), reducida}
print(len(pendientes))
```
```output
Orden(simbolo='SINT', lado='compra', cantidad=50, precio=142.22)
100
2
```

La original sigue con 100 títulos y el conjunto tiene dos órdenes, no tres: las dos primeras son iguales.

## Orden y prioridad: precio-tiempo

Un libro de órdenes ejecuta primero la orden con mejor precio y, a igual precio, la que llegó antes. Es la prioridad
**precio-tiempo**. Con `order=True`, una dataclass compara sus campos como una tupla, en el orden en que se declaran,
así que basta con declarar primero el precio y después la llegada. Los campos que no deben contar en la comparación
llevan `field(compare=False)`:

```python
@dataclass(frozen=True, order=True)
class OrdenLibro:
    precio: float
    llegada: int
    id: str = field(compare=False)
    cantidad: int = field(compare=False)

ventas = [
    OrdenLibro(142.30, 3, "v3", 200),
    OrdenLibro(142.25, 5, "v5", 100),
    OrdenLibro(142.30, 1, "v1", 50),
    OrdenLibro(142.25, 2, "v2", 300),
]
for o in sorted(ventas):
    print(o.id, o.precio, o.llegada, o.cantidad)
```
```output
v2 142.25 2 300
v5 142.25 5 100
v1 142.3 1 50
v3 142.3 3 200
```

Para las ventas, el mejor precio es el más bajo, que es justo el orden natural. Para las compras, el mejor precio es
el más alto y el truco no sirve tal cual: el ejercicio 2 lo resuelve con una clave de ordenación.

## Posición y cartera: composición

Una posición sí cambia: cada ejecución modifica su cantidad y su precio medio, y al reducirla se **realiza** un
beneficio o una pérdida. Es una dataclass normal, sin `frozen`. El método `aplicar` recibe una cantidad con signo
(positiva para comprar, negativa para vender):

```python
@dataclass
class Posicion:
    simbolo: str
    cantidad: int = 0
    precio_medio: float = 0.0
    pnl_realizado: float = 0.0

    def aplicar(self, cantidad: int, precio: float) -> None:
        """Suma una ejecución: cantidad > 0 compra; < 0 vende."""
        if self.cantidad == 0 or self.cantidad * cantidad > 0:
            total = self.cantidad + cantidad  # abre o amplía
            self.precio_medio = (self.cantidad * self.precio_medio
                                 + cantidad * precio) / total
            self.cantidad = total
            return
        signo = 1 if self.cantidad > 0 else -1  # reduce
        cerrada = min(abs(cantidad), abs(self.cantidad))
        self.pnl_realizado += signo * cerrada * (
            precio - self.precio_medio)
        self.cantidad += cantidad
        if self.cantidad == 0:
            self.precio_medio = 0.0
        elif self.cantidad * signo < 0:  # ha dado la vuelta
            self.precio_medio = precio

    def valor(self, precio: float) -> float:
        return self.cantidad * precio

    def pnl_no_realizado(self, precio: float) -> float:
        return self.cantidad * (precio - self.precio_medio)

p = Posicion("RVAR")
p.aplicar(100, 150.0)
p.aplicar(50, 160.0)
p.aplicar(-80, 166.0)
print(p)
```
```output
Posicion(simbolo='RVAR', cantidad=70, precio_medio=153.33333333333334, pnl_realizado=1013.3333333333326)
```

Las dos compras dejan 150 títulos a un precio medio de 153,33; la venta de 80 a 166 realiza
80 × (166 − 153,33) = 1.013,33 de beneficio, y quedan 70 títulos al mismo precio medio. El `repr` de la dataclass
muestra los floats tal cual (`153.33333333333334`, `1013.3333333333326`): para leerlos se redondean al imprimir, como
en la celda siguiente.

La cartera no *es* una posición: *tiene* posiciones, y efectivo. Eso es **composición**: un objeto que contiene
otros y les delega el trabajo. La cartera solo sabe convertir una `Orden` en una ejecución con signo, cobrar la
comisión y sumar.

Un detalle de las dataclasses: un valor por defecto mutable, como un diccionario vacío, sería **el mismo objeto** para
todas las carteras. La dataclass no lo permite:

```python raises
@dataclass
class CarteraMal:
    efectivo: float
    posiciones: dict = {}
```
```output
ValueError: mutable default <class 'dict'> for field posiciones is not allowed: use default_factory
```

La forma correcta es `field(default_factory=dict)`, que crea un diccionario nuevo para cada instancia:

```python
@dataclass
class Cartera:
    efectivo: float
    comision: float = 1.0  # euros por orden
    posiciones: dict[str, Posicion] = field(default_factory=dict)

    def ejecutar(self, orden: Orden) -> None:
        signo = 1 if orden.lado == "compra" else -1
        if orden.simbolo not in self.posiciones:
            self.posiciones[orden.simbolo] = Posicion(orden.simbolo)
        self.posiciones[orden.simbolo].aplicar(
            signo * orden.cantidad, orden.precio)
        self.efectivo -= signo * orden.importe + self.comision

    def valor(self, precios: dict[str, float]) -> float:
        return self.efectivo + sum(
            p.valor(precios[s]) for s, p in self.posiciones.items())

    def pnl(self, precios: dict[str, float]) -> tuple[float, float]:
        realizado = sum(p.pnl_realizado
                        for p in self.posiciones.values())
        no_realizado = sum(p.pnl_no_realizado(precios[s])
                           for s, p in self.posiciones.items())
        return realizado, no_realizado

cartera = Cartera(efectivo=50_000)
for o in [Orden("RVAR", "compra", 100, 150.0),
          Orden("ORO", "compra", 50, 165.0),
          Orden("RVAR", "compra", 50, 160.0),
          Orden("RVAR", "venta", 80, 166.0)]:
    cartera.ejecutar(o)

# cierres del 2025-12-31 en cartera_sint.csv
cierres = {"RVAR": 164.35, "RFIJ": 102.29, "ORO": 171.44,
           "TECH": 77.82}
realizado, no_realizado = cartera.pnl(cierres)
print(f"Efectivo:     {cartera.efectivo:>10,.2f}")
print(f"Valor total:  {cartera.valor(cierres):>10,.2f}")
print(f"Realizado:    {realizado:>10,.2f}")
print(f"No realizado: {no_realizado:>10,.2f}")
print(f"Comisiones:   {4 * cartera.comision:>10,.2f}")
```
```output
Efectivo:      32,026.00
Valor total:   52,102.50
Realizado:      1,013.33
No realizado:   1,093.17
Comisiones:         4.00
```

Las cifras cuadran: el valor total menos los 50.000 iniciales es igual al P&L realizado más el no realizado menos las
cuatro comisiones. Esa identidad es una buena candidata a test automático (lección 0.4.5): si algún día deja de
cumplirse, hay un error en la contabilidad.

## Herencia: variantes de una orden

La **herencia** crea una clase a partir de otra: la subclase recibe los atributos y métodos de la clase base y puede
añadir o sustituir los suyos. Encaja cuando un objeto *es un* caso particular de otro. Una orden de mercado, una
limitada y una de stop son órdenes; lo que cambia entre ellas es **cuándo se ejecutan**.

La clase base declara el método `se_ejecuta` como abstracto (`abc.ABC` y `@abstractmethod`): no se puede crear una
orden genérica, solo una de sus variantes. Cada subclase añade sus campos y su regla:

```python
from abc import ABC, abstractmethod

@dataclass(frozen=True)
class OrdenBase(ABC):
    simbolo: str
    lado: str
    cantidad: int

    @abstractmethod
    def se_ejecuta(self, precio: float) -> bool: ...

@dataclass(frozen=True)
class OrdenMercado(OrdenBase):
    def se_ejecuta(self, precio: float) -> bool:
        return True

@dataclass(frozen=True)
class OrdenLimitada(OrdenBase):
    limite: float

    def se_ejecuta(self, precio: float) -> bool:
        if self.lado == "compra":
            return precio <= self.limite
        return precio >= self.limite

@dataclass(frozen=True)
class OrdenStop(OrdenBase):
    stop: float

    def se_ejecuta(self, precio: float) -> bool:
        if self.lado == "venta":
            return precio <= self.stop
        return precio >= self.stop

ordenes = [OrdenMercado("SINT", "compra", 10),
           OrdenLimitada("SINT", "compra", 10, limite=140.0),
           OrdenStop("SINT", "venta", 10, stop=138.0)]
for precio in (142.0, 139.5, 137.8):
    activas = [type(o).__name__ for o in ordenes
               if o.se_ejecuta(precio)]
    print(precio, activas)
```
```output
142.0 ['OrdenMercado']
139.5 ['OrdenMercado', 'OrdenLimitada']
137.8 ['OrdenMercado', 'OrdenLimitada', 'OrdenStop']
```

El bucle no pregunta de qué tipo es cada orden: llama a `se_ejecuta` y cada objeto responde con su regla. Eso es
**polimorfismo**, y es lo que hace útil la herencia. Añadir una orden de stop limitada no obliga a tocar el bucle,
solo a escribir una subclase más. Intentar crear la clase base falla, como se quería:

```python raises
OrdenBase("SINT", "compra", 10)
```
```output
TypeError: Can't instantiate abstract class OrdenBase without an implementation for abstract method 'se_ejecuta'
```

Una trampa de las dataclasses con herencia: los campos de la subclase van después de los de la base. Si la base
tuviera un campo con valor por defecto, la subclase no podría añadir uno sin él (`TypeError: non-default argument
follows default argument`). La salida es declarar los campos como solo-palabra-clave con `@dataclass(kw_only=True)`,
o no poner valores por defecto en la base.

## Herencia o composición

Las dos relaciones se dibujan distinto. Las flechas «es una» son herencia; las flechas «tiene» y «ejecuta», composición:

```mermaid
flowchart LR
  M[OrdenMercado] -->|es una| B[OrdenBase]
  L[OrdenLimitada] -->|es una| B
  S[OrdenStop] -->|es una| B
  C[Cartera] -->|tiene| P[Posicion]
  C -->|ejecuta| O[Orden]
```

La regla práctica: **hereda solo si la frase «X es un Y» es cierta y lo seguirá siendo**; en cualquier otro caso,
compón. Los errores típicos en código de trading van casi siempre en la misma dirección, heredar de más:

- `class Estrategia(Cartera)`: una estrategia no es una cartera. Si mañana dos estrategias comparten cartera, o una
  estrategia reparte capital entre dos, la herencia no lo permite.
- `class MiEstrategia(EstrategiaMediasMoviles, GestorRiesgo, ConectorBroker)`: herencia múltiple para «reunir
  funciones». Cada clase base puede pisar métodos de las otras, y probar la estrategia exige un bróker.
- Jerarquías de cinco niveles (`Estrategia` → `EstrategiaTendencia` → `EstrategiaMedias` → …) en las que un cambio
  arriba rompe algo abajo.

Con composición, la estrategia recibe sus piezas al crearla. Cambiar el gestor de riesgo, o usar uno falso en un
test, no exige tocar la estrategia:

```python
@dataclass
class LimiteRiesgo:
    max_importe: float

    def permite(self, orden: Orden) -> bool:
        return orden.importe <= self.max_importe

@dataclass
class Ejecutor:
    cartera: Cartera
    riesgo: LimiteRiesgo
    rechazadas: list[Orden] = field(default_factory=list)

    def enviar(self, orden: Orden) -> bool:
        if not self.riesgo.permite(orden):
            self.rechazadas.append(orden)
            return False
        self.cartera.ejecutar(orden)
        return True

ejecutor = Ejecutor(Cartera(efectivo=20_000),
                    LimiteRiesgo(max_importe=10_000))
print(ejecutor.enviar(Orden("TECH", "compra", 100, 77.82)))
print(ejecutor.enviar(Orden("ORO", "compra", 100, 171.44)))
print(ejecutor.cartera.posiciones)
print([o.simbolo for o in ejecutor.rechazadas])
```
```output
True
False
{'TECH': Posicion(simbolo='TECH', cantidad=100, precio_medio=77.82, pnl_realizado=0.0)}
['ORO']
```

La compra de TECH (7.782 €) pasa el límite; la de ORO (17.144 €) no, y queda registrada como rechazada. Para un
límite distinto, por ejemplo por exposición total de la cartera, basta con otra clase que tenga un método `permite`
con la misma firma. Cómo declarar formalmente esa «firma común» (un `Protocol`) se ve en la lección 0.4.2.

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

Las clases no son obligatorias. Una función que recibe precios y devuelve una serie (como `senal_cruce` en la lección
0.1.1) no necesita clase: no guarda estado entre llamadas. Usa una clase cuando haya estado que proteger (una
posición) o variantes con la misma interfaz (los tipos de orden).

</aside>

## Ejercicios

1. Añade a `Orden` un campo opcional `etiqueta: str = ""` (por ejemplo, el nombre de la estrategia que la generó)
   que no cuente al comparar órdenes. Comprueba que dos órdenes iguales salvo la etiqueta son iguales.

**Solución.**

```python
@dataclass(frozen=True, slots=True)
class OrdenEtiquetada:
    simbolo: str
    lado: str
    cantidad: int
    precio: float
    etiqueta: str = field(default="", compare=False)

x = OrdenEtiquetada("SINT", "compra", 100, 142.22, "cruce")
y = OrdenEtiquetada("SINT", "compra", 100, 142.22, "ruptura")
print(x == y, hash(x) == hash(y))
```
```output
True True
```

Con `compare=False` el campo queda fuera de `__eq__` y de `__hash__`: las dos órdenes son iguales y caen en el mismo
hueco de un conjunto. El campo con valor por defecto va al final, después de los que no lo tienen.

2. Ordena estas órdenes de compra por prioridad precio-tiempo (mejor precio = el más alto; a igual precio, la que
   llegó antes): `OrdenLibro(142.20, 4, "c4", 100)`, `OrdenLibro(142.25, 6, "c6", 50)`,
   `OrdenLibro(142.20, 1, "c1", 200)`.

**Solución.**

```python
compras = [OrdenLibro(142.20, 4, "c4", 100),
           OrdenLibro(142.25, 6, "c6", 50),
           OrdenLibro(142.20, 1, "c1", 200)]
for o in sorted(compras, key=lambda o: (-o.precio, o.llegada)):
    print(o.id, o.precio, o.llegada)
```
```output
c6 142.25 6
c1 142.2 1
c4 142.2 4
```

La clave `(-precio, llegada)` invierte el precio y mantiene el tiempo: primero 142,25 y, a 142,20, la orden que llegó
en el instante 1 antes que la del instante 4.

3. Con la `Cartera` de la lección, abre un corto: vende 40 TECH a 80 y recompra 40 a 77,82. ¿Cuál es el P&L
   realizado? ¿Qué cantidad y precio medio quedan en la posición?

**Solución.**

```python
corta = Cartera(efectivo=10_000)
corta.ejecutar(Orden("TECH", "venta", 40, 80.0))
corta.ejecutar(Orden("TECH", "compra", 40, 77.82))
print(corta.posiciones["TECH"])
print(f"Efectivo: {corta.efectivo:,.2f}")
```
```output
Posicion(simbolo='TECH', cantidad=0, precio_medio=0.0, pnl_realizado=87.20000000000027)
Efectivo: 10,085.20
```

El corto gana 40 × (80 − 77,82) = 87,20 y la posición vuelve a cero con precio medio cero. El `repr` muestra
`87.20000000000027`, no `87.2`: `80 - 77.82` da `2.180000000000007` en coma flotante (lección 0.3.1). Es un error de
representación, no de contabilidad, pero obliga a dos cosas: redondear al mostrar y comparar con tolerancia en los
tests (`pytest.approx`, lección 0.4.5), nunca con `==`. El efectivo sube esos 87,20 menos las dos comisiones de 1 €.
`Posicion.aplicar` trata igual largos y cortos porque trabaja con cantidades con signo.

## Autoevaluación

1. Una orden ya enviada al bróker no debe cambiar de cantidad por accidente. ¿Qué opción lo garantiza en Python?

   - A. Un diccionario con la clave `cantidad`
   - B. Una dataclass con `frozen=True`: asignar un campo lanza `FrozenInstanceError`
   - C. Una clase normal con un comentario que diga «no modificar»
   - D. Una dataclass con `order=True`

   *Respuesta B: Una dataclass con `frozen=True`: asignar un campo lanza `FrozenInstanceError`.* `frozen=True` hace que asignar un campo después de crear el objeto lance una excepción. Para «cambiar» una orden se crea otra con `dataclasses.replace`, y la original queda intacta. `order=True` solo añade comparaciones.

2. Una dataclass tiene el campo `posiciones: dict = {}`. ¿Qué pasa al definir la clase?

   - A. Nada: cada cartera tendrá su propio diccionario vacío
   - B. Todas las carteras comparten el mismo diccionario, sin aviso
   - C. Lanza `ValueError`: los valores por defecto mutables se declaran con `field(default_factory=dict)`
   - D. Lanza `TypeError` al crear la primera cartera

   *Respuesta C: Lanza `ValueError`: los valores por defecto mutables se declaran con `field(default_factory=dict)`.* Un `{}` por defecto sería el mismo objeto para todas las instancias. La dataclass lo detecta al definir la clase y lanza `ValueError`; `default_factory` crea un diccionario nuevo por instancia.

3. Con `@dataclass(order=True)` y los campos `precio` y `llegada` (en ese orden), ¿cómo ordena `sorted()` una lista de órdenes de venta?

   - A. Por precio y, a igual precio, por llegada: la prioridad precio-tiempo de un libro de órdenes
   - B. Solo por precio
   - C. Por el orden en que se crearon los objetos
   - D. Al azar, porque los objetos no se pueden comparar

   *Respuesta A: Por precio y, a igual precio, por llegada: la prioridad precio-tiempo de un libro de órdenes.* `order=True` compara los campos como una tupla, en el orden en que se declaran. Los campos con `field(compare=False)` quedan fuera de la comparación.

4. Una estrategia necesita una cartera y un gestor de riesgo. ¿Qué diseño sigue la regla de la lección?

   - A. `class Estrategia(Cartera, GestorRiesgo)`
   - B. `class Estrategia(Cartera)` y el riesgo como métodos de la estrategia
   - C. Una estrategia que tiene como atributos una cartera y un gestor de riesgo (composición)
   - D. Variables globales compartidas por todos los módulos

   *Respuesta C: Una estrategia que tiene como atributos una cartera y un gestor de riesgo (composición).* Una estrategia no es una cartera: tiene una. Con composición se puede cambiar el gestor de riesgo sin tocar la estrategia, y cada pieza se prueba por separado.

## Ideas clave

- Una clase junta datos y comportamiento. `__repr__` y `__eq__` deciden cómo se imprime y cuándo dos objetos son iguales.
- `@dataclass` escribe `__init__`, `__repr__` y `__eq__` por ti; `frozen=True` impide modificar un objeto y lo hace utilizable en conjuntos y como clave de diccionario.
- `order=True` compara los campos como una tupla: la prioridad precio-tiempo de un libro de órdenes sale de declarar los campos en ese orden.
- Los valores por defecto mutables van con `field(default_factory=…)`; la dataclass impide el error clásico de compartir una lista o un diccionario entre instancias.
- Hereda cuando el objeto es una variante de otro (una orden limitada es una orden); compón cuando lo usa (una estrategia tiene una cartera).

## Recursos

### Research

- [Máquinas de estado finito para la ejecución en trading cuantitativo](https://pythonparatrading.com/maquinas-estado-finito-trading-python/) · Research-0029 · 49 min

### Documentación

- [Documentación de Python 3.14: dataclasses](https://docs.python.org/3.14/library/dataclasses.html) · docs.python.org
- [Tutorial de Python en español: clases](https://docs.python.org/es/3/tutorial/classes.html) · docs.python.org

### Libros

- [Luciano Ramalho, Fluent Python, 2.ª ed. (O'Reilly, 2022)](https://www.fluentpython.com/) · fluentpython.com

### Datos

- [Datos del nivel 0: cierres de cuatro activos ficticios (CSV)](https://pythonparatrading.com/curso/datos/nivel-0/cartera_sint.csv) · Descarga · cartera_sint.csv

Lección anterior: [0.3.6 · Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15](https://pythonparatrading.com/curso-python-trading/nivel-0/excepciones-modulos-y-paquetes/)

Lección siguiente: [0.4.2 · Tipado: type hints, mypy o ty y Pydantic para configuraciones](https://pythonparatrading.com/curso-python-trading/nivel-0/tipado-type-hints-y-pydantic/)
