Lección 0.6.2 · 45 min

Dashboards rápidos con Streamlit: un panel de seguimiento en un solo script

Un notebook sirve para investigar; un panel, para mirar cada día lo mismo sin tocar código. Streamlit convierte un script de Python en una página web con selectores y gráficos. Esta lección monta un panel de precios y drawdown sobre los datos del curso y explica la única idea que hay que entender para no pelearse con él: el script se ejecuta entero en cada clic.

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

Del notebook al panel

En la lección 0.6.1 los gráficos se hacían en un notebook: se cambia una línea, se ejecuta la celda y se mira el resultado. Para investigar es lo correcto. Para seguir una cartera cada mañana no lo es: nadie quiere editar código para cambiar de activo o de periodo, y menos aún pedírselo a un compañero que no programa.

Un panel (dashboard) resuelve eso: una página con unos pocos controles (activo, fechas) y siempre las mismas cifras y gráficos. Streamlit construye esa página a partir de un script de Python normal, sin HTML ni JavaScript. Cada st.algo(…) del script añade un elemento a la página, en el orden en que aparece.

HerramientaPara qué sirveQuién lo usa
Notebook (Jupyter, Marimo)Investigar, probar ideas, documentar un análisisQuien escribe el código
Script con savefig()Informes fijos que se generan solos (PDF, PNG)Cualquiera, sin interacción
Panel de StreamlitSeguimiento diario con filtros: activo, periodo, estrategiaCualquiera, con interacción
Aplicación web a medidaProducto con usuarios, permisos y mucha cargaUn equipo de desarrollo

Streamlit ocupa el hueco entre el notebook y la aplicación a medida: en un centenar de líneas hay un panel útil. A cambio, su forma de ejecutarse es distinta de la de un script normal, y casi todos los problemas de un panel lento o que «se olvida» de los datos vienen de no entenderla.

Streamlit no forma parte del entorno del curso: el código de la interfaz se muestra pero no se ejecuta en esta página. Lo que sí se ejecuta, y es lo importante, son las funciones de datos que el panel usa.

El script entero, en cada clic

Un script normal se ejecuta una vez, de la primera línea a la última, y termina. Una app de Streamlit se ejecuta entera, de arriba abajo, cada vez que el usuario toca un widget (y cada vez que guardas el archivo mientras la desarrollas). Cuando el usuario cambia el activo en un selector, Streamlit no ejecuta «la parte del selector»: vuelve a lanzar todo el script, y esta vez la línea del selector devuelve el activo nuevo.

ya calculadanuevaToca un widgetScript completoLee los widgetsFunción cacheadaCopia de cachéEjecuta y guardaPinta la página
Fuente Mermaid
flowchart TB
  U[Toca un widget] --> S[Script completo]
  S --> W[Lee los widgets]
  W --> C{Función cacheada}
  C -->|ya calculada| K[Copia de caché]
  C -->|nueva| F[Ejecuta y guarda]
  K --> P[Pinta la página]
  F --> P
  P --> U

De esa regla salen tres consecuencias prácticas:

  • Lo caro se repite si no se cachea. Leer un CSV de cinco años tarda poco; descargar precios de un proveedor o leer un Parquet de varios gigas, no. Sin caché, cada clic lo repite.
  • Las variables no sobreviven entre ejecuciones. Un contador n = n + 1 vuelve a empezar en cada clic, porque el script empieza de nuevo. Lo que deba recordarse entre clics (una lista de órdenes que el usuario va añadiendo) va en st.session_state, que este panel no necesita.
  • El orden del script es el orden de la página. No hay callbacks que ordenar ni eventos que conectar: se lee de arriba abajo, como el notebook del que viene.

Las funciones de datos, sin Streamlit

La regla de diseño más útil para un panel: la interfaz no calcula nada. Las funciones que leen datos, recortan periodos y calculan métricas van en un módulo propio, panel_datos.py, que no importa Streamlit. Así se prueban como cualquier función (lección 0.4.5), se reutilizan en un notebook o en un informe, y app.py queda como una capa fina que solo pinta.

