Arnaud Barbier
Mode allocation
Allocation multi-actifs · 26 août 2026

Mode allocation, backtester un portefeuille multi-actifs et réfuter son résultat

Le mode allocation permet de backtester un portefeuille multi-actifs avec une comptabilité complète de la trésorerie, de la dette, des frais et des devises, puis de soumettre le résultat à une couche de validation qui cherche à montrer que la stratégie n'apporte rien. Toute cette politique comptable se déclare en quarante lignes ; la page montre le code réel que l'utilisateur écrit et la sortie réelle que le moteur renvoie. Le mode investissement reprend cette comptabilité ; le mode algorithmique n'y accède que par la couche de sizing.

Le run présenté ici est un run qui échoue : +0,42 % par an, un drawdown de −43,43 % et un verdict sous le taux sans risque. C'est volontaire, car l'objet de la page est la mesure, pas la stratégie.

Identité comptable
4,44 e−16
résidu maximal, 2 300 barres
Durée du run, mode allocation
3,2 s
2 298 barres, 30 retours en Python
Tranches d'emprunt
11
tracées à l'unité
Réfuter le résultat
3 lignes
trois appels, trois verdicts
01Déclarer un portefeuille

Tout ce qui suit est écrit une fois, et le moteur s'occupe du reste

Réserve fixe et dynamique, emprunt indexé sur un taux de marché réel, frais provisionnés sur une horloge et payés sur une autre, commission de surperformance avec high-water mark, et des réserves qui ne sont pas dans la devise du book. Une seule déclaration.

Sur les devises, le décompte mérite d'être explicite, parce que la spec n'en montre que trois. Le CurrencySpec en déclare trois : USD pour le book et le cash gagné, CHF pour la réserve fixe, EUR pour la réserve dynamique et le vault. La quatrième entre par le book lui-même, dont une des cinq jambes est GBP/USD. Les actifs portent "USD" dans les bindings parce que le book est exprimé en devise de base ; les jambes non-dollar passent par des taux croisés. Le choix de tenir les réserves hors de la devise du portefeuille est délibéré : leur valeur bouge alors même quand rien n'est fait, ce qui est le cas qu'un moteur mono-devise ne peut pas représenter.

Bloc AllocationSpec complet
Deux horloges pour un même frais. charge_frequency="year" est le rythme auquel la commission se provisionne, payout_frequency="quarter" celui auquel elle quitte le compte. Les confondre est la façon habituelle dont un backtest sous-estime son propre coût.
Appel du moteur avec la fenêtre réservée
La fenêtre de test est scellée au moment de la déclaration, pas après coup. ValidationSpec.simple_oos(ratio_oos=0.30) met les 30 % de fin hors d'atteinte pour tout ce qui suit. Le taux d'emprunt est une vraie série SOFR, et les jambes non-dollar passent par des taux croisés.
Courbe du taux overnight utilisé par le kernel
Le taux que paie l'emprunt, tel que le kernel le lit. SOFR là où il existe, DFF avant, raccordés en une seule série. Le coût de financement suit donc le cycle réel : quasi nul jusqu'en 2016, remontée jusqu'en 2019, effondrement en 2020, puis 5,3 % en 2024. Un modèle à taux fixe aurait effacé cette histoire. Cette courbe court jusqu'en 2026, les résultats s'arrêtent en 2022 : la donnée d'entrée existe sur toute la période, la fenêtre de mesure non. C'est le sceau, rendu visible.
02Décider, et être interrompu

Un calendrier, et ce qui a le droit de l'interrompre

La stratégie décide toutes les cinquante barres. Un scanner compilé surveille le portefeuille chaque semaine et peut déclencher deux actions de nature différente selon l'événement rencontré.

Ce run n'est pas un balayage compilé d'un seul tenant, ce qui explique les 3,2 secondes. Le kernel s'arrête à chaque décision, rend la main à Python, puis reprend. C'est ce que veut dire mode="resume" : l'état du portefeuille traverse la frontière au lieu d'être recalculé depuis le début. Positions, trésorerie par palier, frais provisionnés, tranches d'emprunt et high-water mark reviennent tels quels de l'autre côté. Les 3,2 secondes sont donc le prix de 30 allers-retours avec un registre qui survit, pas celui du calcul numérique. La version rapide consisterait à garder la décision dans le kernel ; elle coûterait d'écrire chaque stratégie en numba.

