Lección 0.3.4 · 50 min

Control de flujo: if, elif, else, for, while y match-case

Una regla de trading es una decisión que se repite en cada barra. La lección enseña a escribir decisiones con if, a repetirlas con for y while y a despachar órdenes de distintos tipos con match, con los precios diarios de SINT.

Fig. — Trailing stop del 5 % sobre SINT: entrada el 1 de noviembre de 2023 y salida el 23 de febrero de 2024
04/06
Módulo
0.3 · Fundamentos del lenguaje
Duración
50 min
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

Un programa que decide y repite

Hasta ahora el código se ejecutaba de arriba abajo, una línea tras otra. Una regla de trading necesita dos cosas más: decidir («si el cierre está por encima de la media, comprar») y repetir («para cada sesión, comprobar si saltó el stop»). Las instrucciones de control de flujo cambian el orden en que se ejecuta el código: if elige entre caminos, for y while repiten y match elige según la forma de un dato.

Los ejemplos usan las barras diarias de SINT. Las tres primeras líneas de la celda leen el fichero con el módulo csv de la biblioteca estándar; los ficheros se ven en la lección 0.4.4 y pandas, que lo hace en una línea, en el módulo 0.5. Lo importante es el resultado: una lista de tuplas (fecha, apertura, máximo, mínimo, cierre), una por sesión, como las de la lección 0.3.3.

IN [01]python
import csv

with open("sint_diario.csv", encoding="utf-8") as f:
    filas = list(csv.DictReader(f))
barras = [(r["fecha"], float(r["apertura"]), float(r["maximo"]),
           float(r["minimo"]), float(r["cierre"])) for r in filas]
print(len(barras))
print(barras[-1])
OUT [01]Barras de SINT como lista de tuplas
1303
('2025-12-31', 140.01, 143.4, 139.67, 142.22)

Decidir con if, elif y else

Una barra es alcista si cierra por encima de su apertura, bajista si cierra por debajo y doji si el cuerpo (la distancia entre apertura y cierre) es muy pequeño respecto al rango del día. Con un umbral del 10 % del rango, la clasificación es una cadena de condiciones:

IN [02]python
fecha, apertura, maximo, minimo, cierre = barras[-1]
cuerpo = abs(cierre - apertura)
rango = maximo - minimo

if rango == 0 or cuerpo <= 0.10 * rango:
    tipo = "doji"
elif cierre > apertura:
    tipo = "alcista"
else:
    tipo = "bajista"
print(fecha, tipo, f"cuerpo {cuerpo:.2f} / rango {rango:.2f}")
OUT [02]Clasificar una barra
2025-12-31 alcista cuerpo 2.21 / rango 3.73

Cuatro reglas de la sintaxis:

  • La condición termina en dos puntos, y el código que depende de ella va sangrado cuatro espacios. En Python la sangría no es estética: es lo que dice qué líneas pertenecen a cada rama. Una línea mal sangrada cambia el programa.
  • Se ejecuta solo la primera rama cuya condición sea verdadera. Por eso el doji va primero: una barra con un cuerpo mínimo y el cierre por encima de la apertura también cumpliría cierre > apertura.
  • elif («else if») y else son opcionales; puede haber tantos elif como haga falta y un solo else, al final.
  • rango == 0 trata aparte una barra sin rango (un día sin negociación): se clasifica como doji sin más cálculos. or deja de evaluar en cuanto encuentra algo verdadero.

Cuando la decisión solo elige entre dos valores, la expresión condicional lo hace en una línea:

IN [03]python
lado = "largo"
signo = 1 if lado == "largo" else -1
pnl = signo * (cierre - 139.98) * 100
print(signo, f"{pnl:+.2f}")
OUT [03]Expresión condicional
1 +224.00

Las condiciones no tienen por qué ser comparaciones. En un if, una colección vacía, el 0, la cadena vacía y None cuentan como falso, y todo lo demás como verdadero. if not ordenes_pendientes: se lee «si no hay órdenes pendientes». Para comprobar si un valor falta, la forma correcta es if stop is None:, no if not stop:: un stop en 0 no es lo mismo que no tener stop.

