Lección 0.3.3 · 50 min

Listas, tuplas, diccionarios, conjuntos y comprensiones

Una serie de cierres, una barra OHLC, una cartera y el universo de un índice son colecciones de datos, y cada una tiene su estructura en Python. La lección enseña las cuatro y las comprensiones, la forma corta de construirlas.

Fig. — Rentabilidades diarias de SINT en la segunda mitad de diciembre de 2025, calculadas con una comprensión
03/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

Colecciones: muchos datos bajo un nombre

Hasta ahora cada variable guardaba un dato: un precio, una cantidad, un ticker (lecciones 0.3.1 y 0.3.2). Un programa de trading trabaja casi siempre con muchos: los cierres de un mes, los activos que se vigilan, las posiciones de una cartera, los componentes de un índice. Python tiene cuatro colecciones integradas para guardarlos, y cada una responde a una necesidad distinta:

ColecciónLiteral¿Ordenada?¿Modificable?¿Duplicados?Uso típico en trading
Lista (list)[139.32, 139.23]SíSíSíSerie de cierres, watchlist, órdenes pendientes
Tupla (tuple)(140.01, 143.40, 139.67, 142.22)SíNoSíUna barra OHLC, un registro fijo
Diccionario (dict){"SINT": 100, "ORO": 20}Por inserciónSíClaves únicasCartera, precios por ticker, parámetros
Conjunto (set){"SINT", "ORO"}NoSíNoUniverso de un índice, símbolos únicos

Listas: series y watchlists

Una lista es una secuencia ordenada de valores entre corchetes. Se indexa y se corta exactamente igual que una cadena (lección 0.3.2): el primer elemento es el [0], el último el [-1] y [inicio:fin] devuelve una lista nueva con ese tramo. Los cierres de SINT de la segunda mitad de diciembre de 2025 (del fichero sint_diario.csv, enlace en «Recursos») son una lista de float:

IN [01]python
cierres = [146.36, 145.13, 144.22, 143.66, 139.91, 139.43,
           139.36, 139.32, 139.23, 139.56, 139.98, 142.22]
print(len(cierres), cierres[0], cierres[-1])
print(cierres[-3:])
print(max(cierres), min(cierres))
print(sum(cierres) / len(cierres))
OUT [01]Una serie de cierres
12 146.36 142.22
[139.56, 139.98, 142.22]
146.36 139.23
141.53166666666667

len, max, min y sum funcionan con cualquier colección de números. cierres[-3:] son los tres últimos cierres, y la última línea es la media de la serie. Con la misma idea, cierres[-20:] daría las últimas veinte sesiones de una serie más larga: así empieza el cálculo de una media móvil.

A diferencia de las cadenas, las listas son modificables. Sus métodos cambian la propia lista y no devuelven nada (devuelven None). Una watchlist, la lista de activos que se vigilan, es el ejemplo natural:

IN [02]python
watchlist = ["SINT", "RVAR", "ORO"]
watchlist.append("TECH")      # añade al final
watchlist.insert(0, "RFIJ")   # inserta en una posición
watchlist.remove("ORO")       # quita por valor
ultimo = watchlist.pop()      # quita y devuelve el último
print(watchlist, ultimo)

watchlist.sort()
print(watchlist, "SINT" in watchlist, watchlist.index("SINT"))
print(sorted(cierres)[:3])
OUT [02]Modificar una watchlist
['RFIJ', 'SINT', 'RVAR'] TECH
['RFIJ', 'RVAR', 'SINT'] True 2
[139.23, 139.32, 139.36]

sort() ordena la lista en su sitio; sorted() devuelve una lista nueva ordenada y deja la original como estaba. La diferencia importa con una serie de precios: ordenar los cierres en su sitio destruye el orden temporal, y con él cualquier cálculo de rentabilidades que venga después. Para una serie temporal, sorted, nunca sort.

La trampa más común de las listas es esta: asignar una lista a otro nombre no la copia. Los dos nombres apuntan a la misma lista, y lo que se haga con uno se ve en el otro.

IN [03]python
pendientes = ["SINT", "ORO"]
para_enviar = pendientes          # el mismo objeto
para_enviar.append("TECH")
print(pendientes)

copia = pendientes.copy()         # un objeto nuevo
copia.clear()
print(pendientes, copia)
print(para_enviar is pendientes, copia is pendientes)
OUT [03]Dos nombres, una lista
['SINT', 'ORO', 'TECH']
['SINT', 'ORO', 'TECH'] []
True False

