Este artículo no es un simple volcado de código de TradingView. Es una explicación línea por línea del código de una estrategia de trading desarrollada en Pinescript, por qué se tomó cada decisión, y qué se rompe si la eliminas.
Para mostrar como se programa una estrategia en Pinescript se usa como ejemplo un método de trading para operar en el BTCUSD en el gráfico diario. Recorrido completo desde la declaración hasta el backtesting.
La mayoría de los artículos sobre Pine Script te dan código. Pégalo, ejecútalo, mira lo que sucede. El código es el objetivo principal.
Este artículo funciona de manera diferente. El código existe, pero no es lo que importa. Lo que importa es el razonamiento: por qué está cada línea, para qué está diseñada, y qué escribiría un principiante en su lugar que se ve similar pero no funciona de la misma manera.
Al final tendrás una estrategia completa y backtesteable. Más importante aún, entenderás cada decisión estructural que distingue el código que funciona del código que simplemente se ejecuta.
¿En que consiste la estrategia que vamos a programar?
Comprar BTC cuando retroceda dentro de una tendencia alcista y el impulso empiece a recuperarse, con un stop loss ajustado por volatilidad y un take profit definido. Salir cuando se alcance cualquiera de los dos niveles.
Eso es todo. La estrategia completa es esa frase. Cada línea de código o bien implementa uno de esos cuatro componentes —filtro de tendencia alcista, detección de retrocesos, recuperación del impulso, gestión de salida— o bien respalda esos componentes con la configuración adecuada de riesgo y ejecución.
Si una línea de código no se corresponde con uno de esos cuatro componentes, no debería estar ahí.
Código para Pinescript de la estrategia y descripción
A continuación vamos a describir parte por parte el código completo de la estrategia:
Sección 1: Declaración de variables
El bloque strategy(): seis decisiones, no una
La mayoría de los principiantes tratan la llamada a strategy() como código repetitivo: lo copian de una plantilla y siguen adelante. En realidad, cada parámetro que contiene es una elección deliberada que afecta la validez de todo lo que sigue.
Declaración de la estrategia:
//@version=6
strategy("RSI Pullback Strategy",
overlay = true,
initial_capital = 10000,
default_qty_type = strategy.percent_of_equity,
default_qty_value = 25,
commission_type = strategy.commission.percent,
commission_value = 0.1,
slippage = 2)
overlay = true
Dibuja la estrategia directamente sobre el gráfico de precios, en lugar de en un panel separado. Es esencial cuando trazas medias móviles o niveles de stop que deben ser visibles frente al precio.
overlay = false dibuja todo en su propio panel debajo del gráfico. Tu EMA de 200 aparece como un gráfico de líneas independiente sin contexto de precio, algo inútil para una confirmación visual.
default_qty_value = 25
Cada operación utiliza el 25 % del capital disponible. Esto significa que teóricamente podrían estar abiertas cuatro posiciones simultáneas y que una sola pérdida no puede superar el 25 % de la cuenta. Más importante aún: el lastre de las comisiones con un tamaño del 25 % es una cuarta parte de lo que sería con un tamaño del 100 %.
Con un tamaño del 100 % y una comisión del 0,1 % por operación, en una cuenta de $10 000 cada trade cuesta $10. En 300 operaciones, eso son $3 000 en comisiones: un 30 % del capital inicial consumido antes de calcular cualquier resultado neto. Por eso la mayoría de las configuraciones de backtest por defecto producen resultados inflados.
commission_type = strategy.commission.percent
Este parámetro debe establecerse explícitamente cuando se usa comisión porcentual. Sin él, commission_value se interpreta como una cantidad fija en efectivo por operación, lo que arroja resultados completamente erróneos para cualquier tamaño de posición que no sea exactamente $1 000.
Sin esta línea, commission_value=0.1 significa $0,10 fijos por operación, sin importar el tamaño de la posición. En una posición de $2 500 (el 25 % de $10 000), la comisión real es de $2,50. Estarías modelando $0,10. El backtest estaría equivocado por un factor de 25.
slippage = 2
Cada orden se ejecuta 2 ticks peor que el precio de cierre de la vela de señal. En un gráfico diario de BTC, las señales se disparan al cierre, pero las órdenes se ejecutan a la apertura del día siguiente, a menudo entre un 1 % y un 3 % de diferencia. slippage=2 es un reconocimiento conservador, pero distinto de cero, de esta brecha.
slippage=0 implica que cada orden se ejecuta al precio exacto en el que se generó la señal. Esto nunca ocurre en el trading real. Los backtests sin slippage superan sistemáticamente los resultados reales porque cada entrada y salida se modela a un precio óptimo que no existe.
Sección 2 – Entradas
Parámetros como variables: cómo hacer la estrategia ajustable
Declaración de las entradas
// ── Entradas ──────────────────────────────────────────── trendLen = input.int(200, "Longitud EMA de Tendencia", minval=50) rsiLen = input.int(14, "Longitud RSI", minval=2) rsiLevel = input.int(40, "Nivel de Entrada RSI", minval=20, maxval=60) atrLen = input.int(14, "Longitud ATR", minval=1) stopMult = input.float(2.0, "Múltiplo ATR del Stop", minval=0.5, step=0.1) tpMult = input.float(3.0, "Múltiplo ATR del TP", minval=0.5, step=0.1)
Por qué usar entradas en lugar de números fijos
Cada número en una estrategia es una hipótesis sobre lo que funciona. Las entradas hacen esa hipótesis visible, ajustable y comprobable. Cuando un colega pregunta “¿por qué 200 para la EMA de tendencia?”, una entrada con etiqueta y rango comunica que se trata de un valor predeterminado meditado, no de una constante arbitraria.
Los números fijos —escribir ta.ema(close, 200) directamente— producen un código que requiere editar el código fuente para cambiar cualquier parámetro. Eso no es un problema hasta que necesitas comparar la EMA 100 contra la EMA 200 contra la EMA 50. Las entradas convierten ese cambio en un ajuste simple desde el panel de Configuración, no en una edición del código.
minval y maxval en cada entrada
Establecer minval=50 en la EMA de tendencia evita que alguien (incluido tu yo del futuro) la configure a 5, lo que produciría resultados sin sentido. maxval=60 en el nivel de entrada del RSI impide entradas por encima del punto medio, lo que transformaría accidentalmente una estrategia de retrocesos en una estrategia de ruptura.
Sin límites, se acepta cualquier valor. Un nivel de entrada del RSI de 80 significa que la estrategia solo entra cuando el RSI está por encima de 80, que es territorio de sobrecompra, exactamente lo opuesto a una entrada en un retroceso. El código se ejecuta sin errores y produce resultados erróneos en silencio.
Sección 3 – Cálculos
Construyendo los tres componentes de la señal
Cálculos de la señal
// ── Filtro de Tendencia ────────────────────────────────────── trendEma = ta.ema(close, trendLen) inUptrend = close > trendEma // ── Momento RSI ────────────────────────────────────────────── rsiVal = ta.rsi(close, rsiLen) rsiRecov = ta.crossover(rsiVal, rsiLevel) // ── Señal de Entrada - ambas condiciones deben ser verdaderas ── entrySignal = inUptrend and rsiRecov // ── ATR para dimensionar la salida ───────────────────────────── atrVal = ta.atr(atrLen)
inUptrend = close > trendEma (no ema > ema[1])
Este código hace la siguiente pregunta básica: ¿Está el precio por encima de la línea de tendencia? En otras palabras, es un filtro binario. La alternativa, comprobar si la propia EMA está subiendo, introduce retraso y produce un comportamiento diferente en activos con tendencia frente a activos en rango. Precio por encima de la EMA es más simple, más directo y responde a la pregunta exacta: ¿es este un contexto de mercado alcista?
Usar trendEma > trendEma[1] como filtro de tendencia significa entrar cuando la EMA de 200 acaba de empezar a subir, lo que va 200 velas por detrás del precio. Estarías confirmando una tendencia que el precio descubrió hace meses. El filtro se convierte en un mecanismo de retraso en lugar de una condición.
ta.crossover(rsiVal, rsiLevel) — cruce, no comparación
ta.crossover(a, b) es verdadero solo en la barra en la que a cruza por encima de b. Usar rsiVal > rsiLevel en su lugar sería verdadero en todas las barras donde el RSI esté por encima de 40, lo que podría significar que la estrategia permanece dentro de una operación durante semanas con la condición de entrada satisfecha continuamente, pudiendo reentrar inmediatamente después de una salida.
Usar rsiVal > 40 en vez de ta.crossover: la entrada se activa en cada vela con RSI > 40, no solo en la del cruce. Durante un periodo sostenido de RSI por encima de 40, obtendrías docenas de reentradas. Esto significa que crossover se activa una vez por evento. La comparación simple se activa continuamente. Son señales fundamentalmente distintas.
entrySignal = inUptrend and rsiRecov (variable con nombre)
Combinar ambas condiciones en una variable con nombre sirve a dos propósitos. Hace que la lógica de entrada se lea como una frase: «la señal de entrada es verdadera cuando hay tendencia alcista y el RSI se recupera». Y permite reutilizarla: la misma variable va al bloque if y a la llamada plotshape() sin duplicar la condición.
Escribir la condición completa dos veces —dentro del if y dentro de plotshape()— no solo es redundante. Si modificas una y te olvidas de la otra, los marcadores del gráfico ya no coinciden con las barras reales de entrada. La variable con nombre garantiza consistencia.
Sección 4 – Ejecución
La lógica de entrada y salida: donde la mayoría de las estrategias tienen sus peores errores
Ejecución de entrada y salida
// ── Entrada ─────────────────────────────────────────────
if entrySignal
strategy.entry("Long", strategy.long)
// ── Salida - basada en el precio de ejecución real ─────────────────
// Detecta la vela en la que se acaba de abrir la posición
var float stopLevel = na
var float tpLevel = na
if strategy.position_size > 0 and
strategy.position_size[1] == 0
entryPrice = strategy.position_avg_price
stopLevel := entryPrice - stopMult * atrVal
tpLevel := entryPrice + tpMult * atrVal
strategy.exit("Exit", "Long",
stop = stopLevel,
limit = tpLevel)
strategy.position_size > 0 and strategy.position_size[1] == 0
Esta condición es verdadera en exactamente una vela: la primera vela en la que la estrategia mantiene una posición después de no tener ninguna. Detecta el momento en que se ejecutó la entrada. Es la única vela en la que strategy.position_avg_price refleja el precio real de entrada, porque la posición se acaba de abrir.
Establecer los niveles de salida en el mismo bloque if entrySignal que la entrada es la versión más común de este error. La señal se dispara al cierre de la vela. La entrada se ejecuta a la apertura de la vela siguiente. En la vela de señal, strategy.position_avg_price todavía es 0.0. El stop loss y el take profit se calculan a partir de cero, no del precio real de entrada.
var float stopLevel = na (declarada fuera del bloque if)
Declarar con var implica que estas variables persisten a través de las velas: una vez asignadas, mantienen su valor hasta que la siguiente entrada las actualiza. Declararlas fuera del bloque if permite que sean accesibles en la sección de trazado al final. Sin var, se reinician a na en cada vela.
Sin var: stopLevel y tpLevel son na en todas las velas excepto en la de entrada. Las llamadas a plot() al final reciben na en cada vela, y las líneas de stop/take profit no se dibujan en el gráfico, lo que imposibilita verificar visualmente la ubicación de la salida.
stopLevel = entryPrice − stopMult × atrVal
El ATR (Average True Range) mide la distancia promedio que se mueve BTC en un día. Un stop de 2x ATR sitúa la salida por pérdida a dos movimientos diarios típicos por debajo de la entrada. Esto es sensible a la volatilidad: cuando BTC está agitado, el stop se amplía. Cuando está tranquilo, se estrecha. Un stop de porcentaje fijo (−2 %) no se adapta a las condiciones.
Un stop fijo de entryPrice × 0.98 durante un período en el que el ATR de BTC es del 4 % se alcanzará casi de inmediato: el rango diario supera la distancia del stop. Durante períodos de baja volatilidad, un 2 % puede ser demasiado amplio. Los stops basados en ATR ajustan el stop al comportamiento real del mercado en lugar de a un porcentaje arbitrario.
tpMult = 3.0 (take profit a 3x ATR, stop a 2x ATR)
La relación recompensa/riesgo en la entrada es de 3:2 = 1.5. Si se alcanza el stop, la pérdida es de 2 ATR. Si se alcanza el take profit, la ganancia es de 3 ATR. Con una tasa de acierto del 40 %, esta estrategia queda en equilibrio (40 % × 3 = 120, 60 % × 2 = 120). Cualquier tasa de acierto superior al 40 % es rentable antes de comisiones. La relación R:R determina la tasa de acierto mínima requerida: una decisión deliberada de diseño, no un valor por defecto.
Un stop y un take profit iguales (ambos a 2x ATR) exigen una tasa de acierto superior al 50 % para ser rentable. Es un requisito más duro. Una relación R:R de 3:2 es más indulgente: la estrategia puede equivocarse más veces de las que acierta y aún así ganar dinero. Por eso la R:R se elige deliberadamente y no se deja como un valor predeterminado.
Sección 5 — Trazado de elementos visuales
Gráficos que sirven a la estrategia, no que la decoran
Salidas visuales
// ── Visuales ───────────────────────────────────────────
plot(trendEma, "EMA de Tendencia",
color=color.blue, linewidth=2)
plot(strategy.position_size > 0 ? stopLevel : na,
"Stop", color=color.red,
style=plot.style_linebr, linewidth=1)
plot(strategy.position_size > 0 ? tpLevel : na,
"TP", color=color.green,
style=plot.style_linebr, linewidth=1)
plotshape(entrySignal, style=shape.triangleup,
location=location.belowbar,
color=color.new(color.green, 0), size=size.small)
bgcolor(strategy.position_size > 0 ?
color.new(color.green, 92) : na)
plot(strategy.position_size > 0 ? stopLevel : na)
La línea de stop solo se dibuja cuando la estrategia mantiene una posición. Cuando no hay operación, na le indica a Pine Script que no dibuje nada; plot.style_linebr rompe entonces la línea en lugar de conectar el último valor con el siguiente. El resultado: un segmento de línea horizontal limpio para cada operación, no una línea continua a lo largo de todo el gráfico.
plot(stopLevel) sin la condición dibuja una línea continua en todas las barras: cuando está en una operación muestra el stop, y cuando no lo está, dibuja el último valor del stop indefinidamente por todo el gráfico. El gráfico se ve saturado y los niveles de stop resultan visualmente engañosos fuera de las posiciones activas.
Código completo de la estrategia
Todas las secciones anteriores en conjunto
//@version=6
strategy("Estrategia de Retroceso con RSI",
overlay = true,
initial_capital = 10000,
default_qty_type = strategy.percent_of_equity,
default_qty_value = 25,
commission_type = strategy.commission.percent,
commission_value = 0.1,
slippage = 2)
// ── Entradas ────────────────────────────────────────────
trendLen = input.int(200, "Longitud EMA de Tendencia", minval=50)
rsiLen = input.int(14, "Longitud RSI", minval=2)
rsiLevel = input.int(40, "Nivel de Entrada RSI", minval=20, maxval=60)
atrLen = input.int(14, "Longitud ATR", minval=1)
stopMult = input.float(2.0, "Múltiplo ATR del Stop", minval=0.5, step=0.1)
tpMult = input.float(3.0, "Múltiplo ATR del TP", minval=0.5, step=0.1)
// ── Cálculos ──────────────────────────────────────
// ── Filtro de Tendencia ──────────────────────────────────────
trendEma = ta.ema(close, trendLen)
inUptrend = close > trendEma
// ── Momento RSI ──────────────────────────────────────
rsiVal = ta.rsi(close, rsiLen)
rsiRecov = ta.crossover(rsiVal, rsiLevel)
// ── Señal de Entrada - ambas condiciones deben ser verdaderas ──
entrySignal = inUptrend and rsiRecov
// ── ATR para dimensionar la salida ─────────────────────────────
atrVal = ta.atr(atrLen)
// ── Entrada ─────────────────────────────────────────────
if entrySignal
strategy.entry("Long", strategy.long)
// ── Salida - establecida a partir del precio de ejecución real ─────────────────
// Detecta la vela en la que se acaba de abrir la posición
var float stopLevel = na
var float tpLevel = na
if strategy.position_size > 0 and
strategy.position_size[1] == 0
entryPrice = strategy.position_avg_price
stopLevel := entryPrice - stopMult * atrVal
tpLevel := entryPrice + tpMult * atrVal
strategy.exit("Exit", "Long",
stop = stopLevel,
limit = tpLevel)
// ── Visuales ───────────────────────────────────────────
plot(trendEma, "EMA de Tendencia", color=color.blue, linewidth=2)
plot(strategy.position_size > 0 ? stopLevel : na,
"Stop", color=color.red,
style=plot.style_linebr, linewidth=1)
plot(strategy.position_size > 0 ? tpLevel : na,
"TP", color=color.green,
style=plot.style_linebr, linewidth=1)
plotshape(entrySignal, style=shape.triangleup,
location=location.belowbar,
color=color.new(color.green, 0), size=size.small)
bgcolor(strategy.position_size > 0 ?
color.new(color.green, 92) : na)
Resultados del backtesting
Podemos probar esta estrategia en un gráfico diario del BTCUSDT con cualquier ventana reciente. Esto es lo que mostrará el Probador de Estrategias y lo que cada número te dice sobre la lógica:

El número de operaciones es bajo porque el cruce del RSI en 40 es una señal selectiva: solo se activa cuando el RSI se recupera de una caída genuina, no en cada vela en la que el RSI está por encima de 40. El lastre de las comisiones está controlado porque el tamaño del 25 % implica que las tarifas son una cuarta parte de lo que serían con el tamaño completo de la posición. La relación ganancia/pérdida es superior a 1.0 porque la recompensa/riesgo de 3:2 basada en ATR está incorporada en la lógica de salida.
Que esta estrategia sea rentable depende por completo del régimen de mercado. En un mercado alcista con tendencia, las entradas en retrocesos del RSI capturan la continuación y alcanzan el take profit con frecuencia. En un mercado agitado y oscilante, las entradas se activan en falsas recuperaciones que se revierten antes de llegar al take profit y son cerradas con pérdida por el stop loss. No es que la estrategia esté completamente rota en los mercados “choppy”: simplemente no está diseñada para ellos.
Una estrategia que funciona en un régimen y falla en otro no es una mala estrategia. Es una estrategia con una condición de funcionamiento definida. Conocer esa condición es la diferencia entre una estrategia que puedes usar y una estrategia que solo pruebas.
Pueden acceder a una lista de brokers recomendadados para TradingView en: Directorio de brokers de TradingView
Que hace distinto a este artículo de una simple presentación de código
Cada línea de esta estrategia existe por una razón que ahora conoces. El parámetro commission_type está ahí porque sin él el cálculo de la comisión es erróneo. El ta.crossover está ahí porque una comparación simple se activaría continuamente. La salida se establece en la vela posterior a la entrada porque es cuando se conoce el precio de ejecución. El stop utiliza el ATR porque un porcentaje fijo no se adapta a la volatilidad.
Ninguna de estas decisiones es evidente con solo leer el código. Solo son visibles cuando entiendes qué hace cada línea y qué sucede si la quitas o la cambias. Esa comprensión es lo que separa a un programador que escribe estrategias de uno que realmente las entiende.
La próxima vez que leas el Pine Script de otra persona —de la biblioteca pública, de un tutorial, de una IA— ahora tienes un marco para evaluarlo. No se trata de «¿este código se ejecuta?», sino de «¿cada línea tiene una razón y sé cuál es esa razón?».
¿Quieres desarrollar una estrategia de trading o indicador modificado para TradingView pero no tienes el tiempo o el conocimiento para hacerlo por tu cuenta? Comunícate con nosotros y te podremos ayudar. Más información en: admin@tecnicasdetrading.com