Repetir con for

for recorre una colección y ejecuta su bloque una vez por elemento. Con range(n) recorre los números de 0 a n−1, y con enumerate da a la vez la posición y el elemento:

IN [04]python
conteo = {"alcista": 0, "bajista": 0, "doji": 0}
for fecha, apertura, maximo, minimo, cierre in barras:
    if not fecha.startswith("2025"):
        continue
    rango = maximo - minimo
    if rango == 0 or abs(cierre - apertura) <= 0.10 * rango:
        conteo["doji"] += 1
    elif cierre > apertura:
        conteo["alcista"] += 1
    else:
        conteo["bajista"] += 1
print(conteo, sum(conteo.values()))
OUT [04]Contar barras de cada tipo en 2025
{'alcista': 124, 'bajista': 109, 'doji': 28} 261

El desempaquetado de la lección 0.3.3 funciona en la cabecera del for: cada tupla se reparte en cinco variables. continue salta al siguiente elemento sin ejecutar el resto del bloque: aquí descarta las barras que no son de 2025. El diccionario conteo acumula el resultado, una práctica habitual para contar por categorías.

Un for recorre la colección entera salvo que algo lo corte. break sale del bucle en el acto, y es lo que se usa para buscar la primera vez que pasa algo. Un for admite además un else, que se ejecuta solo si el bucle acabó sin break, es decir, si no se encontró lo que se buscaba.

Stop y objetivo: break y el else del bucle

Una posición larga con stop y objetivo se cierra en la primera sesión en la que el precio toca uno de los dos. Con barras diarias, el stop se toca si el mínimo del día llega al stop, y el objetivo si el máximo llega al objetivo. El orden de las comprobaciones importa:

sínosínoSiguiente barra¿Mínimo ≤ stop?Salir en el stop¿Máx. ≥ objetivo?Salir en objetivo
Fuente Mermaid
flowchart TB
  B[Siguiente barra] --> S{¿Mínimo ≤ stop?}
  S -- sí --> XS[Salir en el stop]
  S -- no --> O{¿Máx. ≥ objetivo?}
  O -- sí --> XO[Salir en objetivo]
  O -- no --> B

El stop se comprueba primero por una razón de prudencia. Si en la misma barra el mínimo toca el stop y el máximo toca el objetivo, los datos diarios no dicen qué pasó antes. Suponer que fue el objetivo es elegir el resultado favorable, y un backtest que siempre elige lo favorable se infla; el supuesto conservador es el stop.

La entrada del ejemplo es una compra al cierre del 2 de mayo de 2025, con un stop un 3 % por debajo y un objetivo un 6 % por encima:

IN [05]python
indice = {b[0]: i for i, b in enumerate(barras)}
i_entrada = indice["2025-05-02"]
entrada = barras[i_entrada][4]
stop, objetivo = entrada * 0.97, entrada * 1.06
print(f"Entrada {entrada:.2f}  stop {stop:.2f}  "
      f"objetivo {objetivo:.2f}")

for i, (fecha, ap, mx, mn, ci) in enumerate(barras):
    if i <= i_entrada:
        continue
    if mn <= stop:
        print(f"{fecha}: stop tocado (mínimo {mn:.2f})")
        break
    elif mx >= objetivo:
        print(f"{fecha}: objetivo tocado (máximo {mx:.2f})")
        break
else:
    print("Ni stop ni objetivo: posición abierta")
print("Sesiones en posición:", i - i_entrada)
OUT [05]Primera sesión que toca el stop o el objetivo
Entrada 142.51  stop 138.23  objetivo 151.06
2025-05-07: stop tocado (mínimo 134.69)
Sesiones en posición: 3

La posición dura tres sesiones: el 7 de mayo el mínimo, 134,69, atraviesa el stop de 138,23. Esa sesión abrió en 139,22, por encima del stop, así que la venta se habría hecho en el stop y no más abajo.