El operador is pregunta si dos nombres apuntan al mismo objeto. En un sistema de órdenes, este error tiene consecuencias: un script que «vacía la copia» de la lista de órdenes pendientes después de enviarlas puede estar vaciando la original. La misma trampa reaparece con los argumentos por defecto de las funciones (lección 0.3.5).

Tuplas: una barra OHLC

Una tupla es una secuencia como la lista, pero inmutable: una vez creada no se puede cambiar. Se escribe entre paréntesis, o simplemente con comas. Es la estructura adecuada para un registro con un número fijo de campos que no deben cambiar, como una barra de precios: apertura, máximo, mínimo y cierre (OHLC, por open, high, low, close).

IN [04]python
barra = ("2025-12-31", 140.01, 143.40, 139.67, 142.22)
fecha, apertura, maximo, minimo, cierre = barra
print(fecha, cierre)
print(f"Rango: {maximo - minimo:.2f}  Cuerpo: {cierre - apertura:+.2f}")
print(barra[1:])
OUT [04]Una barra como tupla
2025-12-31 142.22
Rango: 3.73  Cuerpo: +2.21
(140.01, 143.4, 139.67, 142.22)

La segunda línea es el desempaquetado: una tupla (o cualquier secuencia) se reparte en tantas variables como elementos tenga. Es lo que hacía base, cotizada = "EUR", "USD" en la lección 0.3.2: a la derecha había una tupla sin paréntesis. Si el número de variables no coincide con el de elementos, Python da un error.

La inmutabilidad es una garantía: una barra histórica no debería cambiar, y una tupla no deja hacerlo (barra[4] = 150.0 da un error). Una barra que se pudiera modificar por descuido sería un dato histórico falsificado en medio de un backtest.

Diccionarios: cartera y precios por ticker

Un diccionario asocia claves a valores. Se escribe entre llaves con pares clave: valor, y se consulta por la clave, no por la posición. Una cartera es un diccionario de ticker → acciones; los precios, otro de ticker → último cierre (los de SINT y de los activos de cartera_sint.csv el 31 de diciembre de 2025):

IN [05]python
cartera = {"SINT": 100, "ORO": 20, "RFIJ": 150}
precios = {"SINT": 142.22, "ORO": 171.44, "RFIJ": 102.29,
           "TECH": 77.82}

print(cartera["SINT"], len(cartera), "TECH" in cartera)
cartera["TECH"] = 30          # alta de una posición
cartera["ORO"] += 10          # aumentar una posición
del cartera["RFIJ"]           # cierre de una posición
print(cartera)
print(list(cartera.keys()), list(cartera.values()))
OUT [05]Una cartera como diccionario
100 3 False
{'SINT': 100, 'ORO': 30, 'TECH': 30}
['SINT', 'ORO', 'TECH'] [100, 30, 30]

Asignar a una clave que no existe la crea; asignar a una que existe sustituye su valor. in pregunta por las claves, no por los valores. Desde Python 3.7, los diccionarios conservan el orden en que se insertaron las claves.

Para valorar la cartera hay que recorrerla. items() da las parejas clave-valor, y el bucle for (lección 0.3.4) las procesa una a una:

IN [06]python
valor_total = 0.0
for ticker, acciones in cartera.items():
    valor = acciones * precios[ticker]
    valor_total += valor
    print(f"{ticker:<5} {acciones:>4} x {precios[ticker]:>7.2f}"
          f" = {valor:>9,.2f}")
print(f"{'Total':<22}{valor_total:>9,.2f}")
OUT [06]Valor de la cartera
SINT   100 x  142.22 = 14,222.00
ORO     30 x  171.44 =  5,143.20
TECH    30 x   77.82 =  2,334.60
Total                 21,699.80

Leer una clave que no existe con corchetes es un error (KeyError), y en trading pasa a menudo: un ticker sin precio ese día, un símbolo mal escrito. Si la ausencia es normal y tiene un valor razonable, get lo da sin error:

IN [07]python
print(cartera.get("SINT", 0), cartera.get("BTC", 0))
print(precios.get("BTC"))
OUT [07]Claves que pueden faltar
100 0
None