StatefulSpec avec son ResumeWatchSpec
La décision reste du Python lisible ; seul le scanner est compilé. Le partage suit la fréquence d'appel, pas une intuition sur la taille des données : le scanner vit dans la boucle de barres, la fonction de décision tourne 30 fois sur ce run. Ce qui est parcouru en boucle est compilé, ce qui est appelé quelques dizaines de fois reste lisible. L'écart entre les deux ne fait que croître avec la longueur de l'historique.
Le scanner njit, huit lignes
Le scanner tourne dans numba : il ne peut renvoyer que des entiers. Huit lignes.
Les trois dictionnaires de binding
Trois tables traduisent ces entiers en Python. L'une dit quelle action exécuter, l'autre pourquoi elle s'est déclenchée. Les deux sont séparées parce qu'elles sont indépendantes : deux raisons peuvent appeler la même action, et chacune garde son propre compteur de silence.

Le moteur tient le journal correspondant, une ligne par événement : barre de déclenchement, barre de décision, fonction appelée, raison, statut fired ou cooldown. C'est là que se vérifie le décalage d'une barre entre l'événement et l'action.

03Ce qui empêche de tricher

Les garanties qui rendent l'interruption honnête

01

Le moteur n'agit jamais sur la barre qui a déclenché. decision_delay_bars=1 : sa clôture n'est pas connaissable au moment du signal. Le journal d'événements le montre ligne par ligne.

02

Le rembobinage est explicite. allow_rewind est un choix déclaré, jamais un comportement implicite.

03

Le silence est compté par raison. Un drawdown qui se déclenche ne consomme pas le cooldown d'une durée sous l'eau.

04

Le budget d'actions est appliqué dans le scanner compilé. max_fires_per_period=1 : un budget épuisé ne coûte même pas de temps de calcul, et le levier ne peut pas s'emballer.

05

Le nombre d'événements par découpe est plafonné. max_watch_events_per_cut=10.

04Le registre

Chaque euro emprunté est attribuable à sa barre

La politique principale ne tire rien automatiquement. Les onze tranches de ce run ont toutes été ouvertes par un événement du watcher, et chacune porte son ticket.

Chronologie des tranches d'emprunt
Le ticket de caisse, dans le temps. Chaque tranche porte sa barre de tirage, son principal, son encours, sa barre de remboursement et ses intérêts courus. Neuf se sont refermées seules à leur maturité, deux restent ouvertes en fin de période, et le registre en compte onze pour 0,130121 d'intérêts payés. Toutes portent policy_id 2 : le moteur déduplique les politiques par contenu, donc une politique réutilisée reste un seul identifiant.
Bilan comptable, trois panneaux
Le troisième panneau est celui qui compte. Il trace nav − (invested + cash + pending) et gross − (equity + borrowed), qui doivent rester plats à zéro. Le maximum atteint est 4,44 e−16. Sur un portefeuille de 100 000 €, cela fait un écart de quatre milliardièmes de centime entre ce que le portefeuille possède et ce qu'il doit. C'est le plus petit écart qu'un ordinateur sache encore représenter à cette échelle : en dessous il n'y a plus de nombre, il y a zéro. Et c'est tenu sur 2 300 barres, avec emprunt, change, frais et watchers actifs. Ce panneau n'est jamais rééchantillonné pour l'affichage : un pic d'une seule barre est exactement ce qu'il existe pour attraper.
05Attribution des coûts

Du rendement avant coûts au rendement net

Les coûts d'exécution ne sont plus une déclaration sans effet dans le mode allocation. Spread, commission, taxe, impact résiduel et slippage sont rapprochés sur les jambes réellement exécutées ; le carry de contrat reste séparé du financement de l'emprunt. Sur ce run avec watcher, ils retirent ensemble 3 606,69, soit 3,6068 % du capital initial de 100 000.

