---
title: "0.3.4 Control flow: if, for, while and match"
description: "Decisions and loops on SINT data: classify bars, find the day a stop is hit, a trailing stop with while and orders with match-case."
url: "https://pythonparatrading.com/en/python-trading-course/level-0/python-control-flow-if-for-while-match/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-0/python-control-flow-if-for-while-match.png"
---

# 0.3.4 · Control flow: if, elif, else, for, while and match-case

> A trading rule is a decision repeated on every bar. This lesson shows how to write decisions with `if`, repeat them with `for` and `while`, and dispatch different order types with `match`, using SINT's daily prices.

- Course: [Level 0: Python from scratch and a data mindset](https://pythonparatrading.com/en/python-trading-course/level-0/)
- Module: 0.3 · Language fundamentals
- Duration: 50 min
- Prerequisites: [0.3.3 · Lists, tuples, dictionaries, sets and comprehensions](https://pythonparatrading.com/en/python-trading-course/level-0/python-lists-tuples-dicts-sets/)

## By the end of this lesson

- Classify a bar as bullish, bearish or doji with `if`, `elif` and `else`.
- Iterate over a series of bars with `for` and `enumerate`, and exit the loop with `break` as soon as a stop or a target is hit.
- Simulate a trailing stop with a `while` loop and explain when to use `while` instead of `for`.
- Validate and dispatch market, limit and stop orders with `match`-`case` and mapping patterns.

## A program that decides and repeats

So far the code ran from top to bottom, one line after another. A trading rule needs two more things: to
**decide** (“if the close is above the moving average, buy”) and to **repeat** (“for each session, check whether the
stop was hit”). **Control flow** statements change the order in which code runs: `if` chooses between paths, `for`
and `while` repeat, and `match` chooses according to the shape of a value.

The examples use SINT's daily bars. The first three lines of the cell read the file with the standard library's `csv`
module; files are covered in lesson 0.4.4, and pandas, which does this in one line, in module 0.5. What matters is
the result: a list of `(fecha, apertura, maximo, minimo, cierre)` tuples (date, open, high, low, close), one per
session, like the ones in lesson 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)
```

## Deciding with if, elif and else

A bar is **bullish** if it closes above its open, **bearish** if it closes below it, and a **doji** if the body (the
distance between open and close) is very small relative to the day's range. With a threshold of 10% of the range, the
classification is a chain of conditions:

```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 = "bullish"
else:
    tipo = "bearish"
print(fecha, tipo, f"body {cuerpo:.2f} / range {rango:.2f}")
```
```output
2025-12-31 bullish body 2.21 / range 3.73
```

Four syntax rules:

- The condition ends with a **colon**, and the code that depends on it is **indented** four spaces. In Python
  indentation is not cosmetic: it is what says which lines belong to each branch. A wrongly indented line changes the
  program.
- **Only the first branch** whose condition is true runs. That is why the doji goes first: a bar with a tiny body and
  the close above the open would also satisfy `cierre > apertura`.
- `elif` (“else if”) and `else` are optional; there can be as many `elif` as needed and a single `else`, at the end.
- `rango == 0` handles a bar with no range (a day without trading) separately: it is classified as a doji with no
  further calculation. `or` stops evaluating as soon as it finds something true.

When the decision only chooses between two values, the **conditional expression** does it in one line:

```python
lado = "largo"   # long; "corto" is short
signo = 1 if lado == "largo" else -1
pnl = signo * (cierre - 139.98) * 100
print(signo, f"{pnl:+.2f}")
```
```output
1 +224.00
```

Conditions do not have to be comparisons. In an `if`, an empty collection, `0`, the empty string and `None` count as
false, and everything else as true. `if not ordenes_pendientes:` reads “if there are no pending orders”. To check
whether a value is missing, the correct form is `if stop is None:`, not `if not stop:`: a stop at 0 is not the same as
having no stop.

## Repeating with for

`for` iterates over a collection and runs its block once per element. With `range(n)` it goes through the numbers
from 0 to n−1, and with `enumerate` it yields both the position and the element:

```python
conteo = {"bullish": 0, "bearish": 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["bullish"] += 1
    else:
        conteo["bearish"] += 1
print(conteo, sum(conteo.values()))
```
```output
{'bullish': 124, 'bearish': 109, 'doji': 28} 261
```

The unpacking from lesson 0.3.3 works in the `for` header: each tuple is split into five variables. `continue` jumps
to the next element without running the rest of the block: here it discards the bars that are not from 2025. The
`conteo` dictionary accumulates the result, a common practice for counting by category.

A `for` goes through the whole collection unless something cuts it short. `break` exits the loop immediately, and it
is what you use to find **the first time** something happens. A `for` also accepts an `else`, which runs only if the
loop finished without `break`, that is, if what was being searched for was not found.

## Stop and target: break and the loop's else

A long position with a stop and a target is closed in the first session in which the price touches either of them.
With daily bars, the stop is hit if the day's **low** reaches the stop, and the target if the **high** reaches the
target. The order of the checks matters:

```mermaid
flowchart TB
  B[Next bar] --> S{Low ≤ stop?}
  S -- yes --> XS[Exit at the stop]
  S -- no --> O{High ≥ target?}
  O -- yes --> XO[Exit at target]
  O -- no --> B
```

The stop is checked first for the sake of prudence. If in the same bar the low touches the stop and the high touches
the target, daily data do not say what happened first. Assuming it was the target means picking the favorable
outcome, and a backtest that always picks the favorable outcome gets inflated; the conservative assumption is the
stop.

The entry in the example is a buy at the close of May 2, 2025, with a stop 3% below and a target 6% above:

```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"Entry {entrada:.2f}  stop {stop:.2f}  "
      f"target {objetivo:.2f}")

for i, (fecha, ap, mx, mn, ci) in enumerate(barras):
    if i <= i_entrada:
        continue
    if mn <= stop:
        print(f"{fecha}: stop hit (low {mn:.2f})")
        break
    elif mx >= objetivo:
        print(f"{fecha}: target hit (high {mx:.2f})")
        break
else:
    print("Neither stop nor target: position open")
print("Sessions in position:", i - i_entrada)
```
```output
Entry 142.51  stop 138.23  target 151.06
2025-05-07: stop hit (low 134.69)
Sessions in position: 3
```

The position lasts three sessions: on May 7 the low, 134.69, goes through the stop at 138.23. That session opened at
139.22, above the stop, so the sale would have been made at the stop and not lower.

The first line is a dictionary comprehension (lesson 0.3.3) that maps each date to its position in the list: that is
how the entry session is found without going through the list by hand. In the `for` header, `enumerate` yields the
position `i` and the tuple, which is unpacked inside parentheses. Sessions before the entry are skipped with
`continue`; so is the entry session itself, because the buy happens at its close and that day's low and high had
already occurred.

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

Comparing the stop with the low of the **same** bar on which the signal is computed is look-ahead bias (lesson
0.1.1). That is why the loop starts checking on the session after the entry. And the stop does not guarantee the
price: if the market opens below the stop, the sale is executed at the open, lower.

</aside>

## Trailing stop with while

A **trailing stop** moves up with the price and never down: it sits a fixed percentage below the highest close since
the entry. It protects part of the gain without setting a target. You do not know in advance how many sessions the
position will last, and that is what a **`while`** loop expresses: it repeats its block **as long as** the condition
is true.

```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)   # if it opens below, at the open
        abierta = False
    else:
        maximo_cierre = max(maximo_cierre, ci)
        trailing = max(trailing, maximo_cierre * 0.95)