El panel trabaja con cinco activos ficticios del curso: SINT y los cuatro de cartera_sint.csv. La primera función junta sus cierres en una tabla con una columna por activo:

IN [01]python
import numpy as np
import pandas as pd


def cargar_cierres() -> pd.DataFrame:
    """Cierres diarios: una columna por activo, índice de fechas."""
    sint = pd.read_csv("sint_diario.csv", parse_dates=["fecha"],
                       index_col="fecha")["cierre"]
    cartera = pd.read_csv("cartera_sint.csv",
                          parse_dates=["fecha"], index_col="fecha")
    return cartera.join(sint.rename("SINT"), how="inner")


cierres = cargar_cierres()
print(cierres.shape)
cierres.tail(2)
OUT [01]panel_datos.py · carga
(1303, 5)
              RVAR    RFIJ     ORO   TECH    SINT
fecha
2025-12-30  165.47  102.30  170.50  78.25  139.98
2025-12-31  164.35  102.29  171.44  77.82  142.22

how="inner" solo conserva las fechas que están en los dos archivos. Aquí coinciden todas, pero con datos reales de dos mercados con festivos distintos (una bolsa europea y una americana) no pasaría, y es mejor que la función lo resuelva de forma explícita que encontrarse huecos en el gráfico.

La segunda función recorta un activo a un periodo. El detalle está en el tipo de las fechas: el selector de fechas de Streamlit devuelve objetos datetime.date, mientras que el índice de pandas es de Timestamp. Recortar un índice de fechas con datetime.date está obsoleto en pandas 3 (emite un aviso y dejará de funcionar), así que la función convierte antes de recortar:

IN [02]python
import datetime as dt


def tramo(cierres: pd.DataFrame, ticker: str,
          desde: dt.date, hasta: dt.date) -> pd.Series:
    """Cierres de un activo entre dos fechas, ambas incluidas."""
    inicio, fin = pd.Timestamp(desde), pd.Timestamp(hasta)
    return cierres.loc[inicio:fin, ticker].dropna()


serie = tramo(cierres, "TECH", dt.date(2025, 1, 1),
              dt.date(2025, 12, 31))
print(f"{serie.name}: {len(serie)} sesiones, "
      f"del {serie.index[0]:%Y-%m-%d} al {serie.index[-1]:%Y-%m-%d}")
OUT [02]panel_datos.py · periodo
TECH: 261 sesiones, del 2025-01-01 al 2025-12-31

Las 261 sesiones incluyen el 1 de enero: SINT y la cartera del curso no tienen festivos. Con datos de un mercado real, el primer día del tramo sería la primera sesión del año, y la función no necesita saberlo.

La tercera calcula las cifras que el panel enseña arriba: rentabilidad del periodo, volatilidad anualizada (la desviación típica diaria por 252\sqrt{252}; el porqué está en la lección 0.7.1) y drawdown máximo. Un periodo con menos de dos sesiones no tiene ninguna de las tres, y la función lo dice con un error claro en lugar de devolver NaN:

IN [03]python
def metricas(cierre: pd.Series) -> dict[str, float]:
    """Rentabilidad, volatilidad anual y drawdown máximo."""
    if len(cierre) < 2:
        raise ValueError("Hacen falta al menos dos sesiones")
    rent = cierre.pct_change().dropna()
    drawdown = cierre / cierre.cummax() - 1
    return {
        "rentabilidad": cierre.iloc[-1] / cierre.iloc[0] - 1,
        "volatilidad": rent.std() * np.sqrt(252),
        "drawdown_max": drawdown.min(),
    }


for nombre, valor in metricas(serie).items():
    print(f"{nombre:<13}{valor:8.2%}")
OUT [03]panel_datos.py · métricas
rentabilidad   -9.25%
volatilidad    28.82%
drawdown_max  -31.91%

