Arnaud Barbier
Moteur de backtestMode Research · octobre 2026

Mode Research, développer un indicateur et mesurer ses signaux

Le mode Research permet de tester une idée d'entrée avant d'en faire une stratégie. L'utilisateur écrit son indicateur en quelques lignes ; le mode mesure ce qui se passe réellement après chaque signal, le compare à des entrées tirées au hasard et à la version précédente, puis affiche le résultat sur un graphique relié au notebook.

À ce stade, aucune gestion de trade, aucun frais et aucune compilation ne sont nécessaires. L'objectif est de savoir rapidement si le signal mérite d'être développé dans le mode Algo.

0compilation pour tester une nouvelle variante
2comparaisons automatiques à chaque run : version précédente et référence
25mesures par horizon et par sens, face au hasard
4phases pour entraîner l'œil à l'aveugle
Aperçu
Déclaration d'une étude EMA 50 avec SignalSpec et ForwardEvaluationSpecDéclaration du graphique de l'étude : EMA et marqueurs long et short
L'étude. À gauche, une EMA 50 : ses signaux et ce que l'on mesure ensuite (horizons de 3, 6 et 40 barres, en unités ATR). À droite, la déclaration du graphique. Cliquez sur une capture pour l'agrandir.
Graphique du serveur Research : bougies, EMA 50 et signaux long et short sur l'or
Le graphique. Le résultat de cette étude dans le serveur local : signaux long et short sur l'or, EMA 50, et en haut les mesures du rapport (MFE, MAE et retour médians à 40 barres, en ATR).
Étude V7_BOXES : parcours des cas actifs et bouton Inspect in notebook
Les cas. Sur une autre étude en développement, chaque signal se parcourt un par un et s'ouvre dans le notebook. En haut à droite, ITER et DSR indiquent le nombre d'essais déjà comptés.
Paramétrage d'une campagne de replay à l'aveugle en quatre phases
L'entraînement. Paramétrage d'une campagne de replay à l'aveugle : quatre phases, des fenêtres réservées et des critères fixés avant le lancement.
01Écrire une étude

Déclarer un indicateur, ses signaux et ce que l'on veut mesurer

Une étude est une fonction Python qui reçoit les prix, calcule ses colonnes et renvoie des signaux long et short. Trois déclarations l'accompagnent, afin que chaque choix reste explicite :

  • SignalSpec indique comment un signal devient un événement : à la première bougie d'une séquence (rising_edge) ou à chaque bougie (every_true_bar), avec un cooldown éventuel ;
  • ForwardEvaluationSpec fixe ce que l'on mesure ensuite : point d'entrée, horizons et unité ;
  • la vue choisit les courbes, les marqueurs et les panneaux affichés sur le graphique.
Code de l'étude ema_cross_50
L'étude utilisée pour les captures de cette page : une EMA 50, mesurée à 3, 6 et 40 barres en unités ATR, sans TP ni SL.
Code de la vue de l'étude ema_cross_50
La vue de la même étude : la courbe de l'EMA et le style des marqueurs long et short.

Éviter de lire le futur

Une bougie ne peut utiliser que son passé. Une fenêtre centrée ou un shift(-1) introduit le futur sans que cela se voie dans le résultat. Pour les données d'une unité de temps supérieure, ctx.causal_feature(...) aligne chaque valeur sur la dernière bougie source clôturée. Les colonnes nommées y_* restent réservées aux labels futurs.

Itérer sans perdre le fil

Une étude peut vivre dans le notebook ou dans un fichier Python rechargé à chaque appel. Seuls le calcul, les paramètres et l'évaluation identifient une variante : changer une couleur ne crée pas un nouvel essai. Les filtres de session et de taille de bougie s'appliquent après l'émission, sans créer de nouveau signal.

02Mesurer contre le hasard

Comparer chaque signal à des entrées tirées au hasard