print(f"Entry {entrada:.2f} on {barras[indice['2023-11-01']][0]}")
print(f"Exit {salida:.2f} on {fecha}, "
      f"highest close {maximo_cierre:.2f}")
print(f"Result {salida / entrada - 1:+.2%} "
      f"in {i - indice['2023-11-01']} sessions")
```
```output
Entry 115.27 on 2023-11-01
Exit 126.71 on 2024-02-23, highest close 133.38
Result +9.93% in 82 sessions
```

The position is held for 82 sessions. The highest close reaches 133.38 and drags the stop up to 126.71; on February
23, 2024 the low, 126.55, touches it, and the exit leaves +9.93%. The figure at the top draws the path: SINT's close,
the trailing stop following it in steps and the exit in amber.

Three details of the loop. The condition has two parts: the position is still open **and** there are bars left;
without the second, if the stop never triggered, `barras[i]` would end up asking for a bar that does not exist. The
trailing stop is updated with the close only after checking the low, so that a day's close is not used to decide
whether the stop was hit that same day. And `min(ap, trailing)` applies the earlier warning: if the session opens
below the stop, the sale happens at the open.

`while` versus `for`: if you go through a known collection, `for`; if you repeat until something happens, `while`. A
`while` whose condition never becomes false is an **infinite loop**: the program hangs. In a system that trades, every
`while` needs a safe exit (a maximum number of attempts, a time limit).

## Dispatching orders with match-case

A system receives orders of several types, and each type needs different fields: a market order, side and quantity;
a limit order, also a limit price; a stop order, a trigger price. With `if`/`elif` you would have to check the type
and then, one by one, that the fields are present. **`match`-`case`** (Python 3.10) compares the **shape** of the
value against a series of patterns and, if it matches, binds its parts to variables. The orders keep the course's
data keys and values: `tipo` (type: `"mercado"` market, `"limite"` limit, `"stop"`), `lado` (side: `"compra"` buy,
`"venta"` sell), `cantidad` (quantity) and `precio` (price):

```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"MARKET  {lado} {q}")
        case {"tipo": "limite", "lado": lado, "cantidad": q,
              "precio": p} if p > 0:
            print(f"LIMIT   {lado} {q} at {p:.2f}")
        case {"tipo": "stop", "lado": "venta", "cantidad": q,
              "precio": p}:
            print(f"STOP    sell {q} if it falls to {p:.2f}")
        case {"tipo": "limite" | "stop"}:
            print(f"REJECTED: invalid price or side {orden}")
        case _:
            print(f"REJECTED: unknown type {orden['tipo']!r}")
