Lección 0.2.4 · 40 min

Terminal básica, rutas, variables de entorno y ficheros .env

Un sistema de trading se arranca desde una terminal, lee datos de una ruta y recibe sus claves por el entorno. Esta lección cubre los tres frentes y termina con un lector de .env escrito a mano que nunca enseña un secreto.

04/06
Módulo
0.2 · Entorno de trabajo moderno
Duración
40 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

La terminal en cinco minutos

La terminal es un programa que lee órdenes escritas y las ejecuta. Ya la has usado para instalar uv (lección 0.2.1) y crear el proyecto (lección 0.2.2). En el trading sistemático es la herramienta diaria: arranca el backtest, lanza el sistema en un servidor, lee sus logs y lo para.

Dentro de la terminal se ejecuta un intérprete de órdenes (shell): bash o zsh en Linux y macOS, PowerShell en Windows. Cada uno tiene su sintaxis, pero los comandos básicos coinciden casi siempre, porque PowerShell define alias con los nombres de Unix:

AcciónLinux y macOSWindows (PowerShell)
¿En qué carpeta estoy?pwdpwd
Listar el contenidols, ls -la (también ocultos)ls, ls -Force
Entrar en una carpeta / subir unacd datos, cd ..cd datos, cd ..
Crear una carpetamkdir -p datos/brutosmkdir datos\brutos
Ver un fichero de textocat .gitignore, head -5 sint.csvcat .gitignore, Get-Content sint.csv -Head 5
Copiar / borrarcp a b, rm acp a b, rm a
Parar el programa en marchaCtrl+CCtrl+C

Una sesión típica en el proyecto del curso:

cd ~/ppt-curso                 # ~ es tu carpeta personal
ls                             # pyproject.toml, src, datos...
head -3 datos/brutos/sint_diario.csv
uv run python -m ppt.backtest  # ejecuta un módulo de tu paquete

Tres hábitos ahorran horas: el tabulador completa nombres de ficheros y comandos; la flecha arriba recupera los comandos anteriores; y Ctrl+C interrumpe el programa en marcha, que es la forma normal de parar un backtest que tarda demasiado o un sistema en papel.

Rutas: absolutas y relativas

Una ruta dice dónde está un fichero. Es absoluta si empieza en la raíz del disco (/home/ana/ppt-curso/… en Linux, C:\Users\ana\ppt-curso\… en Windows) y relativa si se interpreta desde la carpeta actual (datos/sint_diario.csv). Python tiene un módulo para manejarlas sin pelearse con barras y separadores, pathlib:

IN [01]python
from pathlib import Path

ruta = Path("datos") / "brutos" / "sint_diario.csv"
print(ruta)
print(ruta.name, "|", ruta.stem, "|", ruta.suffix)
print(ruta.parent)
print(ruta.parts)
OUT [01]Una ruta con pathlib
datos/brutos/sint_diario.csv
sint_diario.csv | sint_diario | .csv
datos/brutos
('datos', 'brutos', 'sint_diario.csv')

El operador / une trozos de ruta con el separador del sistema: / en Linux y macOS, \ en Windows. Por eso es mejor que escribir rutas a mano como texto. En Windows, además, la barra invertida tiene una trampa: dentro de una cadena de Python, \n es un salto de línea y \t un tabulador.

IN [02]python
from pathlib import PureWindowsPath

mal = "C:\nuevos\ticks.csv"          # \n y \t son caracteres
bien = r"C:\nuevos\ticks.csv"        # r"" = barras literales
print(len(mal), len(bien))
print(PureWindowsPath(bien).name)
print(PureWindowsPath(bien).as_posix())
OUT [02]La barra invertida de Windows
17 19
ticks.csv
C:/nuevos/ticks.csv

La primera cadena tiene dos caracteres menos: \n y \t se han convertido en un salto de línea y un tabulador, y el fichero ticks.csv no aparecerá nunca. Con el prefijo r (cadena «cruda») o con pathlib el problema desaparece. PureWindowsPath permite manejar rutas de Windows desde cualquier sistema; en tu código basta con Path.