TECH cierra 2025 con una pérdida del 9,25 %, pero por el camino llegó a perder un 31,91 %: la cifra de drawdown cuenta una historia que la rentabilidad sola esconde. El drawdown se mide dentro del periodo elegido: el máximo acumulado empieza a contar el primer día del tramo. Es lo que espera quien mira el panel («cuánto he llegado a perder este año»), pero conviene decirlo en la propia página, porque el drawdown desde el máximo histórico puede ser mucho mayor.

Un fin de semana es un periodo válido para el selector y vacío para los datos. La función lo rechaza:

IN [04]python
metricas(tramo(cierres, "SINT", dt.date(2025, 12, 27),
               dt.date(2025, 12, 28)))
OUT [04]Periodo sin sesiones
ValueError: Hacen falta al menos dos sesiones

Antes de escribir una línea de interfaz, unas pruebas mínimas fijan lo que se espera de las métricas. Una serie que sube un 10 % y luego cae a su nivel inicial tiene rentabilidad cero y un drawdown conocido de antemano:

IN [05]python
def test_metricas_ida_y_vuelta():
    fechas = pd.bdate_range("2025-01-06", periods=3)
    m = metricas(pd.Series([100.0, 110.0, 100.0], index=fechas))
    assert m["rentabilidad"] == 0
    assert abs(m["drawdown_max"] - (100 / 110 - 1)) < 1e-12


def test_tramo_incluye_los_extremos():
    s = tramo(cierres, "SINT", dt.date(2025, 12, 1),
              dt.date(2025, 12, 3))
    assert len(s) == 3


test_metricas_ida_y_vuelta()
test_tramo_incluye_los_extremos()
print("pruebas superadas")
OUT [05]Pruebas de las métricas
pruebas superadas

En el repositorio, estas dos funciones irían en tests/test_panel_datos.py y las ejecutaría uv run pytest. Aquí se llaman a mano para que la celda muestre el resultado.

La app: widgets, métricas y gráfico

Con los cálculos resueltos y probados, app.py solo tiene que pedir los datos, leer los widgets y pintar. Este es el panel completo:

IN [06]app.pypython
import datetime as dt

import plotly.graph_objects as go
import streamlit as st

from panel_datos import cargar_cierres, metricas, tramo

st.set_page_config(page_title="Panel SINT", layout="wide")


@st.cache_data(ttl="1h")
def datos():
    return cargar_cierres()


cierres = datos()
ultimo = cierres.index[-1].date()

ticker = st.sidebar.selectbox("Activo", list(cierres.columns))
periodo = st.sidebar.date_input(
    "Periodo",
    value=(dt.date(ultimo.year, 1, 1), ultimo),
    min_value=cierres.index[0].date(),
    max_value=ultimo,
)
if len(periodo) != 2:
    st.info("Elige la fecha final del periodo.")
    st.stop()

serie = tramo(cierres, ticker, *periodo)
if len(serie) < 2:
    st.warning("El periodo no tiene sesiones suficientes.")
    st.stop()

m = metricas(serie)
st.title(f"{ticker} · {serie.index[0]:%d/%m/%Y} a "
         f"{serie.index[-1]:%d/%m/%Y}")
c1, c2, c3 = st.columns(3)
c1.metric("Rentabilidad", f"{m['rentabilidad']:.1%}")
c2.metric("Volatilidad anual", f"{m['volatilidad']:.1%}")
c3.metric("Drawdown máximo (en el periodo)",
          f"{m['drawdown_max']:.1%}")

fig = go.Figure(go.Scatter(x=serie.index, y=serie, name=ticker))
fig.update_layout(template="plotly_dark", height=420,
                  yaxis_title="Cierre")
st.plotly_chart(fig)
st.caption(f"Datos hasta el cierre del {ultimo:%d/%m/%Y}.")

