---
title: "0.3.4 Control de flujo: if, for, while y match"
description: "Decisiones y bucles con datos de SINT: clasificar barras, buscar el día en que salta un stop, un trailing stop con while y órdenes con match-case."
url: "https://pythonparatrading.com/curso-python-trading/nivel-0/control-de-flujo-if-for-while-match/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-0/control-de-flujo-if-for-while-match.png"
---

# 0.3.4 · 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.

- Curso: [Nivel 0: Python desde cero y mentalidad de datos](https://pythonparatrading.com/curso-python-trading/nivel-0/)
- Módulo: 0.3 · Fundamentos del lenguaje
- Duración: 50 min
- Requisitos: [0.3.3 · Listas, tuplas, diccionarios, conjuntos y comprensiones](https://pythonparatrading.com/curso-python-trading/nivel-0/listas-tuplas-diccionarios-y-conjuntos/)

## Al terminar esta lección

- Clasificar una barra como alcista, bajista o doji con `if`, `elif` y `else`.
- Recorrer una serie de barras con `for` y `enumerate`, y cortar el bucle con `break` en cuanto salta un stop o un objetivo.
- Simular un trailing stop con un bucle `while` y explicar cuándo usar `while` en lugar de `for`.
- Validar y despachar órdenes de mercado, límite y stop con `match`-`case` y patrones de diccionario.

## 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.

```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])
```
```output
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:

```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}")
```
```output
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:

```python
lado = "largo"
signo = 1 if lado == "largo" else -1
pnl = signo * (cierre - 139.98) * 100
print(signo, f"{pnl:+.2f}")
```
```output
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:

```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()))
```
```output
{'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:

```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:

```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)
```
```output
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.

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

Comparar el stop con el mínimo de la **misma** barra en la que se calcula la señal es sesgo de anticipación (lección
0.1.1). El bucle empieza a comprobar en la sesión siguiente a la entrada por esa razón. Y el stop no garantiza el
precio: si el mercado abre por debajo del stop, la venta se ejecuta a la apertura, más abajo.

</aside>

## 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.

```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")
```
```output
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:

```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}")
```
```output
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.

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

Rechazar es mejor que adivinar. Una orden con un precio imposible o un tipo desconocido no se corrige en el código ni
se envía «a ver qué pasa»: se rechaza y se registra. La lección 0.3.6 convierte estos rechazos en excepciones.

</aside>

## 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.**

```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%}")
```
```output
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.

2. 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.**

```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"])
```
```output
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).

3. 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.**

```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")
```
```output
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.

## 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?

   - A. `alcista` y `sobre 100`
   - B. `alcista`
   - C. `sobre 100`
   - D. `otra`

   *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`?

   - A. Siempre, al terminar el bucle
   - B. Cuando el bucle termina sin pasar por `break`: el stop no se tocó en ningún día
   - C. Cuando se ejecuta `break`
   - D. Nunca: `for` no admite `else`

   *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?

   - A. Que se tocó primero el objetivo
   - B. Que se tocó primero el stop
   - C. Que no se tocó ninguno
   - D. Que se ejecutaron los dos

   *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}`?

   - A. No encaja, porque la orden tiene una clave más (`lado`)
   - B. Encaja y asigna `p = 101.2`
   - C. Da un error de sintaxis
   - D. Encaja solo si `precio` es un `int`

   *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

### 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

### Documentación

- [Python: más herramientas de control de flujo (tutorial oficial)](https://docs.python.org/3/tutorial/controlflow.html) · docs.python.org
- [PEP 636: Structural Pattern Matching, tutorial](https://peps.python.org/pep-0636/) · peps.python.org

### Datos

- [Datos del nivel 0: precios diarios del ETF ficticio SINT (CSV)](https://pythonparatrading.com/curso/datos/nivel-0/sint_diario.csv) · Descarga · sint_diario.csv

Lección anterior: [0.3.3 · Listas, tuplas, diccionarios, conjuntos y comprensiones](https://pythonparatrading.com/curso-python-trading/nivel-0/listas-tuplas-diccionarios-y-conjuntos/)

Lección siguiente: [0.3.5 · Funciones: argumentos, *args y **kwargs, lambdas y ámbito](https://pythonparatrading.com/curso-python-trading/nivel-0/funciones-argumentos-y-ambito/)