El problema más frecuente con las rutas no es la barra, sino la carpeta actual. Una ruta relativa se resuelve contra la carpeta desde la que se lanzó el programa, no contra la del fichero de código. El código del curso se ejecuta en una carpeta que contiene los datos sueltos, así que:

IN [03]python
print(Path("sint_diario.csv").exists())
print(Path("datos/sint_diario.csv").exists())
print(Path("sint_diario.csv").is_absolute())
print(Path("sint_diario.csv").resolve().is_absolute())
OUT [03]Relativa a la carpeta actual
True
False
False
True

El mismo script, lanzado desde otra carpeta, por cron o por un servicio del servidor, no encontraría el fichero. La solución es anclar las rutas a algo fijo. En un fichero .py, la variable __file__ es la ruta del propio fichero; a partir de ella se sube hasta la raíz del proyecto:

IN [04]src/ppt/rutas.pypython
from pathlib import Path

# src/ppt/rutas.py -> subir dos niveles = raíz del proyecto
RAIZ = Path(__file__).resolve().parents[2]
DATOS_BRUTOS = RAIZ / "datos" / "brutos"
DATOS_PROCESADOS = RAIZ / "datos" / "procesados"

El resto del código importa DATOS_BRUTOS y no vuelve a escribir una ruta. Si mañana los datos se mueven a otro disco, se cambia una línea. Los ficheros CSV, Parquet y JSON con pathlib se ven en la lección 0.4.4.

Variables de entorno

Cada programa recibe al arrancar un conjunto de pares NOMBRE=valor: sus variables de entorno. Algunas las pone el sistema (PATH, la lista de carpetas donde la terminal busca los programas; por eso tras instalar uv había que reabrir la terminal), y otras las pones tú. Son la forma estándar de pasar configuración a un programa sin tocar su código: el modo de operación, el capital, la clave del bróker.

Se definen en la terminal antes de arrancar el programa:

export PPT_MODO=papel                  # Linux y macOS, para la sesión
$env:PPT_MODO = "papel"                # PowerShell, para la sesión
PPT_MODO=real uv run python sistema.py # Linux y macOS, solo esa ejecución

En Python se leen con os.environ, que funciona como un diccionario. Aquí la variable se crea desde Python para que la celda sea reproducible; en tu sistema vendría de la terminal:

IN [05]python
import os

os.environ["PPT_MODO"] = "papel"      # en la vida real: export

print(os.environ.get("PPT_MODO"))
print(os.environ.get("PPT_CAPITAL"))             # no existe
print(os.environ.get("PPT_CAPITAL", "25000"))    # por defecto
OUT [05]Leer variables de entorno
papel
None
25000

os.environ["PPT_CAPITAL"], con corchetes, lanzaría un KeyError si la variable no existe; .get() devuelve None o el valor por defecto. Usa corchetes para lo obligatorio (que falle pronto) y .get() con un valor por defecto seguro para lo opcional. En trading, seguro significa conservador: si falta el modo, papel, no real.

Las variables de entorno son siempre texto. Un número hay que convertirlo, y un sí/no tiene trampa:

IN [06]python
os.environ["PPT_REAL"] = "false"
print(bool(os.environ["PPT_REAL"]))          # ¡cuidado!

def leer_bool(nombre: str, defecto: bool = False) -> bool:
    valor = os.environ.get(nombre)
    if valor is None:
        return defecto
    return valor.strip().lower() in {"1", "true", "si", "sí", "yes"}

print(leer_bool("PPT_REAL"))
capital = float(os.environ.get("PPT_CAPITAL", "25000"))
print(capital * 0.02)                        # riesgo máximo del 2 %
OUT [06]Siempre texto
True
False
500.0

bool("false") es True, porque cualquier cadena no vacía es verdadera. Un sistema que decide si opera en real con bool(os.environ[...]) operaría en real con PPT_REAL=false. leer_bool solo acepta un «sí» explícito; todo lo demás, incluida una errata, cuenta como no.

Ficheros .env

