Ingénierie

Trois bugs de broker TradingView qu'on a trouvés en poussant le corpus de parité à 252

Comment l'extension du corpus de parité PineForge de 167 à 252 stratégies a mis au jour trois comportements de broker TradingView non modélisés — liquidation forcée, conversion FX et armement du trailing stop.
9 min de lecture#parity#corpus#engine#tradingview#broker-emulation

Une stratégie communautaire de notre corpus de parité nommée IES restait en parité moderate vis-à-vis de TradingView depuis des semaines. Les décomptes coïncidaient. Les prix d'entrée, au centime près. Les prix de sortie, idem. Mais le PnL cumulé dérivait de 29 281 $ sur 2 632 opérations déjà appariées.

Quand nous avons publié les chiffres du corpus en mai, celui-ci comptait 167 stratégies de référence, dont 165 en parité stricte trade-for-trade avec TradingView. Il a grossi depuis. Le corpus compte désormais 252 stratégies — 251 en parité « excellente » (99,6 %), une anomalie documentée, zéro échec.

Le chiffre qu'on met en avant, c'est la partie ennuyeuse. La partie intéressante, c'est ce qui a émergé en chemin : trois comportements du broker TradingView qu'on ne modélisait pas, chacun découvert parce qu'une stratégie réelle ou synthétique a exercé un chemin de code que les 167 précédentes ne touchaient pas. Voici trois d'entre eux, en détail.

Où en est le corpus

corpus/validation_report.md (2026-06-29)
  Total probes:    252
  Excellent:       251  (99.6%)
  Anomaly:           1
  Strong/Moderate/Weak/Minimal/Fail: 0
 
  TV trades:      389,590
  Engine trades:  389,688
  Matched:        389,468

Le moteur émet 98 trades de plus que TradingView sur l'ensemble du corpus — un écart de 0,025 % sur 389 590 trades de référence. C'est l'agrégat ; par stratégie, la quasi-totalité de cet écart vient de la seule anomalie ci-dessous, pas d'un bruit étalé sur les 251 autres.

Les six derniers probes forment une catégorie drawing, ajoutée quand on a livré un runtime capable de lire les objets de dessin Pine (line.get_price, géométrie des labels/box) comme des données utilisables en backtest, et pas seulement comme des annotations de graphique. Les stratégies qui utilisent une ligne tracée comme état de calcul — une trendline ancrée manuellement et utilisée comme niveau de support dynamique, par exemple — exigent que le moteur continue de servir la géométrie d'objets déjà supprimés par le script, exactement comme le fait TradingView. Les six probes sont passés sans accroc.

L'unique anomalie, nommée sans détour

On ne filtre pas la seule stratégie qui ne matche pas. anomaly-equity-mirror-strategy-equity-01 se situe exactement à la frontière 1× equity pour l'admission en marge. L'émulateur de broker de TradingView est non monotone juste à cette frontière — il admet certaines entrées en sur-equity et rejette certaines entrées en sous-equity, de façon incohérente, bar après bar. Notre moteur, lui, est déterministe : même equity, même décision d'admission, à chaque fois. C'est correct au regard de la sémantique de marge documentée de Pine. C'est aussi, de façon démontrable, différent de ce que fait réellement le broker TradingView à cette frontière précise. Il faudrait introduire notre propre non-déterminisme pour matcher ce comportement, donc on ne le fait pas. L'écart est documenté, pas caché, et c'est le seul sur 252.

Bug n°1 : la position que TradingView aurait liquidée d'office

Le broker de TradingView n'attend pas que la logique de votre stratégie décide de sortir d'une position qui a dépassé son exigence de marge — il liquide. Si l'equity d'une position à effet de levier ou short chute suffisamment pour que la marge requise dépasse l'equity du compte, TradingView liquide la position d'office à la barre suivante, peu importe les conditions de strategy.exit() remplies ou non.

Le moteur ne modélisait pas ça. Il conservait la position et laissait la logique propre de la stratégie la clôturer plus tard — ce qui, sur une stratégie sans stop serré, pouvait représenter des dizaines de barres avec une position que TradingView avait déjà liquidée. Sur les stratégies du corpus à fort effet de levier, ça produisait des courbes d'equity radicalement différentes en période de stress, même quand la logique d'entrée et de sortie matchait parfaitement partout ailleurs.

