Ingeniería

252 estrategias después: tres bugs de broker que solo un corpus podía encontrar

El corpus de paridad de PineForge pasó de 167 a 252 estrategias. En el camino encontramos tres comportamientos del broker de TradingView que no estábamos modelando — liquidación forzada por margen, conversión de FX en el chequeo de affordability y un trailing stop que nunca se armaba.
9 min de lectura#parity#corpus#engine#tradingview#broker-emulation

Una estrategia comunitaria de Pine de nuestro corpus de paridad llamada IES llevaba semanas sentada en paridad moderate frente a TradingView. Cuando publicamos por última vez los números del corpus en mayo, el corpus de paridad tenía 167 estrategias de referencia, 165 de ellas en paridad estricta trade-por-trade contra TradingView. Desde entonces ha seguido creciendo. El corpus ahora tiene 252 estrategias — 251 en paridad "excellent" (99,6%), una anomalía documentada, cero fallos.

El número titular es la parte aburrida. Lo interesante es lo que apareció mientras llegábamos hasta ahí: tres comportamientos del broker de TradingView que no estábamos modelando, cada uno encontrado porque una estrategia real o sintética ejercitó una ruta de código que las 167 anteriores no tocaban. Este post repasa tres de ellos.

Dónde está el 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

El motor emite 98 trades más que TradingView en todo el corpus — un delta de 0,025% sobre 389.590 trades de referencia. Eso es el agregado; por estrategia, casi todo ese delta cae en la única anomalía de abajo, no en ruido repartido entre las otras 251.

Las seis pruebas más recientes son una categoría drawing, añadida cuando lanzamos un runtime para leer objetos de dibujo de Pine (line.get_price, geometría de label/box) como datos en tiempo de backtest, no solo como anotaciones del gráfico. Las estrategias que usan una línea dibujada como estado computacional — una trendline anclada manualmente que actúa como nivel de soporte dinámico, por ejemplo — necesitan que el motor siga sirviendo geometría para objetos que el script ya borró, igual que hace TradingView. Las seis pasaron limpio.

La única anomalía, nombrada sin rodeos

No filtramos la única estrategia que no encaja. anomaly-equity-mirror-strategy-equity-01 está justo en el límite de equity 1× para admisión de margen. El emulador de broker de TradingView es no-monótono exactamente en ese límite — admite algunas entradas con equity insuficiente y rechaza algunas con equity suficiente, de forma inconsistente, barra a barra. Nuestro motor es determinista: misma equity, misma decisión de admisión, siempre. Eso es correcto según la semántica de margen documentada de Pine. También es, demostrablemente, lo que TradingView no hace en ese límite concreto. Tendríamos que introducir nuestra propia no-determinismo para igualarlo, así que no lo hacemos. El gap está documentado, no escondido, y es el único de las 252.

Bug uno: la posición que TV habría liquidado a la fuerza

El broker de TradingView no espera a que la lógica de tu estrategia decida cuándo cerrar una posición que reventó su requerimiento de margen — liquida directamente. Si la equity de una posición apalancada o en corto cae lo suficiente como para que el margen requerido supere la equity de la cuenta, TradingView fuerza el cierre de la posición en la siguiente barra, sin importar qué condiciones de strategy.exit() se cumplan o no.

El motor no modelaba esto. Mantenía la posición y dejaba que la lógica propia de la estrategia la cerrara eventualmente — lo cual, en una estrategia sin un stop ajustado, podía significar decenas de barras con una posición que TradingView ya había liquidado. En estrategias del corpus con apalancamiento significativo, eso producía curvas de equity radicalmente distintas bajo estrés, incluso cuando la lógica de entrada y salida coincidía perfectamente en todo lo demás.

El fix añade liquidación forzada cuando la equity de la cuenta rompe el requerimiento de margen, calculando el precio de liquidación de la misma forma que el broker de TradingView, y luego cuantizando la cantidad liquidada al lot step del símbolo — TradingView no liquida lotes fraccionarios más pequeños de lo que el exchange permite, y ahora nosotros tampoco.

Esto afecta sobre todo a quien backtestea estrategias apalancadas o en corto sobre perpetuos. Si tu curva de equity se veía demasiado suave durante un drawdown, esta era, plausiblemente, la razón.

Bug dos: el chequeo de affordability que no sabía de FX

Las estrategias de Pine pueden declarar una moneda de cuenta distinta de la moneda de cotización del símbolo — currency.INR operando un par en USDT, por ejemplo. Antes de comprobar si puedes permitirte una orden, el broker de TradingView convierte el valor nocional de la orden a tu moneda de cuenta usando el tipo de cambio vigente, y luego compara contra la equity en esa misma moneda.

El gate de affordability del motor se saltaba la conversión. Comparaba el nocional en USDT directamente contra la equity denominada en INR como si el tipo de cambio fuera 1:1 — lo que hacía que una estrategia con currency.INR pareciera unas 83× más asequible de lo que realmente era (dado que 1 USDT vale aproximadamente esa cantidad de rupias). En los scripts afectados, eso prácticamente duplicaba el número de trades que el motor admitía y que el broker de TradingView habría rechazado por no ser asequibles.

El fix aplica el tipo de cambio de la moneda de cuenta al margen requerido antes de que corra el chequeo de affordability, en el mismo punto del pipeline de órdenes donde lo aplica TradingView. Si estás backtesteando con una moneda de cuenta distinta del USD frente a un símbolo cotizado en USD o USDT, este era el bug que estaba inflando tu número de trades sin que te dieras cuenta.

Bug tres: el trailing stop que nunca se armaba

Este es el que más nos gustaría que un usuario hubiera detectado antes que nosotros, porque falla en silencio. strategy.exit() admite dos formas de especificar un trailing stop: trail_points, una distancia relativa a la entrada, y trail_price, un precio de activación absoluto.

// 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 condición de armado del motor solo comprobaba trail_points. Una salida trailing especificada únicamente vía trail_price compilaba sin error, corría sin error, y nunca se armaba — la posición simplemente se quedaba ahí sin ningún trailing stop activo, en silencio, durante todo el backtest. Sin excepción, sin línea de log, sin trade. La estrategia parecía protegida. No lo estaba.

Encontramos esto porque una prueba del corpus aísla específicamente la activación solo por trail_price — exactamente el tipo de estrategia sintética y acotada para la que existe la categoría validation, según el post sobre el desglose del corpus: no rentable, no realista, construida para ejercitar una sola función de Pine en aislamiento, de modo que un regresión así no pueda esconderse detrás de "los números generales se veían bien".

El fix arma el trailing stop si hay presente trail_points o trail_price. Si alguna vez escribiste una estrategia de Pine usando trail_price y la curva de equity se veía sospechosamente como si no hubiera ningún trailing stop — no lo había, en ninguna versión del motor anterior a esta.

Por qué esto es el verdadero sentido de tener un corpus

Ninguno de estos tres bugs se encontró por code review. Se encontraron porque una estrategia con la forma correcta corrió por el motor y produjo un número que no coincidía con el de TradingView. Esa es toda la tesis detrás de mantener 167 estrategias, luego 252, y contando como un corpus versionado y held-out en vez de un puñado de smoke tests: bugs que no cambian el recuento de trades ni el precio de entrada pueden seguir cambiando el número que de verdad importa — cuánto ganó o perdió la estrategia — y la única forma de detectarlos es seguir corriendo más formas de estrategia contra el mismo diff frente a la misma ground truth.

Seguiremos añadiendo pruebas a medida que encontremos gaps, y seguiremos publicando los números — los buenos y la única anomalía — tal como estén.

Por dónde seguir