Un signal connu à la clôture d'une bougie est évalué à partir de l'ouverture suivante. Pour chaque horizon, le mode mesure le rendement dans le sens du signal, l'excursion favorable (MFE), l'excursion défavorable (MAE) et le franchissement de barrières. En unités ATR, l'échelle utilisée est celle connue au moment du signal.

Le même calcul est appliqué à toutes les barres éligibles de la période, avec la même proportion de long et de short. L'écart entre les deux, le lift, est accompagné d'un intervalle de confiance à 95 % obtenu par bootstrap par jour. Chaque ligne reçoit enfin un diagnostic : POSITIVE, NEGATIVE, INCONCLUSIVE ou INSUFFICIENT.

Signal contre entrées aléatoires, sortie du rapport
SensHorizon¹Signaux validesMoyenne signalMoyenne hasardLift moyenIC 95 % du liftDiagnostic
ALL33307-0.0117640.001692-0.013456[-0.053551 ; 0.032919]INCONCLUSIVE
LONG317960.0177600.019636-0.001876[-0.063809 ; 0.052811]INCONCLUSIVE
SHORT31511-0.046856-0.019636-0.027220[-0.091209 ; 0.042453]INCONCLUSIVE
ALL633070.0247370.0034380.021300[-0.045838 ; 0.084326]INCONCLUSIVE
LONG617960.1395850.0398890.099697[0.017798 ; 0.176226]POSITIVE
SHORT61511-0.111773-0.039889-0.071884[-0.166403 ; 0.016405]INCONCLUSIVE
ALL1233070.0546600.0068860.047773[-0.036672 ; 0.131722]INCONCLUSIVE
LONG1217960.1937160.0799050.113811[-0.008983 ; 0.229892]INCONCLUSIVE
SHORT121511-0.110625-0.079905-0.030720[-0.160339 ; 0.099989]INCONCLUSIVE
¹ Horizon en barres. Les valeurs sont reprises telles quelles de research.report().predictive, sans conversion en pourcentage. Le tableau complet, avec ses 25 colonnes, se déplie ci-dessous.

Lecture de cet exemple : seul LONG à 6 barres porte le diagnostic POSITIVE (lift de 0,099697 ; IC 95 % [0,017798 ; 0,176225]). Les huit autres lignes sont INCONCLUSIVE. Ce résultat décrit le signal évalué, sans établir la rentabilité d’une stratégie après gestion et coûts.

Tableau complet · 25 colonnes
research.report().predictive
directionhorizonsignalsvalidcoveragesignal_daysmean_rmean_r_ci_lowmean_r_ci_highmedian_rpositive_ratepositive_rate_ci_lowpositive_rate_ci_highrandom_barsrandom_mean_rrandom_positive_ratelift_mean_rlift_mean_r_ci_lowlift_mean_r_ci_highlift_positive_pplift_positive_pp_ci_lowlift_positive_pp_ci_highp_lift_gt_zerostandardized_liftedge_status
ALL3330733071.01490-0.011764-0.0519660.034240-0.0328450.4856370.4704620.5012401399450.0016920.500209-0.013456-0.0535510.032919-1.457246-2.9873820.1254000.26875-0.009910INCONCLUSIVE
LONG3179617961.012030.017760-0.0420430.075393-0.0031130.4972160.4748030.5206191399450.0196360.509472-0.001876-0.0638090.052811-1.225554-3.5338681.0770780.47375-0.001415INCONCLUSIVE
SHORT3151115111.01088-0.046856-0.1135080.025539-0.0684820.4718730.4488150.495764139945-0.0196360.489199-0.027220-0.0912090.042453-1.732640-3.9721990.5413480.19875-0.019526INCONCLUSIVE
ALL6330733071.014900.024737-0.0422580.0877610.0379330.5089200.4928760.5250711399420.0034380.5006940.021300-0.0458380.0843260.822662-0.7948022.4277980.742500.011365INCONCLUSIVE
LONG6179617961.012030.1395850.0518360.2202250.1159180.5311800.5070710.5529431399420.0398890.5124840.0996970.0177980.1762261.869666-0.3644333.9468140.995000.055145POSITIVE
SHORT6151115111.01088-0.111773-0.206676-0.017164-0.0730330.4824620.4584920.507810139942-0.0398890.486680-0.071884-0.1664030.016405-0.421825-2.8047251.9616480.07000-0.037034INCONCLUSIVE
ALL12330733071.014900.054660-0.0292370.1403440.0569530.5110370.4951030.5275791399360.0068860.5011160.047773-0.0366720.1317220.992072-0.6149702.6545180.870000.018348INCONCLUSIVE
LONG12179617961.012030.1937160.0702800.3138090.1907920.5356350.5130330.5593391399360.0799050.5156070.113811-0.0089830.2298922.002761-0.1809884.0930670.966250.043656INCONCLUSIVE
SHORT12151115111.01088-0.110625-0.2526570.024479-0.0894580.4818000.4544800.508412139936-0.0799050.483893-0.030720-0.1603390.099989-0.209250-2.6332762.3597800.33500-0.011859INCONCLUSIVE