```
```output
MARKET  compra 100
LIMIT   venta 50 at 145.20
STOP    sell 100 if it falls to 135.00
REJECTED: invalid price or side {'tipo': 'limite', 'lado': 'compra', 'cantidad': 10, 'precio': -1}
REJECTED: unknown type 'iceberg'
```

How to read the patterns:

- A **mapping pattern** matches if the value has **at least** those keys with values that match. Extra keys do not
  matter.
- A literal value (`"mercado"`, `"venta"`) has to be equal; a name (`lado`, `q`, `p`) captures whatever value is there
  and leaves it in that variable for the branch.
- `if p > 0` after the pattern is a **guard**: the case only applies if, besides matching, the condition holds. The
  limit order with price −1 matches the shape, fails the guard and keeps going down.
- `"limite" | "stop"` matches either of the two values. This case collects the limit and stop orders that did not pass
  the previous ones (such as a buy stop, which this system does not accept).
- `case _` matches everything: it is the `else` of `match`. Without it, an order that matches nothing is silently
  ignored.

Cases are tried in order and only the first one that matches runs, just as in `if`/`elif`. That is why the general
patterns (`"limite" | "stop"`, `_`) go at the end: placed at the beginning, they would swallow the valid orders.

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

Rejecting is better than guessing. An order with an impossible price or an unknown type is not fixed in the code or
sent “to see what happens”: it is rejected and logged. Lesson 0.3.6 turns these rejections into exceptions.

</aside>

## Exercises

1. Count how many sessions in 2024 closed with a rise of more than 2% over the previous close, and what the largest
   rise of the year was, with its date.

**Solution.**

```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))` starts at 1 because the first bar has no previous close. The return of the first session of
2024 uses the last close of 2023, which is correct.

2. Repeat the trailing stop with 8% instead of 5%, from the same entry. When does it exit and with what result? Why
   does the number of sessions change?

**Solution.**

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

With 8%, the position exits on March 8, 2024, after 92 sessions, with +6.45% versus +9.93% with 5%. A wider
trailing stop tolerates more noise before triggering: the position lasts more sessions, but exits further from the
high. There is no “correct” percentage; choosing it by looking at the result on a single stretch of data is
overfitting (level 2).

3. Add to the orders `match` a case for market orders with a quantity that is not positive, and check that it rejects
   `{"tipo": "mercado", "lado": "compra", "cantidad": 0}`. Where does the new case have to go?

**Solution.**

```python
orden = {"tipo": "mercado", "lado": "compra", "cantidad": 0}
match orden:
    case {"cantidad": q} if q <= 0:
        print(f"REJECTED: quantity {q}")
    case {"tipo": "mercado", "lado": lado, "cantidad": q}:
        print(f"MARKET {lado} {q}")
    case _:
        print("REJECTED")