Sin segundo argumento, get devuelve None. Cuidado con usarlo para tapar errores: una cartera que no tiene BTC tiene 0 BTC, pero un precio que falta no es un precio de 0. Valorar una posición con precio 0 la hace desaparecer del valor de la cartera sin ningún aviso. Cuándo conviene dejar que salte el error y cómo tratarlo se ve en la lección 0.3.6.

Conjuntos: el universo de un índice

Un conjunto es una colección de elementos únicos y sin orden, entre llaves sin pares. Sirve para dos cosas: quitar duplicados y hacer operaciones de conjuntos, que son exactamente las preguntas que se hacen sobre el universo de un índice. Los componentes de un índice cambian con el tiempo: entran empresas que han crecido y salen las que han caído, se han fusionado o han quebrado.

El ejemplo usa un índice ficticio de seis componentes en dos fechas:

IN [08]python
universo_2024 = {"ALFA", "BETA", "GAMMA", "DELTA", "SIGMA", "OMEGA"}
universo_2025 = {"ALFA", "BETA", "DELTA", "SIGMA", "KAPPA", "LAMBDA"}

altas = universo_2025 - universo_2024
bajas = universo_2024 - universo_2025
siguen = universo_2024 & universo_2025
alguna_vez = universo_2024 | universo_2025
print("Altas:", sorted(altas))
print("Bajas:", sorted(bajas))
print("Siguen:", len(siguen), " Alguna vez:", len(alguna_vez))
print("Cambios:", sorted(universo_2024 ^ universo_2025))
OUT [08]Altas y bajas de un índice
Altas: ['KAPPA', 'LAMBDA']
Bajas: ['GAMMA', 'OMEGA']
Siguen: 4  Alguna vez: 8
Cambios: ['GAMMA', 'KAPPA', 'LAMBDA', 'OMEGA']
OperaciónOperadorPregunta
Diferenciaa - b¿Qué está en a y no en b? (altas o bajas)
Interseccióna & b¿Qué está en los dos?
Unióna | b¿Qué está en alguno?
Diferencia simétricaa ^ b¿Qué está en uno solo? (todos los cambios)

Los conjuntos no tienen orden, así que print los mostraría en un orden cualquiera; sorted los convierte en una lista ordenada para que la salida sea siempre la misma.

La línea de las bajas es la que más importa en trading. Si un backtest de los últimos diez años usa solo los componentes de hoy, nunca compra GAMMA ni OMEGA: las empresas que salieron, muchas veces después de caer, no están en la lista. El backtest solo ve a las supervivientes y sale mejor de lo que habría sido de verdad. Es el sesgo de supervivencia, y el único remedio es usar el universo que existía en cada fecha, alguna_vez incluido:

Fig. — Sesgo de supervivencia: el mismo índice con y sin las empresas que desaparecieron (esquema)

La otra utilidad de los conjuntos es quitar duplicados. Los símbolos operados en un registro de operaciones se repiten; set los deja una vez cada uno:

IN [09]python
simbolos_operados = ["ORO", "SINT", "SINT", "SINT", "ORO", "TECH"]
unicos = set(simbolos_operados)
print(sorted(unicos), len(unicos))
OUT [09]Símbolos únicos
['ORO', 'SINT', 'TECH'] 3

Comprensiones: rentabilidades en una línea

Una comprensión construye una colección a partir de otra en una sola expresión: [expresión for elemento in colección]. Se lee casi como una frase: «la expresión, para cada elemento de la colección». Es la forma idiomática de transformar una serie.

La rentabilidad de un día es rt=Pt/Pt−1−1r_t = P_t / P_{t-1} - 1: necesita cada cierre y el anterior. zip empareja los elementos de dos colecciones; si se le pasan la serie y la serie desplazada una posición (cierres[1:]), cada pareja es (cierre anterior, cierre del día):

IN [10]python
rentabilidades = [c1 / c0 - 1
                  for c0, c1 in zip(cierres, cierres[1:])]
print(len(rentabilidades))
print(" ".join([f"{r:+.2%}" for r in rentabilidades]))

peor = min(rentabilidades)
print(f"Peor día: {peor:+.2%}, sesión {rentabilidades.index(peor) + 1}")
OUT [10]Rentabilidades diarias con una comprensión
11
-0.84% -0.63% -0.39% -2.61% -0.34% -0.05% -0.03% -0.06% +0.24% +0.30% +1.60%
Peor día: -2.61%, sesión 4