Definir diez variables a mano cada vez que abres la terminal no es práctico. La convención es guardarlas en un fichero .env en la raíz del proyecto, una por línea, que el programa lee al arrancar. El .env es de tu máquina: tiene tus claves y tu configuración, está en .gitignore desde el primer commit (lección 0.1.3) y no se comparte. Lo que sí se versiona es un .env.example con los nombres y sin los valores.

Existen librerías para leerlo (python-dotenv, y Pydantic en la lección 0.4.2), pero el formato es tan simple que merece la pena escribir el lector a mano para entender qué hacen. Primero, un .env de ejemplo con claves inventadas:

IN [07]python
Path(".env").write_text("""\
# Bróker (cuenta de papel)
BROKER_API_KEY=pk_demo_8f3a91c2e7b4
BROKER_API_SECRET="sk_demo_41d0c9e2a7f35b18"
export PPT_MODO=papel
PPT_CAPITAL=25000

PPT_RIESGO_MAX=0.02
""");

El lector: ignora líneas vacías y comentarios, admite el prefijo export (para que el mismo fichero sirva en bash), separa por el primer = y quita las comillas que rodean el valor:

IN [08]python
def leer_env(ruta: Path) -> dict[str, str]:
    valores = {}
    for linea in ruta.read_text().splitlines():
        linea = linea.strip()
        if not linea or linea.startswith("#"):
            continue
        linea = linea.removeprefix("export ")
        clave, igual, valor = linea.partition("=")
        if not igual:
            continue                    # línea sin '=': se ignora
        valor = valor.strip()
        comillas = valor[:1] in ("'", '"')
        if comillas and len(valor) >= 2 and valor[-1] == valor[0]:
            valor = valor[1:-1]
        valores[clave.strip()] = valor
    return valores

env = leer_env(Path(".env"))
print(list(env))
OUT [08]Un lector de .env
['BROKER_API_KEY', 'BROKER_API_SECRET', 'PPT_MODO', 'PPT_CAPITAL', 'PPT_RIESGO_MAX']

La celda imprime solo los nombres, no los valores. Es deliberado: un valor impreso acaba en la salida del notebook, en un log o en una captura de pantalla. Cuando haga falta mostrar la configuración (por ejemplo, en el log de arranque del sistema), los secretos se enmascaran:

IN [09]python
SENSIBLES = ("KEY", "SECRET", "TOKEN", "PASSWORD")

def enmascarar(clave: str, valor: str) -> str:
    if any(s in clave.upper() for s in SENSIBLES):
        return valor[:3] + "***" if len(valor) > 12 else "***"
    return valor

for clave, valor in env.items():
    print(f"{clave:<18} {enmascarar(clave, valor)}")
OUT [09]Mostrar la configuración sin secretos
BROKER_API_KEY     pk_***
BROKER_API_SECRET  sk_***
PPT_MODO           papel
PPT_CAPITAL        25000
PPT_RIESGO_MAX     0.02

Tres caracteres bastan para distinguir una clave de papel (pk_) de otra sin revelar nada útil.

Quién gana: entorno, .env y valores por defecto

Un mismo programa se ejecuta en tu portátil, en un servidor y en los tests. Cada sitio necesita ajustar algo sin tocar el código ni el .env de los demás. La convención, que siguen python-dotenv y uv run --env-file, es una cadena de precedencia en la que cada capa sobrescribe a la anterior:

Por defectoFichero .envEntornoConfiguración
Fuente Mermaid
flowchart TB
  D[Por defecto] --> E[Fichero .env]
  E --> V[Entorno]
  V --> C[Configuración]

Los valores por defecto están en el código y son conservadores; el .env los ajusta para tu máquina; una variable de entorno los cambia para una ejecución concreta. Con el lector anterior, la carga completa queda así, anotando de dónde sale cada valor:

IN [10]python
DEFECTO = {"PPT_MODO": "papel", "PPT_CAPITAL": "10000",
           "PPT_RIESGO_MAX": "0.01", "PPT_LOG": "INFO"}

