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.
- 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
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ón | Literal | ¿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í | No | Sí | Una barra OHLC, un registro fijo |
Diccionario (dict) | {"SINT": 100, "ORO": 20} | Por inserción | Sí | Claves únicas | Cartera, precios por ticker, parámetros |
Conjunto (set) | {"SINT", "ORO"} | No | Sí | No | Universo 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:
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))12 146.36 142.22
[139.56, 139.98, 142.22]
146.36 139.23
141.53166666666667len, 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:
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])['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.
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)['SINT', 'ORO', 'TECH']
['SINT', 'ORO', 'TECH'] []
True FalseEl 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).
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:])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):
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()))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:
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}")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.80Leer 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:
print(cartera.get("SINT", 0), cartera.get("BTC", 0))
print(precios.get("BTC"))100 0
NoneSin 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:
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))Altas: ['KAPPA', 'LAMBDA']
Bajas: ['GAMMA', 'OMEGA']
Siguen: 4 Alguna vez: 8
Cambios: ['GAMMA', 'KAPPA', 'LAMBDA', 'OMEGA']| Operación | Operador | Pregunta |
|---|---|---|
| Diferencia | a - b | ¿Qué está en a y no en b? (altas o bajas) |
| Intersección | a & b | ¿Qué está en los dos? |
| Unión | a | b | ¿Qué está en alguno? |
| Diferencia simétrica | a ^ 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:
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:
simbolos_operados = ["ORO", "SINT", "SINT", "SINT", "ORO", "TECH"]
unicos = set(simbolos_operados)
print(sorted(unicos), len(unicos))['ORO', 'SINT', 'TECH'] 3Comprensiones: 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 : 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):
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}")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 4Doce 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:
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))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:
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
- Con la lista
cierresde 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
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)}")Rentabilidad del periodo: -2.83%
Sesiones a la baja: 8La 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.
- El índice ficticio cambia otra vez en 2026: salen BETA y KAPPA y entran OMEGA y RHO. Construye
universo_2026a partir deuniverso_2025con operaciones de conjuntos, y calcula cuántas empresas han formado parte del índice en algún momento de los tres años.
Solución
universo_2026 = (universo_2025 - {"BETA", "KAPPA"}) | {"OMEGA", "RHO"}
print(sorted(universo_2026))
historico = universo_2024 | universo_2025 | universo_2026
print(len(historico), sorted(historico))['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.
- Con el diccionario
preciosde 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
diez = {t: round(10 * p, 2) for t, p in precios.items()}
print(diez)
mayor = max(diez, key=diez.get)
print(mayor, diez[mayor]){'SINT': 1422.2, 'ORO': 1714.4, 'RFIJ': 1022.9, 'TECH': 778.2}
ORO 1714.4round 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
cierres = [139.32, 139.23, 139.56]y despuéscopia = cierres; copia.append(139.98). ¿Cuántos elementos tienecierres?- A3, porque se modificó la copia
- B4, porque
copiaycierresson la misma lista - CDa un error
- D1
Ver respuesta
Respuesta B. 4, porque
copiaycierresson la misma listacopia = cierresno copia nada: crea otro nombre para la misma lista. Para una copia independiente hace faltacierres.copy()olist(cierres).¿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.
antes = {"A", "B", "C"}yahora = {"B", "C", "D"}. ¿Qué devuelveantes - ahora?- A
{"D"} - B
{"A"} - C
{"B", "C"} - D
{"A", "D"}
Ver respuesta
Respuesta B.
{"A"}La diferencia
antes - ahorason los elementos que estaban y ya no están: las bajas del índice. Las altas seríanahora - antes, y^daría los dos cambios juntos.- A
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.
¿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
zipempareja cada cierre con el siguiente (zipse detiene en la lista más corta) y la comprensión calculac1 / c0 - 1para 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.