Doce cierres dan once rentabilidades: zip se detiene al acabarse la lista más corta. La segunda comprensión transforma cada rentabilidad en un texto con formato, y " ".join(…) une esos textos en uno solo, separados por un espacio. La figura de la cabecera dibuja esas once rentabilidades; la peor, la del 22 de diciembre, en ámbar.

Una comprensión puede filtrar con un if al final, y hay versiones para diccionarios y conjuntos:

IN [11]python
subidas = [r for r in rentabilidades if r > 0]
print(f"Días al alza: {len(subidas)} de {len(rentabilidades)}")

valores = {t: n * precios[t] for t, n in cartera.items()}
total = sum(valores.values())
pesos = {t: round(v / total, 3) for t, v in valores.items()}
print(pesos)

operaciones = [("SINT", 7.43), ("ORO", -3123.56),
               ("SINT", -31.50), ("TECH", 1426.71)]
con_perdidas = {s for s, pnl in operaciones if pnl < 0}
print(sorted(con_perdidas))
OUT [11]Filtros, diccionarios y conjuntos por comprensión
Días al alza: 3 de 11
{'SINT': 0.655, 'ORO': 0.237, 'TECH': 0.108}
['ORO', 'SINT']

Las llaves con clave: valor dan un diccionario; sin los dos puntos, un conjunto. Los pesos de la cartera salen en dos pasos: primero el valor de cada posición y después su fracción del total.

Las comprensiones son cómodas mientras caben en una o dos líneas y se leen de un vistazo. Si hace falta más de un if, o la expresión ocupa media línea, un bucle for normal (lección 0.3.4) es más claro.

Qué colección usar

La decisión sale de tres preguntas, en este orden:

sínosínosíno¿Clave y valor?dict¿Solo únicos?set¿Va a cambiar?listtuple
Fuente Mermaid
flowchart TB
  Q1{¿Clave y valor?} -- sí --> D[dict]
  Q1 -- no --> Q2{¿Solo únicos?}
  Q2 -- sí --> S[set]
  Q2 -- no --> Q3{¿Va a cambiar?}
  Q3 -- sí --> L[list]
  Q3 -- no --> T[tuple]

Si cada dato se busca por un nombre (un ticker, un parámetro), diccionario. Si lo que importa es pertenecer o no (está en el índice, se ha operado), conjunto. Si es una secuencia que crece o cambia (una serie que recibe un cierre nuevo cada día, una cola de órdenes), lista. Si es un registro fijo (una barra, una operación cerrada), tupla.

Ejercicios

  1. Con la lista cierres de la lección, calcula la rentabilidad total del periodo (último cierre entre el primero, menos 1) y el número de sesiones en las que el cierre bajó respecto al anterior.
Solución
IN [12]python
total_periodo = cierres[-1] / cierres[0] - 1
bajadas = [r for r in rentabilidades if r < 0]
print(f"Rentabilidad del periodo: {total_periodo:+.2%}")
print(f"Sesiones a la baja: {len(bajadas)}")
OUT [12]Rentabilidad total y días a la baja
Rentabilidad del periodo: -2.83%
Sesiones a la baja: 8

La rentabilidad total solo necesita el primer y el último cierre. Las sesiones a la baja salen de filtrar las rentabilidades negativas con una comprensión.

  1. El índice ficticio cambia otra vez en 2026: salen BETA y KAPPA y entran OMEGA y RHO. Construye universo_2026 a partir de universo_2025 con operaciones de conjuntos, y calcula cuántas empresas han formado parte del índice en algún momento de los tres años.
Solución
IN [13]python
universo_2026 = (universo_2025 - {"BETA", "KAPPA"}) | {"OMEGA", "RHO"}
print(sorted(universo_2026))
historico = universo_2024 | universo_2025 | universo_2026
print(len(historico), sorted(historico))
OUT [13]Universo de 2026
['ALFA', 'DELTA', 'LAMBDA', 'OMEGA', 'RHO', 'SIGMA']
9 ['ALFA', 'BETA', 'DELTA', 'GAMMA', 'KAPPA', 'LAMBDA', 'OMEGA', 'RHO', 'SIGMA']

OMEGA sale en 2025 y vuelve en 2026; el conjunto la cuenta una sola vez. Un backtest honesto de 2024 a 2026 necesita los nueve nombres, con las fechas en que cada uno estuvo dentro.

  1. Con el diccionario precios de la lección, crea un diccionario con el valor de una cartera que tuviera 10 acciones de cada ticker, y di qué ticker tiene el mayor valor.