Dans cet exemple, seul le sens LONG à 6 barres obtient un diagnostic POSITIVE (lift de 0,0997, intervalle à 95 % de 0,0178 à 0,1762). Les huit autres lignes restent INCONCLUSIVE. Le résultat porte sur le signal seul : il ne dit encore rien de la rentabilité d'une stratégie après gestion et frais.

03Comparer les versions

Retrouver à chaque modification la version précédente et la référence

Chaque run est enregistré puis comparé automatiquement à la dernière version distincte et à la référence de la série, c'est-à-dire le premier run enregistré tant qu'elle n'est pas déplacée. L'utilisateur voit ainsi immédiatement si sa modification améliore le signal, et s'il s'éloigne de son point de départ.

research.run(ema_signal, params={"ema_span": 30})   # A : la référence
research.run(ema_signal, params={"ema_span": 40})   # B
research.run(ema_signal, params={"ema_span": 50})   # C
report = research.report()
report.comparisons["previous"]   # C contre B
report.comparisons["baseline"]   # C contre A
Trois versions du même indicateur dans une même série.

Le rapport indique quels signaux sont conservés, ajoutés ou retirés, et comment les mesures évoluent. Un changement de données, de période ou d'évaluation ouvre une nouvelle série, afin de ne jamais comparer des expériences différentes.

L'historique est conservé dans une base SQLite, accompagnée d'un journal Markdown lisible : les rapports restent consultables après un redémarrage. Renommer un notebook conserve son historique ; le copier crée une nouvelle identité qui garde la trace de sa filiation.

04Juger le timing

Reconnaître une entrée plus précoce ou plus tardive sur le même mouvement

Lorsqu'une modification avance une entrée de trois bougies, une comparaison classique voit un signal supprimé et un autre ajouté. Le juge de timing rattache plutôt chaque signal au mouvement de prix auquel il appartient, puis compare les deux versions mouvement par mouvement. Chaque entrée reçoit l'une des quatre catégories suivantes :

  1. 01Anticipée

    L'entrée précède un mouvement de même sens, dans la fenêtre autorisée.

  2. 02Dans le mouvement

    L'entrée appartient au mouvement, et sa position y est mesurée.

  3. 03Tardive

    Une trop grande part du mouvement est déjà consommée, ou un repli important a déjà eu lieu.

  4. 04Hors mouvement

    Aucun mouvement compatible n'est trouvé.

research.set_movement_judge(MovementJudge())
research.run(study, params=params)
m = research.report().comparisons["previous"].movements
m.summary, m.transitions, m.worst(15)
Le rapport fait ressortir les changements de timing et les mouvements qui méritent d'être inspectés.
05Inspecter et rejouer

Un graphique relié au notebook, et un replay qui masque le futur

research.serve(run) ouvre un serveur local qui affiche le résultat déjà calculé : consulter le graphique ne relance pas l'étude.