Leído de arriba abajo, como lo ejecuta Streamlit:

  • st.set_page_config fija el título de la pestaña del navegador y el ancho de la página ("wide" aprovecha toda la pantalla).
  • datos() es la carga, envuelta en una función con @st.cache_data (siguiente sección). En la primera ejecución lee los CSV; en las siguientes, devuelve una copia de lo que ya leyó.
  • st.sidebar.selectbox pone en la barra lateral un desplegable con los cinco activos y devuelve el elegido, un str. Por defecto, el primero de la lista.
  • st.sidebar.date_input con una tupla como value se convierte en un selector de rango. Devuelve una tupla de 0 a 2 fechas: mientras el usuario ha marcado el inicio y aún no el final, la tupla tiene una sola. Desempaquetar desde, hasta = … sin comprobarlo rompe la app a mitad de selección; por eso se mira len(periodo) y st.stop() detiene esa ejecución con un aviso. La siguiente interacción vuelve a lanzar el script.
  • st.columns(3) reparte las tres métricas en una fila; metric muestra una etiqueta y una cifra grande.
  • st.plotly_chart pinta la figura de Plotly de la lección 0.6.1, con su zoom y su cursor. Para algo rápido, st.line_chart(serie) dibuja la línea sin construir la figura.
  • st.caption dice hasta qué fecha llegan los datos. Un panel que no lo dice invita a tomar decisiones con precios de hace una semana sin saberlo.

La caché: cargar una vez, no en cada clic

@st.cache_data guarda el resultado de una función según sus argumentos. La primera llamada con unos argumentos dados ejecuta la función y guarda lo que devuelve; las siguientes con los mismos argumentos se saltan la ejecución. Como el script se ejecuta en cada clic, es lo que separa un panel que responde al instante de uno que relee los datos cada vez.

La biblioteca estándar tiene un mecanismo parecido, functools.cache, que sí se puede ejecutar aquí y enseña la idea:

IN [07]python
from functools import cache

lecturas = 0


@cache
def cargar(ruta: str) -> pd.DataFrame:
    global lecturas
    lecturas += 1
    return pd.read_csv(ruta, parse_dates=["fecha"],
                       index_col="fecha")


for _ in range(5):        # cinco «clics»
    df = cargar("sint_diario.csv")
print(f"Llamadas: 5 · lecturas del archivo: {lecturas}")
print(f"¿Mismo objeto? {cargar('sint_diario.csv') is df}")
OUT [07]Caché por argumentos
Llamadas: 5 · lecturas del archivo: 1
¿Mismo objeto? True

Cinco llamadas, una sola lectura. La segunda línea muestra la diferencia importante con Streamlit: functools.cache devuelve el mismo objeto cada vez. Si un trozo del código hiciera df["cierre"] *= 1.1 para un cálculo cualquiera, la caché quedaría modificada para todas las llamadas siguientes. st.cache_data lo evita: guarda el resultado serializado (con pickle) y entrega una copia a cada llamada. Lo que la función devuelva tiene que poder serializarse: tablas, arrays, números, listas.

Lo que no es un dato sino un recurso compartido —una conexión a una base de datos, un cliente de la API del bróker, un modelo cargado en memoria— va con st.cache_resource, que no copia: todas las sesiones usan el mismo objeto.

Tres parámetros que se usan a menudo:

ParámetroEjemploPara qué
ttlttl="1h" o ttl=3600Caducidad. Sin él, los datos cacheados no se renuevan nunca mientras el servidor siga vivo
max_entriesmax_entries=20Límite de entradas guardadas (una por combinación de argumentos)
show_spinnershow_spinner="Cargando precios…"Texto mientras la función se ejecuta la primera vez

El ttl es la decisión de trading de esta sección. Con datos diarios, una hora basta; con un panel que lee precios intradía, una caché de una hora enseña precios de hace una hora con aspecto de actuales. La caché se borra a mano con datos.clear() o con st.cache_data.clear().

Ejecutarla en local y guardar los secretos

Streamlit se instala como cualquier dependencia del proyecto (lección 0.2.2) y se arranca con streamlit run. La estructura del proyecto queda así:

uv add streamlit
uv run streamlit run app.py --server.address localhost
ArchivoContenido¿En Git?
app.pyLa interfazSí
panel_datos.pyCarga y cálculosSí
tests/test_panel_datos.pyPruebas de los cálculosSí
.streamlit/config.tomlOpciones del servidor y del temaSí
.streamlit/secrets.tomlClaves de API, contraseñasNo (en .gitignore)

