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.
- Módulo
- 0.2 · Entorno de trabajo moderno
- Duración
- 40 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
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ón | Linux y macOS | Windows (PowerShell) |
|---|---|---|
| ¿En qué carpeta estoy? | pwd | pwd |
| Listar el contenido | ls, ls -la (también ocultos) | ls, ls -Force |
| Entrar en una carpeta / subir una | cd datos, cd .. | cd datos, cd .. |
| Crear una carpeta | mkdir -p datos/brutos | mkdir datos\brutos |
| Ver un fichero de texto | cat .gitignore, head -5 sint.csv | cat .gitignore, Get-Content sint.csv -Head 5 |
| Copiar / borrar | cp a b, rm a | cp a b, rm a |
| Parar el programa en marcha | Ctrl+C | Ctrl+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 paqueteTres 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:
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)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.
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())17 19
ticks.csv
C:/nuevos/ticks.csvLa 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:
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())True
False
False
TrueEl 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:
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ónEn 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:
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 defectopapel
None
25000os.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:
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 %True
False
500.0bool("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:
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:
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))['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:
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)}")BROKER_API_KEY pk_***
BROKER_API_SECRET sk_***
PPT_MODO papel
PPT_CAPITAL 25000
PPT_RIESGO_MAX 0.02Tres 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:
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:
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}")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_*** .envCada 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.sistemaLas 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
- En la terminal, sin usar el explorador de archivos: crea dentro de
ppt-cursolas carpetasdatos/brutosydatos/procesados, copia endatos/brutosel ficherosint_diario.csvde 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.
- Amplía
cargar_configpara que falle con un mensaje claro si falta una variable obligatoria (por ejemplo,BROKER_API_KEY) y para que conviertaPPT_CAPITALyPPT_RIESGO_MAXafloat, comprobando que el riesgo está entre 0 y 0,05.
Solución
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"])50000.0 0.02El 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 %.
- Escribe el
.env.exampleque acompañaría al.envde la lección. ¿Qué diferencia hay entre los dos y cuál va a Git?
Solución
# 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.02El .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
Tu script lee
datos/sint_diario.csvy funciona cuando lo lanzas desde la carpeta del proyecto. Programado con cron, falla conFileNotFoundError. ¿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.
¿Qué devuelve
bool(os.environ.get("PPT_REAL", "false"))si la variable no existe?- A
False - B
True, porque"false"es una cadena no vacía - C
None - DUn error
Ver respuesta
Respuesta B.
True, porque"false"es una cadena no vacíaLas 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_boolen la lección. Un error así puede poner un sistema a operar en real.- A
La variable
PPT_CAPITALvale10000en el entorno y25000en el.env. Con la precedencia de la lección, ¿qué capital usa el sistema?- A25000, porque el
.enves 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
.envEl
.envda 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-filey la libreríapython-dotenv.- A25000, porque el
Has subido por error un
.envcon 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
pathliba 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.