Mismas entradas, misma respuesta
Un resultado es reproducible cuando tú, un colega o un revisor pueden ejecutar el mismo proyecto más tarde y obtener los mismos números. Un backtest que no se puede reconstruir no se puede auditar, depurar ni creer. Los resultados derivan por unas pocas razones: números aleatorios, versiones distintas de bibliotecas, datos que cambiaron o se editaron a mano, ajustes dispersos por el código y estado oculto, como un cuaderno ejecutado en desorden. Un flujo reproducible elimina cada fuente de deriva y registra lo que se usó.
import random
# Same seed, same "random" numbers
a = random.Random(42)
b = random.Random(42)
print([round(a.random(), 3) for _ in range(3)]) # [0.639, 0.025, 0.275]
print([round(b.random(), 3) for _ in range(3)]) # [0.639, 0.025, 0.275]
# A different seed gives a different but equally repeatable sequence
print(round(random.Random(7).random(), 3)) # 0.324Las semillas y qué generador las posee
Las simulaciones y el bootstrap usan números aleatorios, que en realidad son una secuencia determinista que parte de una semilla (seed). Fija la semilla y la ejecución se repite exactamente. random.seed(42) inicializa el único generador global que comparte todo el programa, así que cualquier otro código que saque números desplaza tu secuencia. Es más seguro crear tu propio generador, rng = random.Random(42) (o numpy.random.default_rng(42)), pasarlo y sacar números de él. Así tus resultados dependen solo de tu semilla y tu inicialización no altera la aleatoriedad de los demás.
import random
# The global generator is shared: seeding it changes everyone's numbers
random.seed(1)
first = random.random()
random.seed(1)
print(random.random() == first) # True: global seeding repeats...
# ...but a private generator leaves the global one untouched
random.seed(1)
before = random.getstate()
mine = random.Random(99)
mine.random()
print(random.getstate() == before) # True: your draws did not disturb itHuellas para datos y ajustes
¿Cómo sabes que el archivo de datos es el que usaste el mes pasado? Calcula una suma de verificación: un hash como SHA-256 convierte cualquier archivo en una cadena corta de longitud fija que cambia si cambia un solo byte. hashlib.sha256(data).hexdigest() la da; lee los archivos grandes por bloques. Los ajustes merecen el mismo trato: serializa la configuración con claves ordenadas (json.dumps(config, sort_keys=True)), aplica el hash a ese texto y la huella es idéntica para dos configuraciones equivalentes, sea cual sea el orden de las claves, y distinta en cuanto cambia un valor.
import hashlib
import json
# A checksum of bytes
print(hashlib.sha256(b"AAPL,184.25").hexdigest()[:12]) # 06ea4aa73ec1 (12 characters of 64)
# A fingerprint of settings, independent of key order
def fingerprint(config):
text = json.dumps(config, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(text.encode("utf-8")).hexdigest()[:12]
one = {"fee": 0.001, "window": 20}
two = {"window": 20, "fee": 0.001}
print(fingerprint(one) == fingerprint(two)) # True
print(fingerprint(one) == fingerprint({"fee": 0.002, "window": 20})) # FalseAnota lo que ejecutaste
Cada ejecución debería dejar un pequeño manifiesto junto a sus resultados: la semilla, la huella de la configuración, las versiones de Python y de las bibliotecas, la suma de verificación de cada archivo de entrada y el commit de Git del código. Con ese registro puedes decir exactamente qué produjo un número y reconstruirlo más tarde. Combínalo con los hábitos anteriores: un entorno fijado en requirements.txt, el código en Git, los ajustes en un solo archivo, un comando que ejecute todo desde los datos en bruto hasta las tablas finales y ninguna edición manual de los datos por el camino.
import json
import platform
# A manifest records what a run used
manifest = {
"seed": 42,
"config_fingerprint": "a1b2c3d4e5f6",
"python": platform.python_version().split(".")[0] + ".x",
"inputs": {"prices.csv": "9f86d081884c"},
}
print(json.dumps(manifest, indent=2, sort_keys=True))
# {
# "config_fingerprint": "a1b2c3d4e5f6",
# "inputs": {
# "prices.csv": "9f86d081884c"
# },
# "python": "3.x",
# "seed": 42
# }- Usa un
random.Random(seed)propio para que los resultados dependan solo de tu semilla. - Huella para datos (suma de verificación de un archivo) y ajustes (JSON ordenado y luego SHA-256).
- Guarda un manifiesto en cada ejecución; entorno fijado + commit de Git + un comando para reconstruir.
A continuación: clases y objetos, para agrupar datos y las funciones que los manejan en una unidad ordenada.
Lab: qué se califica
- Generar rendimientos aleatorios repetibles con
seeded_returns25 pts - Sacar la huella de un archivo de datos con
file_checksum25 pts - Sacar la huella de una configuración con
config_fingerprint25 pts - Registrar lo que usó una ejecución con
run_manifest25 pts