```
```output
REJECTED: quantity 0
```

It has to go **before** the market case: if it came after, the order would first match `{"tipo": "mercado", …}` and
would be sent with quantity 0. Since it does not name the type, the new case also protects limit and stop orders.

## Self-assessment

1. With `cierre = 101` and `apertura = 100`, an `if cierre > apertura` prints “bullish”, the following `elif cierre > 100` prints “above 100” and the `else`, “other”. What is printed?

   - A. `bullish` and `above 100`
   - B. `above 100`
   - C. `other`
   - D. `bullish`

   *Answer D: `bullish`.* In an `if`/`elif`/`else` chain only the first branch whose condition holds runs. Even though `cierre > 100` is also true, the `elif` is not even evaluated.

2. In a `for` loop that looks for the day the low touches the stop, when does the loop's `else` block run?

   - A. Always, when the loop finishes
   - B. When the loop ends without going through `break`: the stop was not touched on any day
   - C. When `break` runs
   - D. Never: `for` does not accept `else`

   *Answer B: When the loop ends without going through `break`: the stop was not touched on any day.* A loop's `else` runs if the loop goes through every element without a `break`. It is the way to say “not found” without a flag variable.

3. A bar has its low below the stop and its high above the target. With daily data, what does a prudent backtest assume?

   - A. That the stop was hit first
   - B. That the target was hit first
   - C. That neither was hit
   - D. That both were executed

   *Answer A: That the stop was hit first.* A daily bar does not tell you what happened first within the day. Assuming the target would mean picking the most favorable outcome; the conservative assumption is the stop.

4. What does `case {"tipo": "limite", "precio": p} if p > 0:` do with the order `{"tipo": "limite", "lado": "compra", "precio": 101.2}`?

   - A. It does not match, because the order has an extra key (`lado`)
   - B. It raises a syntax error
   - C. It matches and binds `p = 101.2`
   - D. It matches only if `precio` is an `int`

   *Answer C: It matches and binds `p = 101.2`.* A mapping pattern requires the keys it names and allows extra keys. `p` captures the value of `precio` and the guard `if p > 0` holds.

## Key points

- `if`/`elif`/`else` runs only the first branch whose condition holds. Indentation (4 spaces) marks which code belongs to each branch.
- `for` iterates over a collection; `enumerate` adds the index. `break` exits the loop and the loop's `else` runs only if there was no `break`.
- `while` repeats as long as a condition holds: for things of unknown duration, such as a position open with a trailing stop. Make sure the condition eventually becomes false.
- With daily bars you cannot tell whether the stop or the target was hit first: a prudent backtest assumes the stop.
- `match`-`case` compares the shape of a value: with mapping patterns and guards, it validates and dispatches market, limit and stop orders in a single block.

## Resources

### Research

- [Finite-State Machines for Quantitative Trading Execution in Python](https://pythonparatrading.com/en/finite-state-machines-trading-python/) · Research-0029 · 47 min

### Documentation

- [Python: more control flow tools (official tutorial)](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

### Data

- [Level 0 data: daily prices of the fictitious ETF SINT (CSV)](https://pythonparatrading.com/curso/datos/nivel-0/sint_diario.csv) · Download · sint_diario.csv

Previous lesson: [0.3.3 · Lists, tuples, dictionaries, sets and comprehensions](https://pythonparatrading.com/en/python-trading-course/level-0/python-lists-tuples-dicts-sets/)

Next lesson: [0.3.5 · Functions: arguments, *args and **kwargs, lambdas and scope](https://pythonparatrading.com/en/python-trading-course/level-0/python-functions-arguments-scope/)