Solución
IN [14]python
diez = {t: round(10 * p, 2) for t, p in precios.items()}
print(diez)
mayor = max(diez, key=diez.get)
print(mayor, diez[mayor])
OUT [14]Diez acciones de cada ticker
{'SINT': 1422.2, 'ORO': 1714.4, 'RFIJ': 1022.9, 'TECH': 778.2}
ORO 1714.4

round quita la cola de decimales que deja la coma flotante en 10 * 102.29 (lección 0.3.1). max recorre las claves del diccionario; key=diez.get le dice que las compare por su valor y no por el nombre. La lección 0.3.5 explica qué es ese argumento key y cómo usarlo con funciones propias.

Respuesta plegada en cada pregunta

Autoevaluación

  1. cierres = [139.32, 139.23, 139.56] y después copia = cierres; copia.append(139.98). ¿Cuántos elementos tiene cierres?

    • A3, porque se modificó la copia
    • B4, porque copia y cierres son la misma lista
    • CDa un error
    • D1
    Ver respuesta

    Respuesta B. 4, porque copia y cierres son la misma lista

    copia = cierres no copia nada: crea otro nombre para la misma lista. Para una copia independiente hace falta cierres.copy() o list(cierres).

  2. ¿Qué estructura es la adecuada para guardar las acciones que tienes de cada ticker y consultarlas por su nombre?

    • AUna lista
    • BUna tupla
    • CUn diccionario
    • DUn conjunto
    Ver respuesta

    Respuesta C. Un diccionario

    Un diccionario asocia cada clave (el ticker) a un valor (las acciones) y la búsqueda por clave es inmediata. Una lista obligaría a recorrerla entera.

  3. antes = {"A", "B", "C"} y ahora = {"B", "C", "D"}. ¿Qué devuelve antes - ahora?

    • A{"D"}
    • B{"A"}
    • C{"B", "C"}
    • D{"A", "D"}
    Ver respuesta

    Respuesta B. {"A"}

    La diferencia antes - ahora son los elementos que estaban y ya no están: las bajas del índice. Las altas serían ahora - antes, y ^ daría los dos cambios juntos.

  4. Un backtest usa solo las empresas que forman hoy un índice para simular los últimos diez años. ¿Qué sesgo tiene?

    • ASesgo de anticipación
    • BSesgo de supervivencia: faltan las empresas que salieron del índice, muchas de ellas porque cayeron
    • CNinguno, si los precios están ajustados
    • DSesgo de costes
    Ver respuesta

    Respuesta B. Sesgo de supervivencia: faltan las empresas que salieron del índice, muchas de ellas porque cayeron

    Las empresas que están hoy en el índice son las que sobrevivieron. Las que quebraron o cayeron lo suficiente para salir no aparecen, y el backtest sale mejor de lo que habría sido. Hace falta el universo de cada fecha.

  5. ¿Qué hace [c1 / c0 - 1 for c0, c1 in zip(cierres, cierres[1:])]?

    • ACalcula la rentabilidad de cada día respecto al anterior
    • BCalcula la rentabilidad total
    • COrdena los cierres
    • DDa un error porque las listas tienen distinta longitud
    Ver respuesta

    Respuesta A. Calcula la rentabilidad de cada día respecto al anterior

    zip empareja cada cierre con el siguiente (zip se detiene en la lista más corta) y la comprensión calcula c1 / c0 - 1 para cada pareja: una rentabilidad por día, una menos que cierres.

Ideas clave

  • Lista: secuencia ordenada y modificable (cierres, watchlist). Tupla: secuencia fija, ideal para un registro como una barra OHLC.
  • Asignar una lista a otro nombre no la copia: los dos nombres apuntan a la misma lista. Para copiar, .copy().
  • Diccionario: clave → valor (ticker → acciones). d[clave] falla si la clave no existe; d.get(clave, 0) devuelve un valor por defecto.
  • Conjunto: elementos únicos sin orden. La diferencia entre el universo de dos fechas da las altas y las bajas; ignorar las bajas es el sesgo de supervivencia.
  • Una comprensión construye una colección en una línea: [c1 / c0 - 1 for c0, c1 in zip(cierres, cierres[1:])] da las rentabilidades diarias.

Recursos