Étude V7_BOXES dans le serveur Research : cas actifs, boutons précédent et suivant, Inspect in notebook
Une étude en développement (split DEV). Chaque cas actif se parcourt avec précédent et suivant, puis s'ouvre dans le notebook avec le contexte choisi : ici 10 barres avant et 10 barres après.

Du graphique vers le notebook

Le graphique permet de :

  • sélectionner des zones et les envoyer au notebook, qui met à jour research.query ;
  • ouvrir le contexte complet d'un signal avec « Inspect in notebook » ;
  • recevoir dans la page les filtres appliqués depuis le notebook.

Une sélection manuelle de zones est signalée comme discrétionnaire.

Rejouer sans voir la suite

Le replay simple démarre en pause, masque la bougie choisie et tout ce qui suit, puis avance signal par signal ou bougie par bougie. Les flèches permettent de naviguer et la barre d'espace lance ou suspend la lecture.

Modifier un paramètre depuis le panneau Paramètres crée une nouvelle variante en DEV. Cet éditeur est verrouillé en OOS.

06Entraîner l'œil

Mesurer une intuition avec des campagnes à l'aveugle

Un trader discrétionnaire peut avoir le sentiment de savoir reconnaître les bons signaux. Les campagnes permettent de le vérifier : un humain ou un modèle trie des signaux (Prendre ou Rejeter) sans voir la suite, puis sa sélection est comparée à des sélections aléatoires parmi les mêmes candidats.

Quatre façons de décider sont distinguées : discrétionnaire sans assistance, discrétionnaire aidé par les valeurs connues au moment du signal, filtre quantitatif et annotation rétrospective. Une annotation faite en voyant le futur n'est jamais mélangée aux résultats à l'aveugle.