La primera línea es una comprensión de diccionario (lección 0.3.3) que asocia cada fecha a su posición en la lista: así se encuentra la sesión de entrada sin recorrerla a mano. En la cabecera del for, enumerate da la posición i y la tupla, que se desempaqueta entre paréntesis. Las sesiones anteriores a la entrada se saltan con continue; la de la propia entrada también, porque la compra se hace a su cierre y el mínimo y el máximo de ese día ya habían pasado.

Trailing stop con while

Un trailing stop sube con el precio y nunca baja: se sitúa a un porcentaje fijo por debajo del cierre más alto desde la entrada. Protege parte de la ganancia sin poner un objetivo. No se sabe de antemano cuántas sesiones durará la posición, y eso es lo que expresa un bucle while: repite su bloque mientras la condición sea verdadera.

IN [06]python
i = indice["2023-11-01"]
entrada = barras[i][4]
maximo_cierre = entrada
trailing = entrada * 0.95
abierta = True

while abierta and i + 1 < len(barras):
    i += 1
    fecha, ap, mx, mn, ci = barras[i]
    if mn <= trailing:
        salida = min(ap, trailing)   # si abre por debajo, a la apertura
        abierta = False
    else:
        maximo_cierre = max(maximo_cierre, ci)
        trailing = max(trailing, maximo_cierre * 0.95)

print(f"Entrada {entrada:.2f} el {barras[indice['2023-11-01']][0]}")
print(f"Salida {salida:.2f} el {fecha}, "
      f"máximo cierre {maximo_cierre:.2f}")
print(f"Resultado {salida / entrada - 1:+.2%} "
      f"en {i - indice['2023-11-01']} sesiones")
OUT [06]Trailing stop del 5 % desde noviembre de 2023
Entrada 115.27 el 2023-11-01
Salida 126.71 el 2024-02-23, máximo cierre 133.38
Resultado +9.93% en 82 sesiones

La posición se mantiene 82 sesiones. El cierre más alto llega a 133,38 y arrastra el stop hasta 126,71; el 23 de febrero de 2024 el mínimo, 126,55, lo toca, y la salida deja un +9,93 %. La figura de la cabecera dibuja el recorrido: el cierre de SINT, el trailing stop que lo sigue en escalones y la salida en ámbar.

Tres detalles del bucle. La condición tiene dos partes: la posición sigue abierta y quedan barras; sin la segunda, si el stop no saltara nunca, barras[i] acabaría pidiendo una barra que no existe. El trailing solo se actualiza con el cierre después de comprobar el mínimo, para no usar el cierre de un día para decidir si saltó el stop ese mismo día. Y min(ap, trailing) aplica el aviso de antes: si la sesión abre por debajo del stop, se vende a la apertura.

while frente a for: si se recorre una colección conocida, for; si se repite hasta que pase algo, while. Un while cuya condición nunca se vuelve falsa es un bucle infinito: el programa se queda colgado. En un sistema que opera, cada while necesita una salida segura (un número máximo de intentos, un tiempo límite).

Despachar órdenes con match-case

Un sistema recibe órdenes de varios tipos, y cada tipo necesita campos distintos: una orden de mercado, lado y cantidad; una límite, además, un precio límite; una stop, un precio de activación. Con if/elif habría que comprobar el tipo y después, uno a uno, que estén los campos. match-case (Python 3.10) compara la forma del dato con una serie de patrones y, si encaja, asigna sus partes a variables:

IN [07]python
ordenes = [
    {"tipo": "mercado", "lado": "compra", "cantidad": 100},
    {"tipo": "limite", "lado": "venta", "cantidad": 50,
     "precio": 145.20},
    {"tipo": "stop", "lado": "venta", "cantidad": 100,
     "precio": 135.00},
    {"tipo": "limite", "lado": "compra", "cantidad": 10,
     "precio": -1},
    {"tipo": "iceberg", "lado": "compra", "cantidad": 5000},
]