streamlit run arranca un servidor web y abre el navegador en http://localhost:8501 (8501 es el puerto por defecto). Si no se fija una dirección, Streamlit muestra además una «Network URL»: el panel queda abierto a los demás equipos de tu red, también a los de una wifi compartida; --server.address localhost (o address = "localhost" en la sección [server] de .streamlit/config.toml) lo limita a tu equipo. Mientras desarrollas, cada vez que guardas app.py Streamlit ofrece volver a ejecutarlo; «Always rerun» lo hace solo.

En cuanto el panel lee datos de un proveedor o de un bróker, necesita una clave. Streamlit la lee de .streamlit/secrets.toml (en la carpeta desde la que se lanza la app; también existe uno global en ~/.streamlit/secrets.toml):

IN [08].streamlit/secrets.tomltoml
[proveedor]
api_key = "pon-aqui-tu-clave"
IN [09]python
clave = st.secrets["proveedor"]["api_key"]

st.secrets se lee como un diccionario. El archivo va en .gitignore desde el primer commit: una clave que llega a GitHub sigue en el historial aunque se borre después, y hay que revocarla (lección 0.2.5). Las variables de entorno y los archivos .env de la lección 0.2.4 siguen siendo válidos; secrets.toml es la forma propia de Streamlit, y la que usan sus servicios de despliegue.

Ejercicios

  1. Añade a panel_datos.py una función rentabilidad_por_anio(cierre) que devuelva la rentabilidad de cada año natural (del último cierre del año anterior al último del año). Pruébala con TECH. ¿Qué devuelve para el primer año, que no tiene año anterior?
Solución
IN [10]python
def rentabilidad_por_anio(cierre: pd.Series) -> pd.Series:
    """Rentabilidad de cada año natural, de cierre a cierre."""
    fin_de_anio = cierre.groupby(cierre.index.year).last()
    return fin_de_anio.pct_change().dropna()


rentabilidad_por_anio(cierres["TECH"]).round(3)
OUT [10]Rentabilidad por año
fecha
2022    0.372
2023    0.131
2024    0.064
2025   -0.104
Name: TECH, dtype: float64

Para 2025 sale un −10,4 %, no el −9,25 % del panel: aquí la base es el último cierre de 2024; en el panel, el primer cierre del periodo elegido. Las dos cifras son correctas, y por eso el panel dice de qué fecha a qué fecha mide. El primer año desaparece: no hay cierre del año anterior con el que compararlo, y pct_change() deja un NaN que dropna() quita. Usar el primer cierre de 2021 como base sería posible, pero daría una rentabilidad de un año incompleto con la misma etiqueta que las demás. Si el panel la enseña, que lo diga.

  1. Un compañero propone quitar @st.cache_data de datos() «porque el CSV es pequeño». ¿Qué pasa cada vez que el usuario mueve el selector de fechas? ¿Y si mañana cargar_cierres() descarga los precios de una API de pago por llamada?
Solución

Sin caché, cada movimiento del selector vuelve a ejecutar el script y, con él, la lectura de los dos CSV. Con un archivo pequeño apenas se nota. Con una API, cada clic es una llamada: el panel se vuelve lento, puede superar el límite de peticiones del proveedor y, si se paga por llamada, cuesta dinero. La caché con un ttl razonable (una hora para datos diarios) hace una llamada por hora sea cual sea el número de clics.

  1. Escribe una función alinear(cierres, base) que reescale todas las columnas para que valgan base el primer día, y úsala con los cinco activos en 2025. ¿Para qué sirve en el panel?
Solución
IN [11]python
def alinear(cierres: pd.DataFrame, base: float = 100) -> pd.DataFrame:
    """Reescala cada columna para que empiece en `base`."""
    return cierres / cierres.iloc[0] * base