NAV nette et trajectoires reconstituées avant coûts sur le run du mode allocation
Une attribution sur le chemin effectivement observé. Le run produit +0,40 %/an net. Réintégrer seulement les coûts d'exécution donne +0,80 %/an ; retirer le carry de contrat change à peine la courbe ; sans frais de gestion, elle atteint +2,23 %/an ; sans financement de l'emprunt, +1,79 %/an. Sans aucun coût, le même chemin ressort à +4,06 %/an. Les coûts représentent 38,5 % de la valeur terminale sur 9,12 ans.
Accumulation des frais de gestion, intérêts d'emprunt et coûts d'exécution
Le registre des débits explicites, cumulé puis ventilé par année. Les frais de gestion et le financement dominent ce run ; la ligne violette retrace les débits d'exécution du kernel. Le slippage mesuré n'est pas ajouté à ce registre de cash : il mord déjà dans le prix de fill et apparaît dans le tableau et dans la cascade.
Coûts par actif en devise du compte, capital initial 100 000
ActifSpreadCommissionTaxeImpact résiduelSlippageExécutionCarry / rollTotal% capitalCarry pb/anSlip fill pb A/RDates carry
BRENTCMDUSD134,52134,520,0053,61807,001 129,650,001 129,651,1297 %0,026,9810
USA500IDXUSD54,24325,440,0097,21341,33818,220,00818,220,8182 %0,012,2810
DEUIDXEUR32,3697,080,0020,83601,39751,660,00751,660,7517 %0,016,4240
XAUUSD71,9410,790,0013,75517,99614,4729,99644,450,6445 %1,410,766101
GBPUSD21,518,070,000,96232,18262,710,00262,710,2627 %0,06,5570
Portefeuille314,57575,900,00186,362 499,893 576,7129,993 606,693,6068 %s.o.s.o.101

Le slippage est mesuré par l'écart entre prix cible et prix exécuté. Son effet est déjà dans la NAV : le tableau le rend visible sans le débiter une seconde fois. La page consacrée aux coûts publie la spec, le même rapprochement dans deux unités et la cascade qui réintègre chaque catégorie de coûts séparément sur une trajectoire fixe.

06Le verdict de ce run

Le verdict du moteur sur ce run

Appel du moteur et première partie du résumé du run
Le run et son porteur sont identifiés avant le verdict. 2 298 observations, 9 ans et 30 décisions, lus sur portfolio_equity(nav−borrowed). Le Sharpe corrigé de l'autocorrélation vaut 0,09, avec un intervalle de Lo à 95 % de [−0,50 ; 0,68].
Deuxième partie du résumé du run et verdict BELOW_RISK_FREE
Une garde statistique favorable ne suffit pas à sauver un résultat économiquement faible. Le PSR ressort à 100 % et le MinTRL demande 288 observations contre 2 298 disponibles. Pourtant, avec un CAGR de +0,42 % et un drawdown de −43,43 %, l'équivalent certain devient négatif dès γ = 3. Le seuil γ* est inférieur à zéro : l'actif sans risque à 4 % gagne dès γ = 0. Verdict BELOW_RISK_FREE.
Registre d'emprunt et frais payés sur le run
Le verdict est relié aux écritures qui l'ont produit. Le levier brut culmine à 1,657× ; onze tirages d'emprunt coûtent 0,125008, neuf sont refermés ou expirent, et les frais de gestion totalisent 0,163899. Aucun appel de marge n'est enregistré.
NAV, equity nette de l'emprunt et drawdown du run moteur
La trajectoire qui se trouve derrière le tableau. La NAV brute termine à 1,4534, mais l'equity nette de l'emprunt à 1,0383. Les marqueurs montrent les trente rebalancements, les actions intra-période et les ouvertures ou fermetures d'emprunt ; le panneau inférieur matérialise le drawdown maximal de 43,43 %.
07La couche de validation

Les baselines ne sont pas calculées à côté, elles sont rejouées dans le moteur

C'est la partie qui justifie tout ce qui précède. Une baseline construite dans un tableur ne paie ni frais ni devises : la comparer à une stratégie qui les paie fabrique un écart qui n'existe pas. Ici la baseline traverse le même kernel, le même registre de trésorerie et la même série de mesure. Seule la règle de pondération change, et l'écart mesuré est donc bien celui de la règle.