for orden in ordenes:
    match orden:
        case {"tipo": "mercado", "lado": lado, "cantidad": q}:
            print(f"MERCADO {lado} {q}")
        case {"tipo": "limite", "lado": lado, "cantidad": q,
              "precio": p} if p > 0:
            print(f"LÍMITE  {lado} {q} a {p:.2f}")
        case {"tipo": "stop", "lado": "venta", "cantidad": q,
              "precio": p}:
            print(f"STOP    venta {q} si cae a {p:.2f}")
        case {"tipo": "limite" | "stop"}:
            print(f"RECHAZADA: precio o lado no válidos {orden}")
        case _:
            print(f"RECHAZADA: tipo desconocido {orden['tipo']!r}")
OUT [07]Validar órdenes con patrones
MERCADO compra 100
LÍMITE  venta 50 a 145.20
STOP    venta 100 si cae a 135.00
RECHAZADA: precio o lado no válidos {'tipo': 'limite', 'lado': 'compra', 'cantidad': 10, 'precio': -1}
RECHAZADA: tipo desconocido 'iceberg'

Cómo leer los patrones:

  • Un patrón de diccionario encaja si el dato tiene al menos esas claves con valores que encajen. Las claves de más no importan.
  • Un valor literal ("mercado", "venta") tiene que coincidir; un nombre (lado, q, p) captura el valor que haya y lo deja en esa variable para la rama.
  • if p > 0 tras el patrón es una guarda: el caso solo vale si, además de encajar, la condición se cumple. La orden límite con precio −1 encaja en la forma, falla en la guarda y sigue bajando.
  • "limite" | "stop" encaja con cualquiera de los dos valores. Este caso recoge las límite y stop que no pasaron los anteriores (como un stop de compra, que este sistema no admite).
  • case _ encaja con todo: es el else de match. Sin él, una orden que no encaja en nada se ignora en silencio.

Los casos se prueban en orden y solo se ejecuta el primero que encaja, igual que en if/elif. Por eso los patrones generales ("limite" | "stop", _) van al final: puestos al principio, se tragarían las órdenes válidas.

Ejercicios

  1. Cuenta cuántas sesiones de 2024 cerraron con una subida de más del 2 % respecto al cierre anterior, y cuál fue la mayor subida del año con su fecha.
Solución
IN [08]python
grandes = 0
mayor, fecha_mayor = 0.0, None
for i in range(1, len(barras)):
    fecha, cierre = barras[i][0], barras[i][4]
    if not fecha.startswith("2024"):
        continue
    r = cierre / barras[i - 1][4] - 1
    if r > 0.02:
        grandes += 1
    if r > mayor:
        mayor, fecha_mayor = r, fecha
print(grandes, fecha_mayor, f"{mayor:+.2%}")
OUT [08]Subidas de más del 2 % en 2024
3 2024-07-17 +2.50%

range(1, len(barras)) empieza en 1 porque la primera barra no tiene cierre anterior. La rentabilidad de la primera sesión de 2024 usa el último cierre de 2023, que es lo correcto.

  1. Repite el trailing stop con un 8 % en lugar de un 5 %, desde la misma entrada. ¿Cuándo sale y con qué resultado? ¿Por qué cambia el número de sesiones?
Solución
IN [09]python
i = indice["2023-11-01"]
entrada = barras[i][4]
maximo_cierre, trailing, abierta = entrada, entrada * 0.92, True
while abierta and i + 1 < len(barras):
    i += 1
    fecha, ap, mx, mn, ci = barras[i]
    if mn <= trailing:
        salida, abierta = min(ap, trailing), False
    else:
        maximo_cierre = max(maximo_cierre, ci)
        trailing = max(trailing, maximo_cierre * 0.92)
print(fecha, f"{salida:.2f}", f"{salida / entrada - 1:+.2%}",
      i - indice["2023-11-01"])
OUT [09]Trailing stop del 8 %
2024-03-08 122.71 +6.45% 92

Con un 8 %, la posición sale el 8 de marzo de 2024, tras 92 sesiones, con un +6,45 % frente al +9,93 % del 5 %. Un trailing más ancho aguanta más ruido antes de saltar: la posición dura más sesiones, pero sale más lejos del máximo. No hay un porcentaje «correcto»; elegirlo mirando el resultado de un solo tramo de datos es sobreajuste (nivel 2).

  1. Añade al match de las órdenes un caso para órdenes de mercado con una cantidad que no sea positiva, y comprueba que rechaza {"tipo": "mercado", "lado": "compra", "cantidad": 0}. ¿Dónde tiene que ir el caso nuevo?