anio = cierres.loc[pd.Timestamp("2025-01-01"):]
alinear(anio).iloc[-1].sort_values().round(1)
OUT [11]Comparar activos en base 100
TECH     90.8
RFIJ    102.0
RVAR    102.6
SINT    104.4
ORO     109.4
Name: 2025-12-31 00:00:00, dtype: float64

Con precios de niveles distintos (RFIJ cerca de 100, ORO por encima de 170), un gráfico con los cinco no se puede comparar. En base 100, cada línea dice cuánto ha ganado o perdido desde el mismo día, y un st.multiselect para elegir qué activos se dibujan completa el panel.

Respuesta plegada en cada pregunta

Autoevaluación

  1. En una app de Streamlit, el usuario cambia el activo en un selectbox. ¿Qué se ejecuta?

    • ASolo la línea del selectbox
    • BSolo las funciones que usan el activo
    • CEl script completo, de arriba abajo, con el nuevo valor del widget
    • DNada hasta que el usuario recarga la página
    Ver respuesta

    Respuesta C. El script completo, de arriba abajo, con el nuevo valor del widget

    Cada interacción con un widget vuelve a ejecutar el script entero. Por eso lo caro (leer datos, descargar, entrenar) se cachea: sin caché, cada clic repetiría la carga.

  2. ¿Qué diferencia hay entre st.cache_data y st.cache_resource?

    • ANinguna, son alias
    • Bst.cache_data guarda datos serializables y devuelve una copia a cada llamada; st.cache_resource guarda un objeto compartido, como una conexión, sin copiarlo
    • Cst.cache_data guarda en disco y st.cache_resource en memoria
    • Dst.cache_resource solo funciona con DataFrames
    Ver respuesta

    Respuesta B. st.cache_data guarda datos serializables y devuelve una copia a cada llamada; st.cache_resource guarda un objeto compartido, como una conexión, sin copiarlo

    Un DataFrame de precios es un dato: se guarda serializado y cada llamada recibe su copia, así que modificarla no estropea la caché. Una conexión a una base de datos o a un bróker es un recurso: se comparte entre sesiones y no se copia.

  3. El panel tiene que leer la clave API de un proveedor de datos. ¿Dónde va?

    • AEn una constante al principio de app.py
    • BEn .streamlit/secrets.toml, que está en .gitignore, y se lee con st.secrets
    • CEn el README.md del repositorio, para que el equipo la tenga
    • DEn un comentario del código
    Ver respuesta

    Respuesta B. En .streamlit/secrets.toml, que está en .gitignore, y se lee con st.secrets

    El archivo de secretos queda fuera del control de versiones y la app lo lee con st.secrets. Una clave escrita en el código acaba en el historial de Git aunque se borre después (lección 0.2.5).

  4. ¿Por qué las funciones de cálculo del panel están en panel_datos.py y no dentro de app.py?

    • APorque Streamlit no admite funciones en app.py
    • BPara que el panel cargue más rápido
    • CPara poder probarlas con pytest o en un notebook sin arrancar la interfaz, y reutilizarlas en otros scripts
    • DPorque st.cache_data solo funciona con funciones importadas
    Ver respuesta

    Respuesta C. Para poder probarlas con pytest o en un notebook sin arrancar la interfaz, y reutilizarlas en otros scripts

    La lógica que calcula rentabilidades o drawdowns es la que puede tener errores caros. Fuera de la interfaz se prueba como cualquier función; app.py queda como una capa fina que solo pinta.

Ideas clave

  • Streamlit ejecuta el script entero, de arriba abajo, cada vez que el usuario toca un widget. Todo lo demás se deduce de esa regla.
  • Las funciones de datos van en un módulo propio, sin import streamlit, y se prueban como cualquier otra función.
  • st.cache_data evita repetir lo caro: guarda el resultado según los argumentos y devuelve una copia; ttl decide cuánto vive.
  • Los widgets devuelven tipos de Python (datetime.date, tuplas de 0 a 2 fechas): conviértelos y valida el caso incompleto antes de usarlos.
  • En local, --server.address localhost deja el panel solo para tu equipo; las claves van en .streamlit/secrets.toml, fuera de Git.

Recursos