Les écarts sont en pt, points de pourcentage de rendement annualisé. C'est une soustraction entre deux taux : 1,94 % − 3,80 % = −1,86 pt. Ni un pourcentage relatif, ni une fraction du capital engagé. C'est un écart de vitesse, que la durée transforme en argent : sur neuf ans, ces −1,86 pt composent en 21 points de capital terminal.

Deux runs distincts, et il ne faut pas les confondre. Les sections 01 à 06 viennent du run avec emprunt et watcher : 30 décisions, drawdown maximal de 43,43 %. Les trois appels ci-dessous ont été passés sur un autre run, la même stratégie sans emprunt, sans watcher et sans spec de devises : 46 décisions, drawdown maximal de 24,76 %. Le registre de trésorerie et la série de mesure y sont les mêmes ; ce qui change, c'est qu'il n'y a plus de levier, d'interruption ni de conversion entre la règle de pondération et le résultat. C'est ce second run que documente la note de validation.

Courbe d'equity du run sans emprunt ni watcher
Le run soumis à réfutation. Réserve fixe et dynamique, frais, devises, mais aucune ligne d'emprunt et aucun marqueur d'action intra-période. Rebalancement toutes les 50 barres, 46 décisions, +18,95 % cumulés, drawdown maximal de 24,76 %.
Baseline naïve rejouée dans le moteur
Une ligne, et la stratégie est comparée à sa propre version sans intelligence. La baseline équipondérée est rejouée dans le moteur, pas approximée en dehors : elle paie les mêmes frais, porte les mêmes devises, suit le même porteur. Deux niveaux, et c'est la distinction qui fait le travail. absolute retire les réserves, matched garde toute la machinerie et ne change que la règle de pondération. Verdict ADDS_NOTHING : l'équivalent certain est inférieur de 0,96 %/an à γ = 3. Le tableau THRESHOLDS dit ce qu'il aurait fallu pour que la conclusion bascule, une réserve capturant +50,3 % en une fois, et répond que c'est atteignable depuis un creux.
Null de sizing, attribution de l'écart
L'écart est décomposé, puis déclaré indétectable. Un run intermédiaire à poids plats à l'intérieur de la sélection de la stratégie sépare ce que l'optimiseur doit à son choix d'actifs de ce qu'il doit à ses poids. Sizing : −0,84 pt, t = −0,63. Sélection : −1,02 pt, t = −1,37. Le verdict ne dit pas « l'optimiseur ne sert à rien », il dit qu'à 46 décisions ce n'est pas établissable, et chiffre ce qu'il faudrait, environ 99 décisions pour trancher à cette taille d'effet. Un backtest qui rapporte un écart sans rapporter sa résolution ne dit rien.
Null de levier, frontière atteignable
Le test qui ne demande aucun modèle. Détenir 66,4 % du benchmark et le reste en liquide reproduit exactement la volatilité de la stratégie, et rapporte plus, à n'importe quel taux sans risque supposé entre 0 et 2 %. L'écart va de −0,74 à −1,42 pt. Verdict DOMINATED : le même risque était disponible avec plus de rendement, par une division.
08Périmètre

Ce qui est publié, et ce qui ne l'est pas

Publié. Le dépôt public est un instantané du moteur algorithmique tel qu'il était en avril 2026 : de quoi définir un signal, le faire tourner sous un modèle d'exécution réaliste, et inspecter les trades produits.

Non publié. Le moteur d'allocation et d'investissement qui a produit cette page est privé : le registre, la machinerie de trésorerie et d'emprunt, la provision des frais, la couche de devises, et la couche de validation. Il est couvert par 401 tests.

Les coûts d'exécution du mode allocation sont désormais débités par le kernel et rapprochés par actif. Leur effet mesuré sur ce run est publié en section 05 ; les hypothèses restent des paramètres de démonstration, pas les conditions d'un courtier.

Arnaud Barbier · moteur, couche de validation, et cette page
Code source →
Allocation mode
Multi-asset allocation · 26 August 2026

