Lección 0.4.1 · 45 min

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.

Lecciones del nivel 0
  1. 0.1 · Bienvenida y expectativas

    1. 0.1.1 Trading sistemático, algorítmico y cuantitativo frente al discrecional
    2. 0.1.2 Expectativas realistas: costes, tiempo y riesgo de pérdida
    3. 0.1.3 Estructura del curso, tests de entrada, repositorio personal y aprendizaje con IA
  2. 0.2 · Entorno de trabajo moderno

    1. 0.2.1 Instalación de Python con uv: versiones y ciclo de soporte
    2. 0.2.2 Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles
    3. 0.2.3 Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)
    4. 0.2.4 Terminal básica, rutas, variables de entorno y ficheros .env
    5. 0.2.5 Git y GitHub: commits, ramas, pull requests y .gitignore
    6. 0.2.6 Introducción a los asistentes de código con IA: prompts, revisión y tests
  3. 0.3 · Fundamentos del lenguaje

    1. 0.3.1 Variables, tipos y operadores; coma flotante y decimal para precios y ticks
    2. 0.3.2 Cadenas, f-strings y formato de números y fechas
    3. 0.3.3 Listas, tuplas, diccionarios, conjuntos y comprensiones
    4. 0.3.4 Control de flujo: if, elif, else, for, while y match-case
    5. 0.3.5 Funciones: argumentos, *args y **kwargs, lambdas y ámbito
    6. 0.3.6 Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15
  4. 0.4 · Python intermedio

    1. 0.4.1 Programación orientada a objetos: clases, herencia, composición y dataclasses
    2. 0.4.2 Tipado: type hints, mypy o ty y Pydantic para configuraciones
    3. 0.4.3 Iteradores, generadores, decoradores y context managers
    4. 0.4.4 Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias
    5. 0.4.5 Logging profesional y testing con pytest
    6. 0.4.6 Concurrencia: threading, multiprocessing, asyncio y free-threading
  5. 0.5 · Computación numérica y datos

    1. 0.5.1 NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios
    2. 0.5.2 pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()
    3. 0.5.3 Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles
    4. 0.5.4 Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas
    5. 0.5.5 DuckDB y SQL esencial: SELECT, JOIN, GROUP BY y funciones de ventana sobre Parquet
  6. 0.6 · Visualización

    1. 0.6.1 Matplotlib orientado a objetos, Seaborn y Plotly: gráficos de precios, drawdown y velas
    2. 0.6.2 Dashboards rápidos con Streamlit: un panel de seguimiento en un solo script
  7. 0.7 · Matemáticas y estadística mínimas

    1. 0.7.1 Rentabilidades simples y logarítmicas, interés compuesto y estadística descriptiva
    2. 0.7.2 Probabilidad y distribuciones, correlación frente a causalidad y regresión simple
    3. 0.7.3 Álgebra lineal intuitiva: vectores y matrices como preparación para carteras

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:

ObjetoQué guarda¿Cambia?Qué hace
Ordensímbolo, lado, cantidad, precioNo: una vez enviada, se cancela o se sustituyeCalcula su importe, se valida
Posiciónsímbolo, cantidad, precio medio, P&L realizadoSí: con cada ejecuciónAplica ejecuciones, calcula su valor
Carteraefectivo y posicionesSí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.

IN [01]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)
OUT [01]Orden como clase normal
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.

IN [02]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}")
OUT [02]Orden como dataclass inmutable
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:

IN [03]python
orden.cantidad = 200
OUT [03]Una orden no se modifica
dataclasses.FrozenInstanceError: cannot assign to field 'cantidad'

Y una orden mal construida no llega a existir:

IN [04]python
Orden("SINT", "comprar", 100, 142.22)
OUT [04]Validación al crear
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:

IN [05]python
reducida = replace(orden, cantidad=50)
print(reducida)
print(orden.cantidad)

pendientes = {orden, Orden("SINT", "compra", 100, 142.22), reducida}
print(len(pendientes))
OUT [05]replace y conjuntos
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):

IN [06]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)
OUT [06]Prioridad de las órdenes de venta
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):

IN [07]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)
OUT [07]Posición con precio medio y P&L realizado
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:

IN [08]python
@dataclass
class CarteraMal:
    efectivo: float
    posiciones: dict = {}
OUT [08]Valor por defecto mutable
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:

IN [09]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}")
OUT [09]Cartera compuesta de posiciones
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:

IN [10]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)
OUT [10]Tipos de orden por herencia
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:

IN [11]python
OrdenBase("SINT", "compra", 10)
OUT [11]La clase base es abstracta
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:

es unaes unaes unatieneejecutaOrdenMercadoOrdenBaseOrdenLimitadaOrdenStopCarteraPosicionOrden
Fuente 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:

IN [12]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])
OUT [12]Composición: riesgo y cartera como piezas
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.

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
IN [13]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))
OUT [13]Campo que no cuenta en la igualdad
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.

  1. 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
IN [14]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)
OUT [14]Prioridad de las compras
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.

  1. 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
IN [15]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}")
OUT [15]Un corto en la cartera
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.

Respuesta plegada en cada pregunta

Autoevaluación

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

    • AUn diccionario con la clave cantidad
    • BUna dataclass con frozen=True: asignar un campo lanza FrozenInstanceError
    • CUna clase normal con un comentario que diga «no modificar»
    • DUna dataclass con order=True
    Ver respuesta

    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?

    • ANada: cada cartera tendrá su propio diccionario vacío
    • BTodas las carteras comparten el mismo diccionario, sin aviso
    • CLanza ValueError: los valores por defecto mutables se declaran con field(default_factory=dict)
    • DLanza TypeError al crear la primera cartera
    Ver respuesta

    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?

    • APor precio y, a igual precio, por llegada: la prioridad precio-tiempo de un libro de órdenes
    • BSolo por precio
    • CPor el orden en que se crearon los objetos
    • DAl azar, porque los objetos no se pueden comparar
    Ver respuesta

    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?

    • Aclass Estrategia(Cartera, GestorRiesgo)
    • Bclass Estrategia(Cartera) y el riesgo como métodos de la estrategia
    • CUna estrategia que tiene como atributos una cartera y un gestor de riesgo (composición)
    • DVariables globales compartidas por todos los módulos
    Ver respuesta

    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