4. Determinism: two runs giving exactly the same thing
Level 7: From the notebook to the process
A result you cannot obtain again is not a result: it is an anecdote. If today you are unable to reproduce the signal you sent to the market three weeks ago, you cannot audit it, you cannot debug it and, above all, you cannot claim that your live system runs the same strategy you tested.
The four leaks of determinism
A process is deterministic when, given the same declared inputs, it always produces the same output. The inputs that sneak in undeclared are almost always these four:
- Randomness. Any resampling, random initialization or simulation introduces a sequence that changes on every run unless the seed is fixed.
- Versions. One function changing its behavior between two minor versions of a library is enough to move a number.
- Time. Reading the clock or the date from inside the computation means feeding in a different input every time it runs.
- Data. The provider revises the history, corrects a dividend, fills a gap. The file you downloaded yesterday and today's are not the same one.
The first three are closed off with discipline and cost you an afternoon. The fourth is the expensive one, because it does not depend on you: it forces you to keep your own copy of what was used, identified by a fingerprint of its contents, and to have the log record which copy fed each decision. Without that, six months from now you will not be able to tell a bug in your code from a silent revision by the provider, which is exactly the argument you never want to have blind.
Seeds and versions: the easy part
Fixing the seed is cheap and must be done in a single place, the entry point, and never inside a function that gets called many times:
# ejecutar.py
import random
import numpy as np
SEMILLA = 20240115
def preparar_azar(semilla=SEMILLA):
random.seed(semilla)
np.random.seed(semilla)Pinning versions is just as cheap: instead of asking for the library plain, you fix the exact version of every dependency, indirect ones included, and that file is versioned alongside the code. Installing the latest available means tomorrow's machine will not run the same program as today's, even though your code has not changed by a single comma.
Time is an input, not an ambient fact
This is the point most people get wrong. Compare:
# Wrong: the function reads the clock internally
from datetime import datetime
def es_dia_operable(calendario):
hoy = datetime.now().date()
return hoy in calendario
# Right: the instant comes in through the door, like any other input
def es_dia_operable(calendario, momento):
return momento.date() in calendarioThe first version cannot be tested. You cannot write a test that checks what it does on Christmas Eve, nor reproduce what it decided on a Tuesday in March, because its answer depends on when you run it: the same call gives different results today and tomorrow without anybody having touched anything. The second is tested in one line and reproduces every time, because the instant travels with the arguments.
The general rule: the time comes in at the entry point, is frozen once and is passed downwards. The whole system operates on that single instant, which also eliminates an entire family of subtle failures, such as two parts of the program crossing midnight at different moments.
# ejecutar.py
from datetime import datetime, timezone
def main():
args = parsear_argumentos()
ahora = (datetime.fromisoformat(args.momento) if args.momento
else datetime.now(timezone.utc))
preparar_azar()
precios = cargar_precios(args.simbolo, hasta=ahora)
decision = calcular_senal(precios)
anotar('decision', momento=ahora.isoformat(), decision=decision)That optional argument turns reproducing a past day into a trivial exercise and, along the way, makes the same entry point usable for walking through the history.
Reproducing means being able to recompute, not being able to look up
Recording in the log which signal went out each day is indispensable, but it is not determinism. The log tells you what happened; determinism lets you obtain it again. For that you have to record, alongside the result, everything that was needed to produce it: frozen instant, code version, dependency versions, seed, identifier of the dataset used and effective configuration. With that, a doubtful result can be redone months later. Without it, the only possible answer to a discrepancy is a shrug.
Real case: February 29, 2012
That day Microsoft's cloud service, Azure, suffered a long outage that affected customers worldwide for hours. The root cause, published by the company itself in its subsequent analysis, was straight out of a textbook: when creating a virtual machine, a component generated a certificate valid for one year, computing the expiry date as the same day of the following year. Run on February 29, 2012, that calculation asked for February 29, 2013, a date that does not exist. The process failed, the retry failed the same way, and the failure spread through the rest of the system.
Nobody wrote that formula badly out of carelessness: it worked perfectly on 1,460 out of every 1,461 days. It failed because the date was a hidden input that no test had been able to fix.
And a second story, with no stock market involved but very illustrative: in 2010 a heavily cited academic paper on debt and growth argued that countries with debt above 90% of GDP grew on average by a negative 0.1%. In 2013, researchers who asked for the original spreadsheet found a badly selected range that left several countries out; with the calculation corrected, that average became positive, around 2.2%. The error took three years to surface for a very simple reason: until somebody reproduced the calculation, there was no way of seeing it. This is education, not investment advice, but the operational moral holds all the same: if nobody can recompute your number, nobody can correct it.