Allocation mode, backtesting a multi-asset portfolio and refuting its result

Allocation mode backtests a multi-asset portfolio with full cash, debt, fee and currency accounting, then hands the result to a validation layer that tries to show the strategy adds nothing. The whole accounting policy is declared in forty lines; the page shows the real code that is written and the real output the engine returns. Investment mode takes that accounting over; algorithmic mode only reaches it through the sizing layer.

The run shown here is a failing one: +0.42 % a year, a −43.43 % drawdown and a verdict below the risk-free rate. That is deliberate, because the point of the page is the measurement, not the strategy.

Accounting identity
4.44 e−16
worst residual, 2,300 bars
Run time, allocation mode
3.2 s
2,298 bars, 30 returns to Python
Borrow tranches
11
traced one by one
Refuting the result
3 lines
three calls, three verdicts
01Declaring a portfolio

All of this is written once, and the engine handles the rest

A fixed and a dynamic cash reserve, borrowing indexed on a real market rate, fees that accrue on one clock and are paid on another, a performance fee with a high-water mark, and reserves that are not held in the book's currency. One declaration.

On currencies the count is worth stating, because the spec only shows three of them. The CurrencySpec declares three: USD for the book and earned cash, CHF for the fixed reserve, EUR for the dynamic reserve and the vault. The fourth arrives through the book itself, one of whose five legs is GBP/USD. The assets carry "USD" in the bindings because the book is expressed in base currency; the non-dollar legs go through cross rates. Holding the reserves outside the portfolio's currency is deliberate: their value then moves even when nothing is done, which is the case a single-currency engine cannot represent.

Full AllocationSpec block
Two clocks for one fee. charge_frequency="year" is the rhythm at which the fee accrues, payout_frequency="quarter" the one at which it leaves the account. Conflating them is the usual way a backtest under-reports its own cost.
Engine call with the reserved window
The test window is sealed at declaration time, not afterwards. ValidationSpec.simple_oos(ratio_oos=0.30) puts the last 30 % out of reach for everything that follows. The borrow rate is a real SOFR series, and the non-dollar legs go through cross rates.
Overnight rate curve used by the kernel
The rate the borrowing actually pays, as the kernel reads it. SOFR where it exists, DFF before that, joined into a single series. Financing cost therefore follows the real cycle: near zero until 2016, climbing through 2019, collapsing in 2020, then 5.3 % in 2024. A fixed-rate model would have erased that history. This curve runs to 2026, the results stop in 2022: the input exists over the whole period, the measurement window does not. That is the seal, made visible.
02Deciding, and being interrupted

A schedule, and what is allowed to interrupt it

The strategy decides every fifty bars. A compiled scanner watches the portfolio weekly and can trigger two actions of different natures depending on the event it meets.

This run is not one compiled sweep, which is why it takes 3.2 seconds. The kernel stops at every decision, hands control back to Python, then picks up again. That is what mode="resume" means: portfolio state crosses the boundary instead of being recomputed from the start. Positions, cash by tier, accrued fees, borrow tranches and the high-water mark all come back untouched on the other side. So the 3.2 seconds are the price of 30 round trips with a surviving ledger, not the price of the arithmetic. The fast version would keep the decision inside the kernel; it would cost writing every strategy in numba.

StatefulSpec with its ResumeWatchSpec
The decision stays readable Python; only the scanner is compiled. The split follows call frequency, not a guess about data size: the scanner is evaluated on every loop, the decision function runs 30 times in this run. What is walked in a loop is compiled, what is called a few dozen times stays readable. The gap between the two only grows with the length of the history.
The njit scanner, eight lines
The scanner runs inside numba: it can only return integers. Eight lines.
The three binding tables
Three tables translate those integers back into Python. One says which action to run, the other says why it fired. They are separate because they are independent: two reasons can call the same action, and each keeps its own silence counter.

The engine keeps the matching log, one row per event: fire bar, decision bar, function called, reason, and a fired or cooldown status. That is where the one-bar delay between event and action is checked.

03What stops this from cheating

The guarantees that keep the interruption honest

01

It never acts on the bar that fired. decision_delay_bars=1: that bar's close is not knowable at signal time. The event log shows it row by row.

