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.
- Módulo
- 0.3 · Fundamentos del lenguaje
- Duración
- 50 min
Lecciones del nivel 0
0.1 · Bienvenida y expectativas
0.2 · Entorno de trabajo moderno
- 0.2.1 Instalación de Python con uv: versiones y ciclo de soporte
- 0.2.2 Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles
- 0.2.3 Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)
- 0.2.4 Terminal básica, rutas, variables de entorno y ficheros .env
- 0.2.5 Git y GitHub: commits, ramas, pull requests y .gitignore
- 0.2.6 Introducción a los asistentes de código con IA: prompts, revisión y tests
0.3 · Fundamentos del lenguaje
- 0.3.1 Variables, tipos y operadores; coma flotante y decimal para precios y ticks
- 0.3.2 Cadenas, f-strings y formato de números y fechas
- 0.3.3 Listas, tuplas, diccionarios, conjuntos y comprensiones
- 0.3.4 Control de flujo: if, elif, else, for, while y match-case
- 0.3.5 Funciones: argumentos, *args y **kwargs, lambdas y ámbito
- 0.3.6 Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15
0.4 · Python intermedio
- 0.4.1 Programación orientada a objetos: clases, herencia, composición y dataclasses
- 0.4.2 Tipado: type hints, mypy o ty y Pydantic para configuraciones
- 0.4.3 Iteradores, generadores, decoradores y context managers
- 0.4.4 Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias
- 0.4.5 Logging profesional y testing con pytest
- 0.4.6 Concurrencia: threading, multiprocessing, asyncio y free-threading
0.5 · Computación numérica y datos
- 0.5.1 NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios
- 0.5.2 pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()
- 0.5.3 Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles
- 0.5.4 Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas
- 0.5.5 DuckDB y SQL esencial: SELECT, JOIN, GROUP BY y funciones de ventana sobre Parquet
0.6 · Visualización
0.7 · Matemáticas y estadística mínimas
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.
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])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:
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}")2025-12-31 alcista cuerpo 2.21 / rango 3.73Cuatro 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») yelseson opcionales; puede haber tantoselifcomo haga falta y un soloelse, al final.rango == 0trata aparte una barra sin rango (un día sin negociación): se clasifica como doji sin más cálculos.ordeja 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:
lado = "largo"
signo = 1 if lado == "largo" else -1
pnl = signo * (cierre - 139.98) * 100
print(signo, f"{pnl:+.2f}")1 +224.00Las 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:
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())){'alcista': 124, 'bajista': 109, 'doji': 28} 261El 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:
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 --> BEl 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:
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)Entrada 142.51 stop 138.23 objetivo 151.06
2025-05-07: stop tocado (mínimo 134.69)
Sesiones en posición: 3La 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.
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")Entrada 115.27 el 2023-11-01
Salida 126.71 el 2024-02-23, máximo cierre 133.38
Resultado +9.93% en 82 sesionesLa 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:
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}")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 > 0tras 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 elelsedematch. 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
- 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
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%}")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.
- 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
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"])2024-03-08 122.71 +6.45% 92Con 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).
- Añade al
matchde 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
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")RECHAZADA: cantidad 0Tiene 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
Con
cierre = 101yapertura = 100, unif cierre > aperturaimprime «alcista», elelif cierre > 100siguiente imprime «sobre 100» y elelse, «otra». ¿Qué se imprime?- A
alcistaysobre 100 - B
alcista - C
sobre 100 - D
otra
Ver respuesta
Respuesta B.
alcistaEn una cadena
if/elif/elsesolo se ejecuta la primera rama cuya condición se cumple. Aunquecierre > 100también sea verdad, elelifni se evalúa.- A
En un bucle
forque busca el día en que el mínimo toca el stop, ¿cuándo se ejecuta el bloqueelsedelfor?- 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:
forno admiteelse
Ver respuesta
Respuesta B. Cuando el bucle termina sin pasar por
break: el stop no se tocó en ningún díaEl
elsede un bucle se ejecuta si el bucle recorre todos los elementos sinbreak. Es la forma de decir «no lo encontré» sin una variable bandera.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.
¿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
precioes unint
Ver respuesta
Respuesta B. Encaja y asigna
p = 101.2Un patrón de diccionario exige las claves que nombra y admite claves de más.
pcaptura el valor deprecioy la guardaif p > 0se cumple.- ANo encaja, porque la orden tiene una clave más (
Ideas clave
if/elif/elseejecuta solo la primera rama cuya condición se cumple. La sangría (4 espacios) marca qué código pertenece a cada rama.forrecorre una colección;enumerateañade el índice.breaksale del bucle y elelsedel bucle se ejecuta solo si no hubobreak.whilerepite 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-casecompara 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.