Solución
IN [10]python
orden = {"tipo": "mercado", "lado": "compra", "cantidad": 0}
match orden:
    case {"cantidad": q} if q <= 0:
        print(f"RECHAZADA: cantidad {q}")
    case {"tipo": "mercado", "lado": lado, "cantidad": q}:
        print(f"MERCADO {lado} {q}")
    case _:
        print("RECHAZADA")
OUT [10]Rechazar cantidades no positivas
RECHAZADA: cantidad 0

Tiene que ir antes del caso de mercado: si fuera después, la orden encajaría primero en {"tipo": "mercado", …} y se enviaría con cantidad 0. Al no nombrar el tipo, el caso nuevo protege también las órdenes límite y stop.

Respuesta plegada en cada pregunta

Autoevaluación

  1. Con cierre = 101 y apertura = 100, un if cierre > apertura imprime «alcista», el elif cierre > 100 siguiente imprime «sobre 100» y el else, «otra». ¿Qué se imprime?

    • Aalcista y sobre 100
    • Balcista
    • Csobre 100
    • Dotra
    Ver respuesta

    Respuesta B. alcista

    En una cadena if/elif/else solo se ejecuta la primera rama cuya condición se cumple. Aunque cierre > 100 también sea verdad, el elif ni se evalúa.

  2. En un bucle for que busca el día en que el mínimo toca el stop, ¿cuándo se ejecuta el bloque else del for?

    • ASiempre, al terminar el bucle
    • BCuando el bucle termina sin pasar por break: el stop no se tocó en ningún día
    • CCuando se ejecuta break
    • DNunca: for no admite else
    Ver respuesta

    Respuesta B. Cuando el bucle termina sin pasar por break: el stop no se tocó en ningún día

    El else de un bucle se ejecuta si el bucle recorre todos los elementos sin break. Es la forma de decir «no lo encontré» sin una variable bandera.

  3. Una barra tiene el mínimo por debajo del stop y el máximo por encima del objetivo. Con datos diarios, ¿qué supone un backtest prudente?

    • AQue se tocó primero el objetivo
    • BQue se tocó primero el stop
    • CQue no se tocó ninguno
    • DQue se ejecutaron los dos
    Ver respuesta

    Respuesta B. Que se tocó primero el stop

    Con una barra diaria no se sabe qué pasó antes dentro del día. Suponer el objetivo sería elegir el resultado más favorable; la hipótesis conservadora es el stop.

  4. ¿Qué hace case {"tipo": "limite", "precio": p} if p > 0: con la orden {"tipo": "limite", "lado": "compra", "precio": 101.2}?

    • ANo encaja, porque la orden tiene una clave más (lado)
    • BEncaja y asigna p = 101.2
    • CDa un error de sintaxis
    • DEncaja solo si precio es un int
    Ver respuesta

    Respuesta B. Encaja y asigna p = 101.2

    Un patrón de diccionario exige las claves que nombra y admite claves de más. p captura el valor de precio y la guarda if p > 0 se cumple.

Ideas clave

  • if/elif/else ejecuta solo la primera rama cuya condición se cumple. La sangría (4 espacios) marca qué código pertenece a cada rama.
  • for recorre una colección; enumerate añade el índice. break sale del bucle y el else del bucle se ejecuta solo si no hubo break.
  • while repite mientras se cumpla una condición: para lo que no se sabe cuánto dura, como una posición abierta con trailing stop. Asegúrate de que la condición acaba siendo falsa.
  • Con barras diarias no se sabe si se tocó antes el stop o el objetivo: un backtest prudente supone el stop.
  • match-case compara la forma de un dato: con patrones de diccionario y guardas, valida y despacha órdenes de mercado, límite y stop en un solo bloque.

Recursos