02

Rewind is explicit. allow_rewind is a declared choice, never an implicit behaviour.

03

Silence is counted per reason. A drawdown that fires does not consume the cooldown of an underwater-duration event.

04

The action budget is enforced inside the compiled scanner. max_fires_per_period=1: an exhausted budget does not even cost compute, and leverage cannot run away.

05

Events per cut are capped. max_watch_events_per_cut=10.

04The ledger

Every borrowed euro is attributable to its bar

The main policy draws nothing automatically. All eleven tranches in this run were opened by a watcher event, and each carries its own ticket.

Borrow tranche timeline
The receipt, over time. Every tranche carries its draw bar, principal, outstanding, repayment bar and accrued interest. Nine closed themselves at maturity, two are still open at the end of the period, and the ledger counts eleven for 0.130121 of interest paid. All of them carry policy_id 2: the engine dedupes policies by content, so a reused policy stays a single id.
Balance sheet audit, three panels
The third panel is the one that matters. It plots nav − (invested + cash + pending) and gross − (equity + borrowed), both of which must sit flat on zero. The worst value reached is 4.44 e−16. On a 100,000 € portfolio that is a gap of four billionths of a cent between what the portfolio owns and what it owes. It is the smallest gap a computer can still represent at this scale: below it there is no number left, there is zero. And it holds over 2,300 bars, with borrowing, currency conversion, fees and watchers active. This panel is never resampled for display: a spike lasting one bar is exactly what it exists to catch.
05Cost attribution

From pre-cost to net performance

Execution costs are no longer declarations without effect in allocation mode. Spread, commission, tax, residual impact and slippage are reconciled against the legs that were actually executed; contract carry remains separate from borrow financing. On this run with a watcher, they remove 3,606.69 in total, or 3.6068 % of the initial capital of 100,000.

Net NAV and reconstructed pre-cost paths on the allocation-mode run
An attribution on the path that was actually observed. The run produces +0.40 %/year net. Adding back execution costs alone gives +0.80 %/year; removing contract carry barely changes the path; without management fees it reaches +2.23 %/year; without borrow financing, +1.79 %/year. Without any cost, the same path returns +4.06 %/year. Costs represent 38.5 % of terminal value over 9.12 years.
Accumulation of management fees, borrow interest and execution costs
The ledger of explicit debits, accumulated and then split by year. Management fees and financing dominate this run; the purple line records kernel execution debits. Measured slippage is not added to this cash ledger: it already bites through the fill price and appears in the table and the cascade.
Costs by asset in account currency, initial capital 100,000
AssetSpreadCommissionTaxResidual impactSlippageExecutionCarry / rollTotal% capitalCarry bp/yFill slip bp RTCarry dates
BRENTCMDUSD134.52134.520.0053.61807.001,129.650.001,129.651.1297 %0.026.9810
USA500IDXUSD54.24325.440.0097.21341.33818.220.00818.220.8182 %0.012.2810
DEUIDXEUR32.3697.080.0020.83601.39751.660.00751.660.7517 %0.016.4240
XAUUSD71.9410.790.0013.75517.99614.4729.99644.450.6445 %1.410.766101
GBPUSD21.518.070.000.96232.18262.710.00262.710.2627 %0.06.5570
Portfolio314.57575.900.00186.362,499.893,576.7129.993,606.693.6068 %n/an/a101

Slippage is measured as the gap between target and execution price. Its effect is already in NAV: the table makes it visible without charging it twice. The dedicated French note publishes the specification, the reconciliation in two units, and the fixed-path cascade.

06This run's verdict

What the engine reports on this run

