Mêmes entrées, même réponse
Un résultat est reproductible quand vous, un collègue ou un relecteur pouvez relancer le même projet plus tard et obtenir les mêmes chiffres. Un backtest qui ne peut pas être reconstruit ne peut être ni audité, ni débogué, ni cru. Les résultats dérivent pour quelques raisons : nombres aléatoires, versions de bibliothèques différentes, données modifiées ou éditées à la main, réglages éparpillés dans le code et état caché, comme un notebook exécuté dans le désordre. Un flux reproductible supprime chaque source de dérive et consigne ce qui a été utilisé.
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.324Les graines, et à quel générateur elles appartiennent
Les simulations et le bootstrap utilisent des nombres aléatoires, qui sont en réalité une suite déterministe partant d'une graine (seed). Fixez la graine et l'exécution se répète à l'identique. random.seed(42) initialise le seul générateur global partagé par tout le programme, si bien que tout autre code qui tire des nombres décale votre suite. Il est plus sûr de créer votre propre générateur, rng = random.Random(42) (ou numpy.random.default_rng(42)), de le transmettre et d'y puiser. Vos résultats ne dépendent alors que de votre graine, et votre initialisation ne perturbe pas l'aléa des autres.
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 itEmpreintes pour les données et les réglages
Comment savoir que le fichier de données est bien celui d'il y a un mois ? Calculez une somme de contrôle : un hachage comme SHA-256 transforme n'importe quel fichier en une courte chaîne de longueur fixe qui change si un seul octet change. hashlib.sha256(data).hexdigest() la fournit ; lisez les gros fichiers par blocs. Les réglages méritent le même traitement : sérialisez la configuration avec des clés triées (json.dumps(config, sort_keys=True)), hachez ce texte, et l'empreinte est identique pour deux configurations équivalentes, quel que soit l'ordre des clés, et différente dès qu'une valeur change.
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})) # FalseNotez ce que vous avez exécuté
Chaque exécution devrait laisser un petit manifeste à côté de ses résultats : la graine, l'empreinte de la configuration, les versions de Python et des bibliothèques, la somme de contrôle de chaque fichier d'entrée et le commit Git du code. Avec cette trace, vous pouvez dire exactement ce qui a produit un chiffre et le reconstruire plus tard. Combinez-la avec les habitudes précédentes : un environnement figé dans requirements.txt, le code dans Git, les réglages dans un seul fichier, une commande qui exécute tout des données brutes aux tableaux finaux, et aucune modification manuelle des données entre-temps.
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
# }- Utilisez un
random.Random(seed)privé pour que les résultats ne dépendent que de votre graine. - Empreinte pour les données (somme de contrôle d'un fichier) et les réglages (JSON trié, puis SHA-256).
- Enregistrez un manifeste à chaque exécution ; environnement figé + commit Git + une commande pour reconstruire.
À suivre : classes et objets, pour regrouper des données et les fonctions qui les traitent en une unité propre.
Lab : ce qui est noté
- Générer des rendements aléatoires répétables avec
seeded_returns25 pts - Prendre l'empreinte d'un fichier de données avec
file_checksum25 pts - Prendre l'empreinte d'une configuration avec
config_fingerprint25 pts - Consigner ce qu'une exécution a utilisé avec
run_manifest25 pts