def cargar_config(ruta: Path) -> dict[str, tuple[str, str]]:
    config = {k: (v, "defecto") for k, v in DEFECTO.items()}
    for k, v in leer_env(ruta).items():
        config[k] = (v, ".env")
    for k in config:
        if k in os.environ:
            config[k] = (os.environ[k], "entorno")
    return config

os.environ["PPT_CAPITAL"] = "50000"   # export PPT_CAPITAL=50000
for clave, (valor, origen) in cargar_config(Path(".env")).items():
    print(f"{clave:<18} {enmascarar(clave, valor):<10} {origen}")
OUT [10]Precedencia: entorno > .env > por defecto
PPT_MODO           papel      entorno
PPT_CAPITAL        50000      entorno
PPT_RIESGO_MAX     0.02       .env
PPT_LOG            INFO       defecto
BROKER_API_KEY     pk_***     .env
BROKER_API_SECRET  sk_***     .env

Cada valor lleva su origen: cuando un sistema hace algo inesperado, saber de dónde salió el capital o el modo es la mitad del diagnóstico. PPT_MODO sale del entorno porque una celda anterior la definió con os.environ; es el mismo efecto que tendría un export olvidado en la terminal.

En tus scripts no hace falta llamar a un lector propio: uv carga el .env al arrancar el programa si se lo pides.

uv run --env-file .env python -m ppt.backtest
uv run --env-file .env --env-file .env.servidor python -m ppt.sistema

Las variables del fichero llegan a os.environ y, como en cargar_config, una variable que ya existía en el entorno no se sobrescribe. Con varios ficheros, cada uno sobrescribe al anterior: un .env común y otro con lo propio del servidor. Lo que uv no hace es validar los valores ni enmascarar los secretos al mostrarlos; eso sigue siendo cosa de tu código, como en el segundo ejercicio.

Ejercicios

  1. En la terminal, sin usar el explorador de archivos: crea dentro de ppt-curso las carpetas datos/brutos y datos/procesados, copia en datos/brutos el fichero sint_diario.csv de los recursos y muestra sus tres primeras líneas.
Solución

En Linux y macOS: cd ~/ppt-curso, mkdir -p datos/brutos datos/procesados, cp ~/Descargas/sint_diario.csv datos/brutos/ (la carpeta de descargas depende de tu sistema) y head -3 datos/brutos/sint_diario.csv. En PowerShell: mkdir datos\brutos, mkdir datos\procesados, cp $HOME\Downloads\sint_diario.csv datos\brutos\ y Get-Content datos\brutos\sint_diario.csv -Head 3. La primera línea es la cabecera: fecha,apertura,maximo,minimo,cierre,volumen.

  1. Amplía cargar_config para que falle con un mensaje claro si falta una variable obligatoria (por ejemplo, BROKER_API_KEY) y para que convierta PPT_CAPITAL y PPT_RIESGO_MAX a float, comprobando que el riesgo está entre 0 y 0,05.
Solución
IN [11]python
OBLIGATORIAS = ("BROKER_API_KEY", "BROKER_API_SECRET")

def config_validada(ruta: Path) -> dict[str, object]:
    crudo = {k: v for k, (v, _) in cargar_config(ruta).items()}
    faltan = [k for k in OBLIGATORIAS if not crudo.get(k)]
    if faltan:
        raise RuntimeError(f"Faltan variables: {', '.join(faltan)}")
    riesgo = float(crudo["PPT_RIESGO_MAX"])
    if not 0 < riesgo <= 0.05:
        raise ValueError(f"PPT_RIESGO_MAX fuera de rango: {riesgo}")
    return crudo | {"PPT_CAPITAL": float(crudo["PPT_CAPITAL"]),
                    "PPT_RIESGO_MAX": riesgo}

cfg = config_validada(Path(".env"))
print(cfg["PPT_CAPITAL"], cfg["PPT_RIESGO_MAX"])
OUT [11]Configuración validada
50000.0 0.02

El mensaje nombra las variables que faltan, pero nunca muestra valores. Fallar al arrancar es mucho mejor que descubrir a mitad de sesión que el capital era una cadena o que el riesgo era del 20 %.

  1. Escribe el .env.example que acompañaría al .env de la lección. ¿Qué diferencia hay entre los dos y cuál va a Git?