Engine call and first part of the run summary
The run and its carrier are identified before the verdict. 2,298 observations, 9 years and 30 decisions, read from portfolio_equity(nav−borrowed). The autocorrelation-corrected Sharpe is 0.09, with a 95 % Lo interval of [−0.50; 0.68].
Second part of the run summary and BELOW_RISK_FREE verdict
A favourable statistical guard cannot rescue a weak economic result. PSR is 100 % and MinTRL requires 288 observations against 2,298 available. Yet a +0.42 % CAGR with a −43.43 % drawdown makes the certainty equivalent negative from γ = 3. The γ* threshold is below zero: the 4 % risk-free asset wins from γ = 0. Verdict BELOW_RISK_FREE.
Borrow ledger and fees paid on the run
The verdict is tied back to the entries that produced it. Gross leverage peaks at 1.657×; eleven borrow draws cost 0.125008, nine close or expire, and management fees total 0.163899. No margin call is recorded.
NAV, equity net of borrowing and drawdown on the engine run
The path behind the table. Gross NAV ends at 1.4534, while equity net of borrowing ends at 1.0383. Markers identify the thirty rebalances, intra-period actions, and borrow opens or closes; the lower panel shows the 43.43 % maximum drawdown.
07The validation layer

The baselines are not computed alongside, they are replayed through the engine

This is the part that justifies everything above it. A baseline built in a spreadsheet pays neither fees nor currencies: comparing it to a strategy that does manufactures an edge that is not there. Here the baseline goes through the same kernel, the same cash ledger and the same performance carrier. Only the weighting rule changes, so the gap measured is the gap of the rule.

Gaps are stated in pt, percentage points of annualised return. It is a subtraction between two rates: 1.94 % − 3.80 % = −1.86 pt. Neither a relative percentage nor a fraction of the capital committed. It is a difference in speed, which duration turns into money: over nine years those −1.86 pt compound into 21 points of terminal capital.

Two separate runs, and they must not be conflated. Sections 01 to 06 come from the run with borrowing and a watcher: 30 decisions, 43.43 % maximum drawdown. The three calls below were made against a different run, the same strategy with no borrowing, no watcher and no currency spec: 46 decisions, 24.76 % maximum drawdown. The cash ledger and the performance carrier are the same there; what changes is that no leverage, no interruption and no translation sit between the weighting rule and the result. That second run is the one the validation note documents.

Equity curve of the run without borrowing or watcher
The run put to refutation. Fixed and dynamic reserve, fees, currencies, but no borrow line and no intra-period action marker. Rebalanced every 50 bars, 46 decisions, +18.95 % cumulative, 24.76 % maximum drawdown.
Naive baseline replayed through the engine
One line, and the strategy is measured against its own version without the intelligence. The equal-weight baseline is replayed through the engine, not approximated outside it: it pays the same fees, carries the same currencies, is read on the same carrier. Two levels, and the distinction is what does the work. absolute strips the reserves, matched keeps every piece of machinery and swaps only the weighting rule. Verdict ADDS_NOTHING: the certainty equivalent is lower by 0.96 %/year at γ = 3. The THRESHOLDS table states what it would have taken for the conclusion to flip, a reserve capturing +50.3 % in one go, and answers that this is reachable off a trough.
Sizing null, attribution of the gap
The gap is decomposed, then declared undetectable. An intermediate flat-weight run inside the strategy's own selection separates what the optimiser owes to its choice of assets from what it owes to its weights. Sizing: −0.84 pt, t = −0.63. Selection: −1.02 pt, t = −1.37. The verdict does not say the optimiser is useless, it says that 46 decisions cannot establish it either way, and prices what it would take: about 99 decisions to settle at this effect size. A backtest that reports a gap without reporting its resolution says nothing.
Leverage null, reachable frontier
The test that requires no model. Holding 66.4 % of the benchmark and the rest in cash reproduces the strategy's volatility exactly, and returns more, at every assumed risk-free rate between 0 and 2 %. The gap runs from −0.74 to −1.42 pt. Verdict DOMINATED: the same risk was available with more return, by a division.
08Scope

What is published, and what is not

Published. The public repository is a snapshot of the algorithmic engine as it stood in April 2026: enough to define a signal, run it under a realistic execution model, and inspect the resulting trades.

Not published. The allocation and investment engine behind this page is private: the ledger, the cash and borrowing machinery, fee accrual, the currency layer, and the validation layer. It is covered by 401 tests.

Allocation-mode execution costs are now debited by the kernel and reconciled by asset. Their measured effect on this run is published in section 05; the inputs remain demonstration parameters, not the terms of a specific broker.