Le correctif ajoute une liquidation forcée dès que l'equity du compte franchit l'exigence de marge, en calculant le prix de liquidation de la même façon que le broker de TradingView, puis en arrondissant la quantité liquidée au lot step du symbole — TradingView ne liquide pas de lots fractionnaires plus petits que ce que permet l'exchange, et nous non plus désormais.

Ça concerne surtout ceux qui backtestent des stratégies à effet de levier ou short sur des perpetuals. Si votre courbe d'equity semblait trop lisse pendant un drawdown, c'était probablement ça.

Bug n°2 : le contrôle d'affordabilité qui ignorait le FX

Une stratégie Pine peut déclarer une devise de compte différente de la devise de cotation du symbole — currency.INR qui trade une paire USDT, par exemple. Avant de vérifier si vous pouvez vous permettre un ordre, le broker de TradingView convertit la valeur notionnelle de l'ordre dans la devise de votre compte au taux de change en vigueur, puis compare ce montant à l'equity dans cette même devise.

Le filtre d'affordabilité du moteur sautait cette conversion. Il comparait directement le notionnel en USDT à une equity libellée en INR comme si le taux de change était de 1:1 — ce qui faisait qu'une stratégie déclarant currency.INR paraissait environ 83× plus abordable qu'elle ne l'était réellement (puisque 1 USDT vaut à peu près ce nombre de roupies). Sur les scripts concernés, ça doublait à peu près le nombre de trades que le moteur admettait alors que le broker de TradingView les aurait rejetés comme inabordables.

Le correctif applique le taux de change de la devise de compte à la marge requise avant que le contrôle d'affordabilité ne s'exécute, exactement au même point du pipeline d'ordre que TradingView. Si vous backtestez avec une devise de compte autre que l'USD contre un symbole coté en USD ou en USDT, c'est ce bug qui gonflait discrètement votre nombre de trades.

Bug n°3 : le trailing stop qui ne s'armait jamais

C'est celui qu'on aurait le plus aimé qu'un utilisateur repère avant nous, parce qu'il échoue en silence. strategy.exit() propose deux façons de spécifier un trailing stop : trail_points, une distance relative au prix d'entrée, et trail_price, un prix d'activation absolu.

// armed correctly today; trail_price-only calls used to never arm at all
strategy.exit("TS", from_entry = "Long", trail_price = close * 1.05, trail_offset = 10)

La condition d'armement du moteur ne vérifiait que trail_points. Un exit trailing spécifié uniquement via trail_price compilait sans erreur, tournait sans erreur, et ne s'armait jamais — la position restait là, sans aucun trailing stop actif, en silence, pendant tout le backtest. Aucune exception, aucune ligne de log, aucun trade. La stratégie avait l'air protégée. Elle ne l'était pas.

On a trouvé ce bug parce qu'un probe du corpus isole spécifiquement l'activation via trail_price seul — exactement le genre de stratégie étroite et synthétique pour laquelle existe la catégorie validation, comme expliqué dans le billet sur la composition du corpus : pas rentable, pas réaliste, construite pour exercer une seule fonctionnalité Pine de façon isolée afin qu'une régression comme celle-ci ne puisse pas se cacher derrière « les chiffres globaux avaient l'air bons ».

Le correctif arme le trailing stop dès que trail_points ou trail_price est présent. Si vous avez déjà écrit une stratégie Pine utilisant trail_price et que la courbe d'equity ressemblait suspicieusement à une absence totale de trailing stop — c'était bien le cas, sur toute version du moteur antérieure à celle-ci.

Pourquoi c'est précisément l'intérêt d'un corpus

Aucun de ces trois bugs n'a été trouvé par revue de code. Ils ont été trouvés parce qu'une stratégie de la bonne forme est passée dans le moteur et a produit un chiffre qui ne matchait pas celui de TradingView. C'est toute la thèse derrière le fait de maintenir 167 stratégies, puis 252, et ça continue comme un corpus versionné et mis à l'écart, plutôt qu'une poignée de smoke tests : des bugs qui ne changent ni le nombre de trades ni le prix d'exécution peuvent quand même changer le chiffre qui compte le plus — ce que la stratégie a gagné ou perdu — et la seule façon de les attraper, c'est de continuer à faire tourner plus de formes de stratégies dans le même diff contre la même vérité de référence.

On continuera d'ajouter des probes à mesure qu'on trouve des trous, et de publier les chiffres — les bons comme l'unique anomalie — tels qu'ils sont.

Pour aller plus loin