Solución
IN [12].env.exampledotenv
# Copia este fichero a .env y rellena los valores
BROKER_API_KEY=
BROKER_API_SECRET=
PPT_MODO=papel
PPT_CAPITAL=25000
PPT_RIESGO_MAX=0.02

El .env.example tiene los mismos nombres, con los secretos vacíos y valores de ejemplo para lo que no es secreto. Va a Git: documenta qué necesita el programa. El .env, con las claves de verdad, no se versiona nunca.

Respuesta plegada en cada pregunta

Autoevaluación

  1. Tu script lee datos/sint_diario.csv y funciona cuando lo lanzas desde la carpeta del proyecto. Programado con cron, falla con FileNotFoundError. ¿Por qué?

    • APorque cron no tiene permisos para leer CSV
    • BPorque la ruta es relativa y se resuelve contra la carpeta actual, que con cron no es la del proyecto
    • CPorque pandas no funciona fuera de la terminal
    • DPorque el fichero se borró
    Ver respuesta

    Respuesta B. Porque la ruta es relativa y se resuelve contra la carpeta actual, que con cron no es la del proyecto

    Una ruta relativa se interpreta desde la carpeta de trabajo del proceso. Cron, un servicio o un editor pueden arrancar el programa desde otra carpeta. La solución es construir la ruta a partir de la ubicación del propio fichero de código o de una raíz conocida.

  2. ¿Qué devuelve bool(os.environ.get("PPT_REAL", "false")) si la variable no existe?

    • AFalse
    • BTrue, porque "false" es una cadena no vacía
    • CNone
    • DUn error
    Ver respuesta

    Respuesta B. True, porque "false" es una cadena no vacía

    Las variables de entorno son siempre texto, y cualquier cadena no vacía es verdadera. Para leer un sí/no hay que comparar el texto con valores concretos, como hace leer_bool en la lección. Un error así puede poner un sistema a operar en real.

  3. La variable PPT_CAPITAL vale 10000 en el entorno y 25000 en el .env. Con la precedencia de la lección, ¿qué capital usa el sistema?

    • A25000, porque el .env es del proyecto
    • B10000, porque el entorno tiene prioridad sobre el .env
    • CLa suma, 35000
    • DNinguno: el programa falla por el conflicto
    Ver respuesta

    Respuesta B. 10000, porque el entorno tiene prioridad sobre el .env

    El .env da valores por defecto para tu máquina; el entorno permite cambiarlos en una ejecución concreta (un servidor, un test) sin tocar ficheros. Es la misma regla que siguen uv con --env-file y la librería python-dotenv.

  4. Has subido por error un .env con la clave de tu bróker a un repositorio público y lo has borrado con otro commit cinco minutos después. ¿Qué haces?

    • ANada: ya está borrado
    • BHacer el repositorio privado
    • CRevocar la clave en el bróker y crear otra; la antigua sigue en el historial y puede haberse copiado ya
    • DCambiar el nombre del fichero
    Ver respuesta

    Respuesta C. Revocar la clave en el bróker y crear otra; la antigua sigue en el historial y puede haberse copiado ya

    Hay programas que rastrean de forma continua los repositorios públicos en busca de claves, y el commit sigue en el historial. Una clave que ha estado en Git se da por comprometida: se revoca, se crea otra y se revisa la actividad de la cuenta.

Ideas clave

  • La terminal es la forma de arrancar, parar (Ctrl+C) y automatizar programas. Pocos comandos bastan: pwd, ls, cd, mkdir, cat.
  • Una ruta relativa depende de la carpeta actual. Construye las rutas con pathlib a partir de una raíz conocida.
  • Las variables de entorno son siempre texto: convierte números y sí/no de forma explícita y da un valor por defecto seguro.
  • Precedencia: valores por defecto, después .env, después el entorno. El entorno gana.
  • Los secretos van en .env, que está en .gitignore; nunca se imprimen, ni en logs ni en notebooks.

Recursos