Panneau Replay / apprentissage : modes de décision, buckets Prendre et Rejeter, validation par défaut et tableau des phases
Le panneau de campagne : mode de décision, buckets Prendre et Rejeter avec leurs touches, règle de validation par défaut (SL d'une ATR, seuil favorable de deux ATR, horizon de 24 barres) et taille de chaque phase.
  1. 01Mesure initiale

    Des décisions à l'aveugle pour établir le point de départ.

  2. 02Apprentissage exploratoire

    Le résultat est montré après chaque décision, dans l'ordre chronologique.

  3. 03Entraînement stabilisé

    Au moins deux blocs, pour séparer entraînement et validation.

  4. 04Évaluation finale

    À l'aveugle, sur des observations jamais vues.

Vérification des effectifs : réservation impossible, 340 signaux disponibles sur 400 demandés
Avant le lancement, le panneau vérifie les effectifs. Ici, la phase d'apprentissage demande 400 signaux alors que 340 seulement restent inédits : la réservation est refusée plutôt que réduite en silence.
Bilan partiel des quatre phases d'une campagne
Bilan partiel d'une campagne : les phases 1 et 2 sont terminées, les phases 3 et 4 restent réservées et leurs résultats protégés.

Chaque fenêtre affichée est enregistrée par date, actif et unité de temps. Le replay libre compte aussi : renommer des signaux ne rend pas une fenêtre « jamais vue ». L'export vers le machine learning exclut les phases 1 et 2, et le modèle est figé avant la phase finale.

07Figer et tester ailleurs

Figer la configuration avant d'ouvrir l'échantillon réservé

La séparation entre développement (DEV) et hors échantillon (OOS) se déclare dès la création de Research. Le freeze enregistre la configuration et les conclusions avant tout accès à la partie réservée, et le contrôle s'exécute avant le calcul OOS. Les barres antérieures peuvent servir à initialiser les indicateurs, mais seules les observations autorisées sont publiées.

research.use_insample()
run_dev = research.run(study, params=params)
research.freeze_configuration(conclusions=conclusions)
research.use_oos()
run_oos = research.run(study, params=params)
cross = research.run_cross_asset(
    study, {"XAGUSD": xag_df}, params=params,
    public_settings=PUBLIC_SETTINGS,
)
Le parcours après création de l'étude : développement, freeze, hors échantillon, puis un autre actif avec les mêmes paramètres.

Les essais sont comptés dans le même registre de validation que le mode Algo : une nouvelle décision compte, un run identique est reconnu, et regarder ou zoomer reste une simple observation. Le test sur un autre actif ne crée pas de nouvel essai, mais chaque lancement est enregistré pour que les répétitions restent visibles.

08Passer au mode Algo

Du signal mesuré à la stratégie exécutée

Research · le signal

Calculer l'indicateur, mesurer ce qui suit ses signaux, comparer les versions.

Algo · la stratégie

Choisir les entrées, gérer les positions, appliquer les frais et le sizing.

En commun : le graphique, le serveur local, le manifeste de freeze et le registre des essais.

Passer au mode Algo consiste à formaliser les entrées en setups, puis à ajouter la gestion des positions, les frais et le sizing. Les conclusions de Research doivent alors être vérifiées à nouveau : un signal qui précède bien les mouvements ne garantit pas une stratégie rentable.

Backtest du mode Algo affiché dans le serveur Research : régimes H1, entrées et sorties
Le même serveur affiche ici un backtest du mode Algo : croisement EMA 50/100 avec régime H1, 734 trades. Les zones colorées sont les régimes, les triangles les entrées et les croix les sorties.

Dans l'autre sens, @engine.research_view déclare un graphique avant le run : les bougies, les entrées et les sorties du moteur y sont ajoutées automatiquement, et engine.bind_research_view(study) peut réutiliser la vue d'une étude. Le résultat s'ouvre en lecture seule : rien n'est recalculé, aucun essai n'est compté et le freeze n'est pas modifié.

Arnaud Barbier · octobre 2026. Les captures servent à montrer le fonctionnement ; aucun résultat de performance n'est revendiqué.Dépôt public (instantané) →
Backtest engineResearch mode · October 2026

Research mode, developing an indicator and measuring its signals

Research mode lets you test an entry idea before turning it into a strategy. The user writes the indicator in a few lines; the mode measures what actually happens after each signal, compares it with randomly drawn entries and with the previous version, then shows the result on a chart linked to the notebook.

At this stage no trade management, no costs and no compilation are needed. The aim is to find out quickly whether the signal is worth developing in Algo mode.

0compilations to test a new variant
2automatic comparisons on every run: previous version and baseline
25measures per horizon and direction, against chance
4phases to train the eye blind
Preview
Declaration of an EMA 50 study with SignalSpec and ForwardEvaluationSpecDeclaration of the study chart: EMA and long and short markers
The study. Left, an EMA 50: its signals and what is measured afterwards (horizons of 3, 6 and 40 bars, in ATR units). Right, the chart declaration. Click a screenshot to enlarge it.
Research server chart: candles, EMA 50 and long and short signals on gold
The chart. The result of that study in the local server: long and short signals on gold, the EMA 50 and, at the top, the report's measures (median MFE, MAE and return at 40 bars, in ATR).
V7_BOXES study: stepping through active cases and the Inspect in notebook button
The cases. On another study in development, each signal can be stepped through one at a time and opened in the notebook. Top right, ITER and DSR show how many trials have already been counted.
Setting up a four-phase blind replay campaign
Training. Setting up a blind replay campaign: four phases, reserved windows and criteria fixed before launch.
01Write a study

Declaring an indicator, its signals and what you want to measure

A study is a Python function that receives prices, computes its columns and returns long and short signals. Three declarations come with it, so that every choice stays explicit:

  • SignalSpec states how a signal becomes an event: on the first candle of a run (rising_edge) or on every candle (every_true_bar), with an optional cooldown;
  • ForwardEvaluationSpec sets what is measured next: entry point, horizons and unit;
  • the view chooses the curves, markers and panes shown on the chart.
Code of the ema_cross_50 study
The study used for the screenshots on this page: an EMA 50, measured at 3, 6 and 40 bars in ATR units, with no TP or SL.
Code of the ema_cross_50 study view
The view of the same study: the EMA line and the style of the long and short markers.

Not reading the future

A candle may only use its past. A centred window or a shift(-1) brings in the future without it showing in the result. For higher-timeframe data, ctx.causal_feature(...) aligns each value to the last closed source candle. Columns named y_* stay reserved for future labels.

Iterating without losing track

A study can live in the notebook or in a Python file reloaded on each call. Only the calculation, the parameters and the evaluation identify a variant: changing a colour does not create a new trial. Session and candle-size filters apply after emission, without creating a new signal.

02Measure against chance

Comparing each signal with randomly drawn entries

A signal known at the close of a candle is evaluated from the next open. For each horizon, the mode measures the return in the signal's direction, the favourable excursion (MFE), the adverse excursion (MAE) and barrier touches. In ATR units, the scale is the one known at the signal.

The same calculation is applied to every eligible bar in the period, with the same share of longs and shorts. The gap between the two, the lift, comes with a 95% confidence interval bootstrapped by day. Each row then receives a diagnosis: POSITIVE, NEGATIVE, INCONCLUSIVE or INSUFFICIENT.

Signal against random entries, report output
DirectionHorizon¹Valid signalsSignal meanRandom meanMean liftLift 95% CIDiagnosis
ALL33307-0.0117640.001692-0.013456[-0.053551 ; 0.032919]INCONCLUSIVE
LONG317960.0177600.019636-0.001876[-0.063809 ; 0.052811]INCONCLUSIVE
SHORT31511-0.046856-0.019636-0.027220[-0.091209 ; 0.042453]INCONCLUSIVE
ALL633070.0247370.0034380.021300[-0.045838 ; 0.084326]INCONCLUSIVE
LONG617960.1395850.0398890.099697[0.017798 ; 0.176226]POSITIVE
SHORT61511-0.111773-0.039889-0.071884[-0.166403 ; 0.016405]INCONCLUSIVE
ALL1233070.0546600.0068860.047773[-0.036672 ; 0.131722]INCONCLUSIVE
LONG1217960.1937160.0799050.113811[-0.008983 ; 0.229892]INCONCLUSIVE
SHORT121511-0.110625-0.079905-0.030720[-0.160339 ; 0.099989]INCONCLUSIVE
¹ Horizon in bars. Values are taken as they are from research.report().predictive, with no conversion to percentages. The full table, with its 25 columns, unfolds below.

Reading this example: only LONG at 6 bars is labelled POSITIVE (lift 0.099697; 95% CI [0.017798; 0.176225]). The other eight rows are INCONCLUSIVE. This describes the evaluated signal, without establishing strategy profitability after management and costs.

Full table · 25 columns
research.report().predictive
directionhorizonsignalsvalidcoveragesignal_daysmean_rmean_r_ci_lowmean_r_ci_highmedian_rpositive_ratepositive_rate_ci_lowpositive_rate_ci_highrandom_barsrandom_mean_rrandom_positive_ratelift_mean_rlift_mean_r_ci_lowlift_mean_r_ci_highlift_positive_pplift_positive_pp_ci_lowlift_positive_pp_ci_highp_lift_gt_zerostandardized_liftedge_status
ALL3330733071.01490-0.011764-0.0519660.034240-0.0328450.4856370.4704620.5012401399450.0016920.500209-0.013456-0.0535510.032919-1.457246-2.9873820.1254000.26875-0.009910INCONCLUSIVE
LONG3179617961.012030.017760-0.0420430.075393-0.0031130.4972160.4748030.5206191399450.0196360.509472-0.001876-0.0638090.052811-1.225554-3.5338681.0770780.47375-0.001415INCONCLUSIVE
SHORT3151115111.01088-0.046856-0.1135080.025539-0.0684820.4718730.4488150.495764139945-0.0196360.489199-0.027220-0.0912090.042453-1.732640-3.9721990.5413480.19875-0.019526INCONCLUSIVE
ALL6330733071.014900.024737-0.0422580.0877610.0379330.5089200.4928760.5250711399420.0034380.5006940.021300-0.0458380.0843260.822662-0.7948022.4277980.742500.011365INCONCLUSIVE
LONG6179617961.012030.1395850.0518360.2202250.1159180.5311800.5070710.5529431399420.0398890.5124840.0996970.0177980.1762261.869666-0.3644333.9468140.995000.055145POSITIVE
SHORT6151115111.01088-0.111773-0.206676-0.017164-0.0730330.4824620.4584920.507810139942-0.0398890.486680-0.071884-0.1664030.016405-0.421825-2.8047251.9616480.07000-0.037034INCONCLUSIVE
ALL12330733071.014900.054660-0.0292370.1403440.0569530.5110370.4951030.5275791399360.0068860.5011160.047773-0.0366720.1317220.992072-0.6149702.6545180.870000.018348INCONCLUSIVE
LONG12179617961.012030.1937160.0702800.3138090.1907920.5356350.5130330.5593391399360.0799050.5156070.113811-0.0089830.2298922.002761-0.1809884.0930670.966250.043656INCONCLUSIVE
SHORT12151115111.01088-0.110625-0.2526570.024479-0.0894580.4818000.4544800.508412139936-0.0799050.483893-0.030720-0.1603390.099989-0.209250-2.6332762.3597800.33500-0.011859INCONCLUSIVE

In this example, only LONG at 6 bars is diagnosed POSITIVE (lift 0.0997, 95% interval from 0.0178 to 0.1762). The other eight rows remain INCONCLUSIVE. The result concerns the signal alone: it says nothing yet about the profitability of a strategy after management and costs.

03Compare versions

Finding the previous version and the baseline after every change

Every run is recorded and automatically compared with the last distinct version and with the series baseline, that is the first recorded run until it is moved. The user can see straight away whether a change improves the signal, and whether it drifts from the starting point.

research.run(ema_signal, params={"ema_span": 30})   # A: the baseline
research.run(ema_signal, params={"ema_span": 40})   # B
research.run(ema_signal, params={"ema_span": 50})   # C
report = research.report()
report.comparisons["previous"]   # C against B
report.comparisons["baseline"]   # C against A
Three versions of the same indicator in one series.

The report shows which signals are kept, added or removed, and how the measures move. Changing the data, the period or the evaluation opens a new series, so different experiments are never compared.

History is stored in a SQLite database with a readable Markdown journal: reports remain available after a restart. Renaming a notebook keeps its history; copying it creates a new identity that records its lineage.

04Judge timing

Recognising an earlier or later entry on the same move

When a change brings an entry forward by three candles, a plain comparison sees one signal removed and another added. The timing judge instead attaches each signal to the price move it belongs to, then compares the two versions move by move. Each entry falls into one of four categories:

  1. 01Early

    The entry precedes a move in the same direction, within the allowed window.

  2. 02Within the move

    The entry belongs to the move, and its position in it is measured.

  3. 03Late

    Too much of the move is already used up, or a large pullback has already happened.

  4. 04Outside a move

    No compatible move is found.

research.set_movement_judge(MovementJudge())
research.run(study, params=params)
m = research.report().comparisons["previous"].movements
m.summary, m.transitions, m.worst(15)
The report highlights timing changes and the moves worth inspecting.
05Inspect and replay

A chart linked to the notebook, and a replay that hides the future

research.serve(run) opens a local server that displays the result already computed: looking at the chart does not rerun the study.

V7_BOXES study in the Research server: active cases, previous and next buttons, Inspect in notebook
A study in development (DEV split). Each active case can be stepped through with previous and next, then opened in the notebook with the chosen context: here 10 bars before and 10 bars after.

From the chart to the notebook

The chart lets you:

  • select zones and send them to the notebook, which updates research.query;
  • open the full context of a signal with “Inspect in notebook”;
  • receive in the page the filters applied from the notebook.

A manual zone selection is flagged as discretionary.

Replaying without seeing what comes next

Simple replay starts paused, hides the chosen candle and everything after it, then advances signal by signal or candle by candle. Arrow keys navigate and the space bar plays or pauses.

Changing a parameter from the Settings panel creates a new variant in DEV. That editor is locked in OOS.

06Train the eye

Measuring an intuition with blind campaigns

A discretionary trader may feel able to recognise good signals. Campaigns make it possible to check: a human or a model sorts signals (Take or Reject) without seeing what follows, then the selection is compared with random selections from the same candidates.

Four ways of deciding are kept apart: unassisted discretion, discretion aided by values known at the signal, quantitative filter and retrospective annotation. An annotation made while seeing the future is never mixed with blind results.

Replay / learning panel: decision modes, Take and Reject buckets, default validation and phase table
The campaign panel: decision mode, Take and Reject buckets with their keys, default validation rule (SL of one ATR, favourable threshold of two ATR, 24-bar horizon) and the size of each phase.
  1. 01Initial measurement

    Blind decisions to set the starting point.

  2. 02Exploratory learning

    The outcome is shown after each decision, in chronological order.

  3. 03Stabilised training

    At least two blocks, to separate training from validation.

  4. 04Final evaluation

    Blind, on observations never seen.

Sample check: reservation refused, 340 signals available out of 400 requested
Before launch, the panel checks sample sizes. Here the learning phase asks for 400 signals while only 340 remain unseen: the reservation is refused rather than silently reduced.
Partial report of the four phases of a campaign
Partial report of a campaign: phases 1 and 2 are complete, phases 3 and 4 remain reserved and their results protected.

Every window shown is logged by date, asset and timeframe. Free replay counts too: renaming signals does not make a window “never seen”. The machine-learning export excludes phases 1 and 2, and the model is frozen before the final phase.

07Freeze and test elsewhere

Freezing the configuration before opening the reserved sample

The split between development (DEV) and out of sample (OOS) is declared when Research is created. The freeze records the configuration and the conclusions before any access to the reserved part, and the check runs before the OOS calculation. Earlier bars can warm up the indicators, but only authorised observations are published.

research.use_insample()
run_dev = research.run(study, params=params)
research.freeze_configuration(conclusions=conclusions)
research.use_oos()
run_oos = research.run(study, params=params)
cross = research.run_cross_asset(
    study, {"XAGUSD": xag_df}, params=params,
    public_settings=PUBLIC_SETTINGS,
)
The workflow once the study exists: development, freeze, out of sample, then another asset with the same parameters.

Trials are counted in the same validation registry as Algo mode: a new decision counts, an identical run is recognised, and looking or zooming remains a plain observation. Testing on another asset does not create a new trial, but each launch is recorded so repetitions stay visible.

08Move into Algo mode

From a measured signal to an executed strategy

Research · the signal

Compute the indicator, measure what follows its signals, compare versions.

Algo · the strategy

Choose entries, manage positions, apply costs and sizing.

Shared: the chart, the local server, the freeze manifest and the trial registry.

Moving into Algo mode means turning entries into setups, then adding position management, costs and sizing. Research's conclusions then have to be checked again: a signal that anticipates moves well does not guarantee a profitable strategy.

Algo mode backtest shown in the Research server: H1 regimes, entries and exits
The same server here shows an Algo mode backtest: EMA 50/100 crossover with an H1 regime, 734 trades. Coloured zones are regimes, triangles are entries and crosses are exits.

In the other direction, @engine.research_view declares a chart before the run: the engine's candles, entries and exits are added automatically, and engine.bind_research_view(study) can reuse a study's view. The result opens read-only: nothing is recomputed, no trial is counted and the freeze is untouched.