---
title: "4.5.1 Arquitectura de un sistema de trading en vivo"
description: "Datos, señal, riesgo, OMS, broker y reconciliación: un sistema en vivo por eventos con reloj lógico y un broker simulado, ejecutado de verdad."
url: "https://pythonparatrading.com/curso-python-trading/nivel-4/arquitectura-de-un-sistema-en-vivo/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-4/arquitectura-de-un-sistema-en-vivo.png"
---

# 4.5.1 · Arquitectura de un sistema en vivo: de los datos a la reconciliación

> Un sistema en vivo no es un backtest con un broker enchufado: tiene estado que sobrevive al día, mensajes que llegan tarde o no llegan y dinero que alguien tiene que cuadrar. Esta lección monta, en celdas que se ejecutan, las seis piezas de la cadena con un bus de eventos y un reloj lógico, comprueba la señal contra el registro de un sistema real y provoca un fallo de transporte para ver qué pieza lo descubre.

- Curso: [Nivel 4: Carteras, riesgo, ejecución y trading en real](https://pythonparatrading.com/curso-python-trading/nivel-4/)
- Módulo: 4.5 · Sistema de trading en vivo
- Duración: 70 min
- Requisitos: [2.4.2 · Backtest por eventos desde cero: bucle, cartera, órdenes y fills](https://pythonparatrading.com/curso-python-trading/nivel-2/backtest-por-eventos-desde-cero/), [2.4.5 · NautilusTrader: arquitectura, datos, estrategias y el mismo código en backtest y en real](https://pythonparatrading.com/curso-python-trading/nivel-2/nautilustrader-arquitectura-y-estrategias/), [4.2.5 · Control del drawdown y kill switch](https://pythonparatrading.com/curso-python-trading/nivel-4/control-del-drawdown-y-kill-switch/)

## Al terminar esta lección

- Situar las seis piezas de un sistema en vivo (datos, señal, riesgo, OMS, broker, reconciliación) y lo que entra y sale de cada una.
- Programar un bus de eventos con reloj lógico, sin `sleep` ni reloj real, que hace el sistema reproducible evento a evento.
- Aplicar controles previos a la orden (valor máximo, número de órdenes, interruptor) y calibrarlos con el historial de órdenes.
- Reconciliar cada día la cartera interna con la del broker y explicar por qué un mensaje perdido se convierte en órdenes equivocadas si nadie lo detecta.

## Del backtest al sistema en vivo

Un backtest por eventos (lección 2.4.2) ya recorre el tiempo como lo haría el sistema en real: llega una barra, la
estrategia decide, el broker simulado ejecuta. Lo que no tiene es lo que hace difícil operar de verdad:

- **Estado que sobrevive.** El backtest empieza de cero en cada ejecución. El sistema en vivo arranca cada mañana con
  posiciones, órdenes abiertas y efectivo de ayer, y tiene que saber cuáles son.
- **Mensajes, no llamadas.** En el backtest el broker devuelve el fill en la misma función. En real, la orden sale por
  la red, el broker contesta cuando contesta, a veces en otro orden, a veces dos veces, a veces nunca (lección 4.5.2).
- **Dos verdades.** El sistema lleva su contabilidad; el broker lleva la suya, que es la que vale. Si no coinciden,
  hay que saberlo antes de la siguiente orden.
- **Controles.** Un error de programación en un backtest da un mal gráfico. En real da una orden.

Esta lección monta la cadena completa en celdas que se ejecutan: el mismo sistema de los datos del nivel (10 ETF
ficticios, pesos por momentum y volatilidad), con un broker simulado dentro del proceso. Las lecciones siguientes
profundizan en cada borde: las órdenes y sus estados (4.5.2), el paper trading y el deslizamiento (4.5.3), la
programación de tareas y las alertas (4.5.4), la tolerancia a fallos y la reconciliación al arrancar (4.5.5) y los
motores que hacen todo esto por ti (4.5.6).

## Seis piezas y un bus

```mermaid
flowchart TB
  D[Datos] --> S[Señal]
  S --> R[Riesgo]
  R --> O[OMS]
  O --> B[Broker]
  B --> C[Cartera]
  B --> O
  C --> Q[Reconciliación]
  B --> Q
  Q --> M[Monitor]
```

| Pieza | Recibe | Publica | Lo que puede fallar |
|---|---|---|---|
| Datos | Precios del proveedor | Precios con su hora | Datos tarde, huecos, ajustes por splits |
| Señal | Precios hasta ahora | Pesos objetivo | Usar información que aún no existe |
| Riesgo | Pesos y órdenes propuestas | Pesos aprobados, bloqueos | Límites mal calibrados: bloquear lo bueno o dejar pasar lo malo |
| OMS | Pesos aprobados, informes del broker | Órdenes con identificador propio | Duplicar o perder órdenes, estados mal llevados |
| Broker | Órdenes | Aceptaciones, ejecuciones, rechazos | Rechazos, ejecuciones parciales, desconexiones |
| Cartera | Ejecuciones | Posiciones y efectivo internos | Contar dos veces una ejecución o no contarla |
| Reconciliación | Cartera interna y la del broker | Descuadres | Llegar tarde: después de la siguiente orden |

La regla que ordena todo: **cada pieza solo conoce los mensajes que le han llegado**. La señal no pregunta al broker
cuántas acciones hay; el OMS no calcula momentum; la cartera no decide nada. Así cada pieza se prueba sola y se puede
sustituir sin tocar las demás. La figura de la cabecera muestra esa cadena en una orden real de los datos, del OMS al
broker y de vuelta, en milisegundos.

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

Las empresas de servicios de inversión que hacen negociación algorítmica en la UE están obligadas a buena parte de
esto: controles previos a cada orden (bandas de precios, valor y volumen máximos, límites de mensajes), una función
para anular de inmediato todas las órdenes abiertas, supervisión en tiempo real y controles después de la
negociación (Reglamento Delegado (UE) 2017/589, artículos 12, 15, 16 y 17). Un particular no está sujeto a ese
reglamento, pero las razones técnicas son las mismas.

</aside>

## Un reloj que no es el del ordenador

El corazón del sistema es un **bus de eventos**: una cola de prioridad ordenada por hora en la que cada pieza publica
mensajes y a la que se suscribe por tipo. La hora no la da `datetime.now()` sino el propio evento: el bus avanza su
reloj al sacar cada mensaje. Con datos históricos el reloj salta de evento en evento sin esperar; en real, el
adaptador de datos publica con la hora del mercado y el bus va al ritmo de la llegada. El resto del código no
distingue los dos casos, y eso es lo que permite repetir un día de producción en el laboratorio.

```python
import hashlib
import heapq
import itertools
from collections import Counter, defaultdict
from dataclasses import dataclass

import exchange_calendars as xcals
import numpy as np
import pandas as pd

@dataclass(frozen=True)
class Evento:
    t: pd.Timestamp       # hora lógica (UTC)
    tipo: str
    datos: dict

class Bus:
    """Cola de eventos por hora; el reloj es el del último evento."""

    def __init__(self):
        self.cola, self.subs = [], defaultdict(list)
        self.n = itertools.count()   # desempate: orden de publicación
        self.ahora = None
        self.registro = []

    def suscribir(self, tipo, fn):
        self.subs[tipo].append(fn)

    def publicar(self, t, tipo, **datos):
        if self.ahora is not None and t < self.ahora:
            raise ValueError(f"{tipo} con hora pasada: {t}")
        heapq.heappush(self.cola,
                       (t, next(self.n), Evento(t, tipo, datos)))

    def correr(self):
        while self.cola:
            t, _, ev = heapq.heappop(self.cola)
            self.ahora = t
            self.registro.append(ev)
            for fn in self.subs[ev.tipo]:
                fn(ev)

bus = Bus()
bus.suscribir("eco", lambda ev: print(ev.t.time(), ev.datos))
t0 = pd.Timestamp("2025-09-02 19:45", tz="UTC")
bus.publicar(t0 + pd.Timedelta(seconds=2), "eco", n=2)
bus.publicar(t0, "eco", n=1)
bus.correr()
```
```output
19:45:00 {'n': 1}
19:45:02 {'n': 2}
```

Los eventos salen por su hora, no por el orden en que se publicaron, y un evento con hora anterior al reloj es un
error: nada puede ocurrir en el pasado. El contador `n` resuelve los empates (dos eventos a la misma hora salen en el
orden en que se publicaron), así que el resultado no depende de nada externo.

## Datos y señal: lo que se sabe a cada hora

Los datos son los del sistema ficticio del nivel: 10 ETF que siguen índices del universo de la lección 4.1.1
(`universo4_precios.parquet`). El precio de cada ETF es su índice por una escala; la escala sale del extracto del
broker (`vivo4_extracto_posiciones.csv`), que da el precio de cierre de cada ETF. Tomamos las fechas anteriores al
desdoblamiento 3 por 1 de ORAU del 17 de noviembre (lo que hace un desdoblamiento con el sistema es el ejercicio 3):

```python
MAPA = {"ORUS": "RV_EEUU", "ORSC": "RV_EEUU_P", "OREU": "RV_EUR",
        "OREM": "RV_EMG", "ORTN": "RF_T710", "ORTL": "RF_T20",
        "ORIG": "RF_CIG", "ORAU": "MP_ORO", "ORCM": "MP_AMP",
        "ORRE": "INM_EEUU"}
ETF = list(MAPA)
u = pd.read_parquet("universo4_precios.parquet").set_index("fecha")
ext = pd.read_csv("vivo4_extracto_posiciones.csv",
                  parse_dates=["fecha"])
prim = ext[ext["fecha"] < "2025-11-17"].groupby("instrumento").first()
escala = pd.Series({t: prim.loc[t, "precio_cierre"]
                    / u.loc[prim.loc[t, "fecha"], a]
                    for t, a in MAPA.items()})
precios = u[list(MAPA.values())].set_axis(ETF, axis=1) * escala

CAL = xcals.get_calendar("XNYS")
SESIONES = CAL.sessions_in_range("2025-09-02", "2025-12-31")
print(len(SESIONES), "sesiones;", "cierre del 2 de septiembre:",
      CAL.session_close(SESIONES[0]), "| del 31 de diciembre:",
      CAL.session_close(SESIONES[-1]))
precios.loc["2025-09-02"].round(2).to_dict()
```
```output
85 sesiones; cierre del 2 de septiembre: 2025-09-02 20:00:00+00:00 | del 31 de diciembre: 2025-12-31 21:00:00+00:00
{'ORUS': 638.29, 'ORSC': 232.33, 'OREU': 61.23, 'OREM': 49.31, 'ORTN': 96.05, 'ORTL': 88.18, 'ORIG': 109.19, 'ORAU': 311.26, 'ORCM': 24.05, 'ORRE': 93.5}
```

El cierre de la NYSE son las 16:00 de Nueva York: las 20:00 UTC hasta el 31 de octubre y las 21:00 UTC desde el 3 de
noviembre, cuando EE. UU. cambia de horario. Un sistema que programe la señal «a las 19:45 UTC» la lanzará una hora
antes de lo previsto la mitad del año; uno que la programe 15 minutos antes del cierre de cada sesión del calendario,
nunca. Las medias sesiones (28 de noviembre y 24 de diciembre, cierre a las 13:00 de Nueva York) también salen del
calendario.

La pieza de datos publica tres eventos por sesión: `precios` 15 minutos antes del cierre (con los cierres **hasta la
víspera**), `cierre` a la hora del cierre (con el de hoy, que solo usa el broker simulado para llenar las órdenes) y
`fin_dia` media hora después. La señal es la regla del sistema del nivel: en cada ETF con momentum de 20 sesiones
positivo, un peso de `min(2,5 % / vol_63, 25 %)`, normalizados si suman más del 100 %.

```python
class Datos:
    """Publica lo que se sabe en cada momento, con su hora."""

    def __init__(self, bus, precios):
        self.bus, self.p = bus, precios

    def programar(self, sesiones):
        for d in sesiones:
            cierre = CAL.session_close(d)
            hist = self.p.loc[:d].iloc[-65:-1]   # hasta la víspera
            self.bus.publicar(cierre - pd.Timedelta(minutes=15),
                              "precios", fecha=d, hist=hist)
            self.bus.publicar(cierre, "cierre", fecha=d,
                              px=self.p.loc[d])
            self.bus.publicar(cierre + pd.Timedelta(minutes=30),
                              "fin_dia", fecha=d)

def pesos_momentum(h):
    mom = h.iloc[-1] / h.iloc[-21] - 1
    vol = h.pct_change().iloc[-63:].std() * np.sqrt(252)
    w = np.minimum(0.025 / vol, 0.25).where(mom > 0, 0.0)
    return w / w.sum() if w.sum() > 1 else w

class Senal:
    def __init__(self, bus):
        self.bus = bus
        bus.suscribir("precios", self.calcular)

    def calcular(self, ev):
        h = ev.datos["hist"]
        self.bus.publicar(ev.t, "objetivo", fecha=ev.datos["fecha"],
                          pesos=pesos_momentum(h), ref=h.iloc[-1])
```

## Riesgo antes de cada orden

La pieza de riesgo hace dos cosas. Sobre los pesos, recorta lo que se salga de los límites de la cartera (aquí, el
25 % por ETF y el 100 % en total). Sobre cada orden que propone el OMS, aplica controles **previos a la orden**:
valor máximo (un porcentaje del patrimonio), número máximo de órdenes por día y un interruptor que, activado, deja de
aprobar objetivos. Son tres de los controles que enumera el artículo 15 del RTS 6; las bandas de precios no hacen
falta aquí porque el sistema solo envía órdenes al cierre. El diseño del interruptor y de sus disparadores es la
lección 4.2.5; aquí solo importa dónde está: entre la señal y el OMS, para que ninguna orden pase sin él.

Un límite mal calibrado tiene un coste: si es demasiado estricto, bloquea órdenes legítimas y la cartera se aleja del
objetivo; si es demasiado holgado, no para nada. La forma honrada de fijarlo es mirar el historial de órdenes
legítimas. Las 398 órdenes del OMS del sistema real (`vivo4_ordenes.csv`), con su valor sobre el capital inicial de
1.000.000 USD:

```python
ordenes = pd.read_csv("vivo4_ordenes.csv")
pct = 100 * ordenes["cantidad"] * ordenes["precio_medio_envio"] / 1e6
print(f"Mayor orden: {pct.max():.1f} % del capital; "
      f"mediana: {pct.median():.1f} %")
for lim in (10, 20, 30):
    print(f"Límite {lim} %: bloquearía {(pct > lim).sum()} "
          "órdenes legítimas")
error_dedo = 10 * pct.max()
print(f"Error de dedo (x10): {error_dedo:.0f} %, bloqueada: "
      f"{error_dedo > 30}")
```
```output
Mayor orden: 26.1 % del capital; mediana: 2.2 %
Límite 10 %: bloquearía 72 órdenes legítimas
Límite 20 %: bloquearía 5 órdenes legítimas
Límite 30 %: bloquearía 0 órdenes legítimas
Error de dedo (x10): 261 %, bloqueada: True
```

*Fig. — Valor de las 398 órdenes del OMS del sistema en vivo, en porcentaje del capital inicial, y cuántas bloquearía un límite de valor máximo del 10, el 20 o el 30 %.* Histograma por tramos de 2 puntos: 180 órdenes valen menos del 2 % del capital y 83 entre el 2 y el 4 %; la cola llega al 26,1 %. Tres líneas verticales marcan límites: al 10 % se bloquearían 72 órdenes legítimas, al 20 % cinco y al 30 % (destacado) ninguna.

La mayor orden legítima vale el 26,1 % del capital: la venta de toda la posición en ORTN del 31 de diciembre. Con un
límite del 30 % no se bloquea ninguna; con uno del 20 % se bloquearían cinco, y cuatro de ellas son **ventas** de
posiciones enteras de bonos en diciembre: el límite impediría salir. Un error de dedo que multiplique una cantidad
por diez se queda muy por encima de cualquiera de los tres. El límite de órdenes por día se calibra igual: el día con
más órdenes del historial tiene 15 (como mucho una por ETF y una más por cada límite que no se llena), así que 25 es
holgado sin ser infinito.

```python
class Riesgo:
    """Límites de cartera y controles previos a cada orden."""

    def __init__(self, bus, peso_max=0.25, valor_max=0.30,
                 ordenes_dia=25):
        self.bus, self.peso_max = bus, peso_max
        self.valor_max, self.ordenes_dia = valor_max, ordenes_dia
        self.interruptor = False      # lección 4.2.5
        self.hoy = Counter()
        bus.suscribir("objetivo", self.revisar)

    def revisar(self, ev):
        if self.interruptor:
            self.bus.publicar(ev.t, "alerta", texto="interruptor")
            return
        w = ev.datos["pesos"].clip(upper=self.peso_max)
        w = w / w.sum() if w.sum() > 1 else w
        self.bus.publicar(ev.t, "objetivo_ok",
                          **{**ev.datos, "pesos": w})

    def comprobar(self, orden, patrimonio):
        """None si pasa; si no, el motivo del bloqueo."""
        if orden["cantidad"] * orden["ref"] > (self.valor_max
                                               * patrimonio):
            return "valor máximo"
        self.hoy[orden["fecha"]] += 1
        if self.hoy[orden["fecha"]] > self.ordenes_dia:
            return "órdenes por día"
        return None
```

## OMS, broker y cartera

El **OMS** (*order management system*) convierte los pesos aprobados en órdenes: calcula el patrimonio con la cartera
interna, el número de acciones objetivo de cada ETF y la diferencia con la posición; si la diferencia vale menos del
0,4 % del patrimonio, no opera (como el sistema del nivel). Cada orden recibe un identificador propio del cliente,
pasa por el riesgo y, si lo supera, sale hacia el broker a los cinco minutos, 10 antes del cierre. El OMS lleva el
estado de cada orden con los informes que devuelve el broker; la lección 4.5.2 convierte ese estado en una máquina de
estados con sus reglas.

El **broker simulado** vive en el mismo proceso pero solo habla por el bus, con 42 ms de latencia en cada sentido (la
mediana de un sentido de la cuenta real de los datos). Rechaza las compras que no caben en el efectivo más las ventas
del día, acepta las demás y las llena en la subasta de cierre, en una a tres ejecuciones, a cierre ± una media
horquilla de 1 pb, con la comisión del broker de los datos (0,0035 USD por acción, mínimo 0,35 USD, máximo el 1 % del
efectivo). Lleva su propia cuenta: es la verdad contable, como el extracto de un broker real.

La **cartera** cuenta cada ejecución una vez (por su identificador) y es la que usa el OMS para decidir. El
**reconciliador** compara, al final de cada día, las posiciones y el efectivo de la cartera con los del broker.

```python
MS = pd.Timedelta(milliseconds=1)

def comision(q, px):
    return round(min(max(0.0035 * q, 0.35), 0.01 * q * px), 2)

class Cartera:
    def __init__(self, bus, efectivo):
        self.efectivo, self.pos = efectivo, pd.Series(0, index=ETF)
        self.vistas = set()
        bus.suscribir("informe", self.anotar)

    def anotar(self, ev):
        d = ev.datos
        if d["evento"] not in ("parcial", "ejecutada") \
                or d["id_ejecucion"] in self.vistas:
            return
        self.vistas.add(d["id_ejecucion"])
        s = 1 if d["lado"] == "compra" else -1
        self.pos[d["inst"]] += s * d["cantidad"]
        self.efectivo -= s * d["cantidad"] * d["precio"] + d["comision"]

    def patrimonio(self, px):
        return self.efectivo + float((self.pos * px).sum())

class OMS:
    def __init__(self, bus, riesgo, cartera, umbral=0.004):
        self.bus, self.riesgo, self.cartera = bus, riesgo, cartera
        self.umbral, self.ordenes = umbral, {}
        bus.suscribir("objetivo_ok", self.rebalancear)
        bus.suscribir("informe", self.actualizar)

    def rebalancear(self, ev):
        ref, w = ev.datos["ref"], ev.datos["pesos"]
        pat = self.cartera.patrimonio(ref)
        delta = np.floor(w * pat / ref) - self.cartera.pos
        for inst, q in delta.sort_values().items():  # ventas antes
            if abs(q) * ref[inst] < self.umbral * pat:
                continue
            o = dict(id=f"C{len(self.ordenes) + 1:05d}",
                     fecha=ev.datos["fecha"], inst=inst,
                     lado="compra" if q > 0 else "venta",
                     cantidad=int(abs(q)), ref=float(ref[inst]))
            self.ordenes[o["id"]] = {**o, "estado": "nueva",
                                     "recibida": 0}
            motivo = self.riesgo.comprobar(o, pat)
            if motivo:
                self.ordenes[o["id"]]["estado"] = "bloqueada"
                self.bus.publicar(ev.t, "alerta", texto=motivo)
                continue
            self.ordenes[o["id"]]["estado"] = "enviada"
            self.bus.publicar(ev.t + 5 * 60_000 * MS, "orden", **o)

    def actualizar(self, ev):
        o = self.ordenes[ev.datos["id"]]
        o["estado"] = ev.datos["evento"]
        o["recibida"] += ev.datos.get("cantidad", 0)
        o["acumulada"] = ev.datos.get("acumulada", 0)

class BrokerSimulado:
    LAT = 42 * MS

    def __init__(self, bus, efectivo, media_horquilla_pb=1.0):
        self.bus, self.efectivo = bus, efectivo
        self.pos = pd.Series(0, index=ETF)
        self.hs, self.cola, self.seq = media_horquilla_pb / 1e4, [], 0
        self.perder = set()          # fallos de transporte a propósito
        bus.suscribir("orden", self.recibir)
        bus.suscribir("cierre", self.subasta)

    def informar(self, t, **d):
        self.seq += 1
        if d.get("id_ejecucion") not in self.perder:
            self.bus.publicar(t + self.LAT, "informe", seq=self.seq,
                              **d)

    def recibir(self, ev):
        o, t = ev.datos, ev.t + self.LAT
        ventas = sum(x["cantidad"] * x["ref"] for x in self.cola
                     if x["lado"] == "venta")
        compras = sum(x["cantidad"] * x["ref"] for x in self.cola
                      if x["lado"] == "compra")
        coste = o["cantidad"] * o["ref"] * (o["lado"] == "compra")
        if coste > 0 and compras + coste > self.efectivo + ventas:
            self.informar(t, id=o["id"], evento="rechazada")
            return
        self.cola.append(o)
        self.informar(t, id=o["id"], evento="aceptada")

    def subasta(self, ev):
        for o in self.cola:
            s = 1 if o["lado"] == "compra" else -1
            px = round(ev.datos["px"][o["inst"]] * (1 + s * self.hs), 2)
            n = 1 if o["cantidad"] < 300 else (
                2 if o["cantidad"] < 1500 else 3)
            partes = [o["cantidad"] // n] * n
            partes[-1] += o["cantidad"] - sum(partes)
            acum = 0
            for k, q in enumerate(partes, 1):
                acum += q
                com = comision(q, px)
                self.pos[o["inst"]] += s * q
                self.efectivo -= s * q * px + com
                fin = acum == o["cantidad"]
                self.informar(ev.t + 300 * k * MS, id=o["id"],
                              evento="ejecutada" if fin else "parcial",
                              inst=o["inst"], lado=o["lado"],
                              cantidad=q, acumulada=acum, precio=px,
                              comision=com,
                              id_ejecucion=f"{o['id']}.{k:02d}")
        self.cola = []

class Reconciliador:
    def __init__(self, bus, cartera, broker):
        self.bus, self.c, self.b = bus, cartera, broker
        self.diario = []
        bus.suscribir("fin_dia", self.cuadrar)

    def cuadrar(self, ev):
        dif = (self.b.pos - self.c.pos)
        dif = dif[dif != 0].to_dict()
        de = round(self.b.efectivo - self.c.efectivo, 2)
        self.diario.append(dict(fecha=ev.datos["fecha"].date(),
                                posiciones=dif, efectivo=de))
        if dif or de:
            self.bus.publicar(ev.t, "alerta", texto=f"descuadre {dif}")
```

Hay un detalle de diseño en el OMS: decide con la cartera **interna**, no con la del broker. Es lo normal (la cartera
interna se actualiza al instante con cada informe y no cuesta una consulta), y es exactamente lo que hace peligroso un
mensaje perdido, como se verá enseguida.

## El sistema completo, del 2 de septiembre al 31 de diciembre

Montar el sistema es crear las piezas sobre el mismo bus y dejar que los datos programen sus eventos. Un monitor
mínimo recoge las alertas (la lección 4.5.4 las envía por Telegram, Slack o correo):

```python
def montar(perder=(), capital=1_000_000.0):
    bus = Bus()
    riesgo = Riesgo(bus)
    cartera = Cartera(bus, capital)
    broker = BrokerSimulado(bus, capital)
    broker.perder = set(perder)
    oms = OMS(bus, riesgo, cartera)
    rec = Reconciliador(bus, cartera, broker)
    Senal(bus)
    alertas = []
    bus.suscribir("alerta", lambda ev: alertas.append(
        (ev.t.date(), ev.datos["texto"])))
    Datos(bus, precios).programar(SESIONES)
    bus.correr()
    return dict(bus=bus, oms=oms, cartera=cartera, broker=broker,
                rec=rec, alertas=alertas)

s = montar()
print(Counter(ev.tipo for ev in s["bus"].registro))
print(Counter(o["estado"] for o in s["oms"].ordenes.values()))
cierre = precios.loc[SESIONES[-1]]
print(f"Patrimonio final: {s['cartera'].patrimonio(cierre):,.2f} USD")
print("Días con descuadre:",
      sum(bool(r["posiciones"] or r["efectivo"])
          for r in s["rec"].diario), "| alertas:", len(s["alertas"]))
```
```output
Counter({'informe': 868, 'orden': 350, 'precios': 85, 'objetivo': 85, 'objetivo_ok': 85, 'cierre': 85, 'fin_dia': 85})
Counter({'ejecutada': 344, 'rechazada': 6})
Patrimonio final: 1,077,668.41 USD
Días con descuadre: 0 | alertas: 0
```

Ochenta y cinco sesiones, tres eventos de datos por sesión y la cadena entera detrás: objetivos, objetivos aprobados,
órdenes, informes del broker. De las 350 órdenes, 344 terminan en `ejecutada` y 6 en `rechazada`: son compras que
el broker no acepta porque no caben en el efectivo. El OMS calcula con el cierre de la víspera y manda las ventas
antes que las compras (por eso hay pocas), pero los precios de la subasta no son los de ayer y una cartera casi
invertida al 100 % se queda a veces sin margen; al día siguiente el OMS vuelve a calcular y la diferencia se manda de
nuevo. La cartera interna cuadra con la del broker los 85 días. El patrimonio final no es el tema de la lección (el
sistema es un ejemplo, y cuatro meses no dicen nada de una estrategia, lección 2.5.5).

```mermaid
flowchart TB
  P[precios 19:45] --> OB[objetivo]
  OB --> OK[objetivo_ok]
  OK --> OR[orden 19:50]
  OR --> AC[aceptada]
  CI[cierre 20:00] --> EJ[ejecuciones]
  AC --> EJ
  EJ --> FD[fin_dia 20:30]
```

Una sesión de septiembre, en el orden en que la recorre el bus. Que el sistema sea correcto no se puede ver en el
patrimonio; se comprueba pieza a pieza. La señal tiene una respuesta conocida: el sistema real del nivel registró los
pesos objetivo de cada orden que mandó (`vivo4_senales.csv`, 347 señales). Si nuestra pieza de señal es la misma
regla, sus pesos tienen que coincidir con los registrados:

```python
obj = {ev.datos["fecha"]: ev.datos["pesos"]
       for ev in s["bus"].registro if ev.tipo == "objetivo"}
sen = pd.read_csv("vivo4_senales.csv")
sen["fecha"] = pd.to_datetime(sen["ts"].str[:10])
sen["nuestro"] = [obj[f][i] for f, i in
                  zip(sen["fecha"], sen["instrumento"])]
dif = (sen["nuestro"].round(4) - sen["peso_objetivo"]).abs()
print(f"{len(sen)} señales; diferencia máxima {dif.max():.4f}; "
      f"distintas a 4 decimales: {(dif > 1e-9).sum()}")
```
```output
347 señales; diferencia máxima 0.0001; distintas a 4 decimales: 1
```

Las 347 coinciden; una difiere en una unidad del cuarto decimal, que es lo que puede dar el redondeo cuando el
registro guarda cuatro decimales. Esta comprobación es la versión en vivo de la reconciliación entre motores de la
lección 2.4.2: la misma regla, con los mismos datos hasta la víspera, tiene que dar los mismos números en el
laboratorio y en producción. Si no los da, hay un error en una de las dos, y es mejor encontrarlo antes del dinero.

## Un mensaje perdido

Ahora el fallo. El broker ejecuta todas las órdenes, pero un informe de ejecución no llega: el de la primera parte de
la compra de ORSC del primer día (en real, un corte de conexión y un broker que no reenvía todo al reconectar; los
datos del nivel tienen dos casos, lección 4.5.2):

```python
perdida = next(ev.datos for ev in s["bus"].registro
               if ev.tipo == "informe"
               and ev.datos["evento"] == "parcial"
               and ev.datos["inst"] == "ORSC")
print(perdida["id_ejecucion"], perdida["lado"], perdida["cantidad"],
      perdida["precio"], perdida["comision"])
f = montar(perder={perdida["id_ejecucion"]})
o_f = f["oms"].ordenes[perdida["id"]]
print("En el OMS:", o_f["estado"], "| pedida", o_f["cantidad"],
      "| sumada", o_f["recibida"], "| acumulada", o_f["acumulada"])
for r in [r for r in f["rec"].diario
          if r["posiciones"] or r["efectivo"]][:3]:
    print(r)
```
```output
C00003.01 compra 208 232.36 0.73
En el OMS: ejecutada | pedida 416 | sumada 208 | acumulada 416
{'fecha': datetime.date(2025, 9, 2), 'posiciones': {'ORSC': 208}, 'efectivo': np.float64(-48331.61)}
{'fecha': datetime.date(2025, 9, 3), 'posiciones': {'ORSC': 208}, 'efectivo': np.float64(-48331.61)}
{'fecha': datetime.date(2025, 9, 4), 'posiciones': {'ORSC': 208}, 'efectivo': np.float64(-48331.61)}
```

El OMS marca la orden como `ejecutada`, porque así lo dice el último informe, pero de las 416 acciones pedidas solo ha
sumado 208; el informe trae la cantidad acumulada (416) y la diferencia está a la vista para quien la compare, que es
lo que hace la máquina de estados de la lección 4.5.2. Este OMS no compara, y la cartera interna se queda con 208 ORSC
menos que el broker y 48.331,61 USD más de efectivo (las 208 acciones a 232,36 más 0,73 de comisión). El
reconciliador lo ve esa misma tarde, a las 20:30 UTC, y todos los días siguientes.

```python
def ordenes_de(sim, fecha, inst):
    return [(o["lado"], o["cantidad"], o["estado"])
            for o in sim["oms"].ordenes.values()
            if o["fecha"] == pd.Timestamp(fecha)
            and o["inst"] == inst]

for dia in ("2025-09-03", "2025-09-04", "2025-09-05"):
    print(dia, "sin fallo:", ordenes_de(s, dia, "ORSC"),
          "| con fallo:", ordenes_de(f, dia, "ORSC"))
print("ORSC al final, broker y cartera interna:",
      f["broker"].pos["ORSC"], f["cartera"].pos["ORSC"],
      "| sistema sano:", s["broker"].pos["ORSC"])
fantasma = (f["broker"].pos["ORSC"] - f["cartera"].pos["ORSC"]) \
    * precios.loc[SESIONES[-1], "ORSC"]
print(f"Valor de la posición fantasma: {fantasma:,.0f} USD; días "
      f"con descuadre: {sum(bool(r['posiciones'])
                            for r in f['rec'].diario)}")
```
```output
2025-09-03 sin fallo: [] | con fallo: [('compra', 205, 'rechazada')]
2025-09-04 sin fallo: [('compra', 53, 'ejecutada')] | con fallo: [('compra', 260, 'rechazada')]
2025-09-05 sin fallo: [('venta', 59, 'ejecutada')] | con fallo: [('compra', 201, 'ejecutada')]
ORSC al final, broker y cartera interna: 208 0 | sistema sano: 0
Valor de la posición fantasma: 57,716 USD; días con descuadre: 85
```

Con el fallo, el 3 de septiembre el OMS intenta comprar 205 ORSC que el sistema sano no compra: está reponiendo lo
que cree que falta. El broker la rechaza, porque ese efectivo ya se gastó en las acciones que el sistema no sabe que
tiene; el 4 lo intenta con 260 y vuelve a ser rechazada, y el 5 la compra de 201 sí entra, mientras el sistema sano
vende 59. El 31 de diciembre el sistema sano no tiene ORSC; el que perdió el
mensaje tiene 208 en el broker y 0 en su cartera: una **posición fantasma** de 57.716 USD al cierre del año, más del 5 % del patrimonio, que ninguna regla de
la estrategia ha decidido y que ningún stop vigila, porque el sistema no sabe que existe. Lo único que avisó, 85
veces, fue el reconciliador. La lección práctica es el orden de las cosas: **reconciliar antes de operar**. Un
descuadre sin explicar debe parar el rebalanceo de ese instrumento (o de todo) hasta que una persona o un proceso de
recuperación lo resuelva; la lección 4.5.5 programa esa recuperación: consultar al broker las ejecuciones del día,
corregir la cartera y arrancar con el estado bueno.

## Reproducible evento a evento

El bus no usa el reloj del ordenador ni hilos, así que el mismo flujo de entrada produce el mismo registro de
eventos. Se comprueba con una huella del registro completo:

```python
def huella(sim):
    h = hashlib.sha256()
    for ev in sim["bus"].registro:
        datos = {k: v for k, v in ev.datos.items()
                 if k not in ("hist", "px", "pesos", "ref")}
        h.update(f"{ev.t.isoformat()}|{ev.tipo}|{datos}".encode())
    return h.hexdigest()[:16]

print(huella(s), huella(montar()), huella(f))
```
```output
c62d14133b310f84 c62d14133b310f84 41fa5d84a31624d5
```

Las dos ejecuciones sanas dan la misma huella; la del fallo, otra. Es la propiedad que el research del sitio sobre
máquinas de estados llama «el replay como test de oro»: si el registro de un día de producción se puede volver a
pasar por el sistema y da lo mismo, cualquier diferencia entre lo que pasó y lo que debía pasar está en las entradas
(datos, mensajes del broker), no en el código. Guardar ese registro, en un fichero de solo añadir o en una base de
datos, es la base del estado persistente de la lección 4.5.5.

## Los mismos componentes en backtest, paper y real

Lo que cambia al pasar al real está en los bordes:

```mermaid
flowchart TB
  N[Núcleo común] --> RL[Reloj lógico]
  N --> RR[Reloj real]
  RL --> DH[Datos históricos]
  RR --> DS[Datos streaming]
  DH --> BS[Broker simulado]
  DS --> BP[Cuenta paper]
  DS --> BR[Cuenta real]
```

| Pieza | Backtest | Paper | Real |
|---|---|---|---|
| Reloj | Lógico, salta de evento en evento | Real | Real |
| Datos | Históricos | En tiempo real | En tiempo real |
| Broker | Simulado en el proceso | Cuenta paper del broker | Cuenta real |
| Señal, riesgo, OMS, cartera, reconciliación | Mismo código | Mismo código | Mismo código |

En real, el bus es el mismo pero lo alimenta un adaptador que recibe mensajes del broker y del proveedor de datos y
los publica con la hora de llegada. Un esquema con `asyncio`, sin ejecutar (las conexiones reales son las de las
lecciones 4.4.2 a 4.4.4):

```python skip title="adaptador_en_vivo.py"
import asyncio
from datetime import datetime, timezone

async def adaptador(bus, flujo_broker, flujo_datos):
    """Publica en el bus cada mensaje con su hora de llegada."""
    async def bombear(flujo, tipo):
        async for msg in flujo:
            ahora = pd.Timestamp(datetime.now(timezone.utc))
            bus.publicar(ahora, tipo, **msg)
            bus.correr()          # procesa lo que haya en la cola
    await asyncio.gather(bombear(flujo_broker, "informe"),
                         bombear(flujo_datos, "precios"))
```

Hay sistemas de un proceso, como este, y sistemas de varios procesos que se hablan por una cola de mensajes. Para una
estrategia diaria, un proceso basta y es más fácil de razonar: los eventos tienen un orden total y el estado está en
un sitio. Varios procesos tienen sentido cuando una pieza es lenta (un modelo pesado) o cuando hay muchas estrategias
que comparten datos y broker; a cambio, el orden de los eventos entre procesos ya no está garantizado y hay que
reconciliar más. Dónde corre el proceso (un servidor propio, una VPS, un contenedor) cambia la latencia y la
disponibilidad, no la arquitectura; el research del sitio sobre Docker explica cómo empaquetarlo.

## Una orden real de punta a punta

Los datos del nivel traen el registro de un sistema con esta misma arquitectura operando cuatro meses (paper hasta el
10 de octubre y cuenta real después). La orden `R00071` es una venta a mercado de 243 ORUS del 28 de octubre:

```python
ev = pd.read_csv("vivo4_eventos_broker.csv")
o = ordenes.set_index("id_orden_cliente").loc["R00071"]
t0 = pd.Timestamp(o["ts_creacion"])
ms = lambda ts: round((pd.Timestamp(ts) - t0).total_seconds() * 1e3)
print(o["id_senal"], o["instrumento"], o["lado"], o["cantidad"],
      o["tipo"], "| envío:", ms(o["ts_envio"]), "ms")
r = ev[ev["id_orden_cliente"] == "R00071"]
pd.DataFrame({"seq": r["seq"], "evento": r["evento"],
              "broker_ms": r["ts_broker"].map(ms),
              "recibido_ms": r["ts_recibido"].map(ms),
              "cantidad": r["cantidad_ultima"],
              "precio": r["precio_ultimo"]}).set_index("seq")
```
```output
S00180 ORUS venta 243 mercado | envío: 277 ms
        evento  broker_ms  recibido_ms  cantidad  precio
seq
176   aceptada        331          378         0     NaN
177    parcial        355          392       122  658.09
178    parcial        668          708        12  658.06
180  ejecutada       1300         1329       109  658.18
```

La señal salió a las 19:45 UTC; el OMS creó la orden cinco minutos después (es la regla del sistema) y la envió 277 ms
más tarde. El broker la aceptó a los 331 ms y la llenó en tres ejecuciones en algo más de un segundo; cada mensaje
tarda entre 30 y 50 ms más en llegar al sistema. Es la figura de la cabecera. Dos observaciones para las lecciones
siguientes: el sistema se enteró de la primera ejecución (392 ms) solo 14 ms después de la aceptación (378 ms), y en
otras órdenes de los datos la ejecución llega **antes** que la aceptación (lección 4.5.2); y el precio medio de las
tres ejecuciones frente al precio medio del mercado al enviar es el deslizamiento de la lección 4.5.3.

## Ejercicios

1. Activa el interruptor de riesgo el 1 de octubre (sin cambiar nada más) y cuenta cuántas sesiones quedan sin
   objetivos aprobados y cuántas alertas salen. ¿Qué le pasa a la cartera mientras el interruptor está activado?

**Solución.**

```python
class RiesgoConFecha(Riesgo):
    def revisar(self, ev):
        self.interruptor = ev.datos["fecha"] >= pd.Timestamp(
            "2025-10-01")
        super().revisar(ev)

def montar_con(riesgo_cls, capital=1_000_000.0):
    bus = Bus()
    riesgo = riesgo_cls(bus)
    cartera = Cartera(bus, capital)
    broker = BrokerSimulado(bus, capital)
    oms = OMS(bus, riesgo, cartera)
    Senal(bus)
    alertas = []
    bus.suscribir("alerta", lambda e: alertas.append(e.datos["texto"]))
    Datos(bus, precios).programar(SESIONES)
    bus.correr()
    return oms, cartera, alertas

oms_i, cart_i, al_i = montar_con(RiesgoConFecha)
print("alertas:", Counter(al_i))
print("órdenes:", len(oms_i.ordenes), "frente a",
      len(s["oms"].ordenes))
print(cart_i.pos.to_dict())
```
```output
alertas: Counter({'interruptor': 64})
órdenes: 68 frente a 350
{'ORUS': 201, 'ORSC': 397, 'OREU': 1835, 'OREM': 1765, 'ORTN': 1360, 'ORTL': 1474, 'ORIG': 1188, 'ORAU': 309, 'ORCM': 0, 'ORRE': 961}
```

Desde el 1 de octubre el riesgo no aprueba ningún objetivo: sale una alerta de interruptor en cada una de las 64
sesiones y el OMS no manda más órdenes (68 en total, todas de septiembre, frente a 350). La cartera se queda congelada con las posiciones del 30 de septiembre: el interruptor de esta lección
deja de operar, no liquida. Que además deba cancelar órdenes abiertas o cerrar posiciones es una decisión de diseño
con su propio coste (lección 4.2.5).

2. Baja el valor máximo por orden al 10 % del patrimonio. ¿Cuántas órdenes se bloquean y cuándo? ¿Qué consecuencia
   tiene en los pesos de la cartera los primeros días?

**Solución.**

```python
class RiesgoEstricto(Riesgo):
    def __init__(self, bus):
        super().__init__(bus, valor_max=0.10)

oms_e, cart_e, al_e = montar_con(RiesgoEstricto)
bloq = pd.Series([o["fecha"] for o in oms_e.ordenes.values()
                  if o["estado"] == "bloqueada"])
print(len(bloq), "bloqueadas en", bloq.nunique(), "de",
      len(SESIONES), "sesiones")
print("Primeros días:", bloq.dt.date.value_counts().sort_index()
      .head(3).to_dict())
fin = precios.loc[SESIONES[-1]]
pesos = lambda c: (c.pos * fin / c.patrimonio(fin)).round(3)
pd.DataFrame({"límite 30 %": pesos(s["cartera"]),
              "límite 10 %": pesos(cart_e)}).T
```
```output
356 bloqueadas en 82 de 85 sesiones
Primeros días: {datetime.date(2025, 9, 2): 6, datetime.date(2025, 9, 3): 6, datetime.date(2025, 9, 4): 5}
              ORUS   ORSC  OREU  OREM  ORTN  ORTL  ORIG   ORAU   ORCM   ORRE
límite 30 %  0.000  0.000   0.0   0.0   0.0   0.0   0.0  0.109  0.000  0.000
límite 10 %  0.108  0.107   0.0   0.0   0.0   0.0   0.0  0.108  0.112  0.109
```

Con un 10 %, el límite bloquea 356 órdenes en 82 de las 85 sesiones: el OMS repite cada día la orden que no pasó.
Los primeros días no puede montar la cartera (6, 6 y 5 bloqueos), y lo peor llega después. Las posiciones se
construyen a trozos por debajo del 10 %, pero cuando la señal pide salir, la venta de toda la posición vale más del
10 % y también se bloquea: el 31 de diciembre el sistema estricto tiene cerca de un 11 % en ORUS, ORSC, ORCM y ORRE,
que la señal quiere a cero. Un límite pensado para frenar errores impide reducir el riesgo. Por eso se calibra con el
historial y, en muchas mesas, las órdenes que reducen la posición tienen límites distintos de las que la aumentan.

3. Explica qué haría falta cambiar en la pieza de datos para que la señal del sistema funcione bien el 17 de
   noviembre, día del desdoblamiento 3 por 1 de ORAU, si el proveedor diera precios sin ajustar.

**Solución.**

Con precios sin ajustar, el cierre de ORAU del 17 de noviembre sería un tercio del de la víspera: el momentum de 20
sesiones se volvería muy negativo de golpe y la volatilidad de 63 sesiones se dispararía, así que la señal vendería
todo ORAU sin ningún motivo económico. La pieza de datos tiene que publicar series ajustadas por eventos
corporativos (lección 1.2.1), y la cartera tiene que multiplicar por 3 la posición y dividir por 3 su precio de coste
ese mismo día; si no, la reconciliación con el broker dará un descuadre de 374 títulos (lección 4.5.5). Lo importante
es que el ajuste se hace en un sitio (la pieza de datos y el registro de eventos corporativos), no repartido por la
señal.

## Autoevaluación

1. ¿Por qué el bus de la lección ordena los eventos por una hora lógica en vez de usar `datetime.now()` y `time.sleep()`?

   - A. Porque es más rápido
   - B. Porque así el mismo flujo de eventos da siempre el mismo resultado: se puede repetir un día entero, en backtest o tras un fallo, y comparar evento a evento
   - C. Porque los brokers no admiten horas reales
   - D. Porque `sleep` no existe en Python 3.14

   *Respuesta B: Porque así el mismo flujo de eventos da siempre el mismo resultado: se puede repetir un día entero, en backtest o tras un fallo, y comparar evento a evento.* Con el reloj inyectado, el orden de los eventos solo depende de sus horas lógicas y del orden de publicación. En la lección, dos ejecuciones completas dan la misma huella SHA-256 del registro de eventos. En real, el reloj lo pone el adaptador de datos; el resto de piezas no cambia.

2. El sistema pierde la ejecución de 208 ORSC el 2 de septiembre. ¿Qué pieza lo detecta y cuándo?

   - A. La señal, al día siguiente
   - B. El OMS, al instante
   - C. El reconciliador, al cierre de ese día, al comparar la cartera interna con la del broker
   - D. Nadie: la diferencia es demasiado pequeña

   *Respuesta C: El reconciliador, al cierre de ese día, al comparar la cartera interna con la del broker.* El OMS da la orden por `ejecutada` (lo dice el último informe) y la cartera interna no sabe nada. El reconciliador compara posiciones y efectivo con los del broker a las 20:30 UTC y encuentra 208 ORSC y 48.331,61 USD de diferencia. Si nadie actúa, el OMS intenta comprar lo que cree que falta y el sistema acaba con una posición fantasma.

3. ¿Qué control previo a la orden habría parado una orden con diez veces la cantidad prevista (un error de dedo)?

   - A. Un límite al valor máximo de cada orden, calibrado por encima de la mayor orden legítima
   - B. Un stop de pérdidas
   - C. La reconciliación del cierre
   - D. Ninguno: el broker la rechaza siempre

   *Respuesta A: Un límite al valor máximo de cada orden, calibrado por encima de la mayor orden legítima.* Es el control b) del artículo 15 del Reglamento Delegado (UE) 2017/589. En el historial de 398 órdenes del sistema la mayor vale el 26,1 % del capital; un límite del 30 % no bloquea ninguna legítima y para la orden multiplicada por diez.

4. ¿Qué cambia entre backtest, paper y real en la arquitectura de la lección?

   - A. Todo: son tres programas distintos
   - B. La señal y el riesgo
   - C. Solo los adaptadores de los bordes: de dónde viene el reloj y los datos, y a qué broker van las órdenes
   - D. Nada, ni siquiera el broker

   *Respuesta C: Solo los adaptadores de los bordes: de dónde viene el reloj y los datos, y a qué broker van las órdenes.* Señal, riesgo, OMS, cartera y reconciliador son el mismo código. Cambia el reloj (lógico o real), la fuente de datos (histórica o en streaming) y el broker (simulado, cuenta paper o cuenta real). Es la idea de NautilusTrader y LEAN (lecciones 2.4.5 y 4.5.6).

5. ¿Por qué la señal de la lección usa los precios hasta la víspera aunque se calcule 15 minutos antes del cierre?

   - A. Por comodidad
   - B. Porque el cierre del día aún no existe a esa hora: en el bus, el evento `cierre` se publica a la hora del cierre y ninguna pieza puede leerlo antes
   - C. Porque así el Sharpe es mayor
   - D. Porque el broker lo exige

   *Respuesta B: Porque el cierre del día aún no existe a esa hora: en el bus, el evento `cierre` se publica a la hora del cierre y ninguna pieza puede leerlo antes.* En un sistema por eventos, la información tiene hora. Lo que un componente sabe es lo que le ha llegado. Es la misma protección contra el sesgo de anticipación que en la lección 2.4.2, ahora con la hora real de cada mensaje.

## Ideas clave

- La cadena de un sistema en vivo tiene seis piezas: datos, señal, riesgo, OMS, broker y reconciliación. Cada una tiene una sola responsabilidad y habla con las demás por mensajes con hora.
- Un bus con reloj lógico hace el sistema reproducible: el registro de eventos de dos ejecuciones tiene la misma huella, y un día de producción se puede repetir en el laboratorio.
- Los controles previos a la orden (valor máximo, volumen, número de mensajes, interruptor) son obligatorios para las empresas que hacen negociación algorítmica en la UE y sensatos para cualquiera; se calibran con el historial de órdenes legítimas.
- El OMS sabe lo que pidió; solo el broker sabe lo que pasó. La reconciliación diaria entre los dos es la única pieza que descubre un mensaje perdido.
- Un descuadre que nadie corrige no se queda quieto: el OMS opera sobre una posición falsa y el error se convierte en órdenes.
- Backtest, paper y real comparten el núcleo; cambian el reloj, los datos y el broker.

## Recursos

### Research

- [Máquinas de Estado Finito para Ejecución de Trading Cuantitativo en Python](https://pythonparatrading.com/maquinas-estado-finito-trading-python/) · Research-0029 · 49 min
- [Docker para trading](https://pythonparatrading.com/docker-para-trading/) · Research-0013 · 4 min

### Documentación

- [Reglamento Delegado (UE) 2017/589 (RTS 6): requisitos de la negociación algorítmica (DOUE, PDF en el BOE)](https://www.boe.es/doue/2017/087/L00417-00448.pdf) · boe.es
- [Python: heapq, cola de prioridad](https://docs.python.org/3/library/heapq.html) · docs.python.org

### Datos

- [Datos del nivel 4: señales del sistema en vivo (CSV)](https://pythonparatrading.com/curso/datos/nivel-4/vivo4_senales.csv) · Descarga · vivo4_senales.csv
- [Datos del nivel 4: órdenes del OMS (CSV)](https://pythonparatrading.com/curso/datos/nivel-4/vivo4_ordenes.csv) · Descarga · vivo4_ordenes.csv

Lección anterior: [4.4.5 · Credenciales seguras: variables de entorno, gestores de secretos y permisos mínimos](https://pythonparatrading.com/curso-python-trading/nivel-4/credenciales-seguras-y-permisos-minimos/)

Lección siguiente: [4.5.2 · Gestión de órdenes: estados, fills parciales, cancelaciones y reconexiones](https://pythonparatrading.com/curso-python-trading/nivel-4/gestion-de-ordenes-estados-fills-y-reconexiones/)
