All boards › Script help

Estrategia JM

Started by Member · 19 Oct 2026, 21:08

19 Oct 2026, 21:08

//@version=6
strategy("Estrategia JM - Backtest Sesiones (A+)", shorttitle = "JM Backtest Sesiones", overlay = true, initial_capital = 50000, currency = currency.USD, default_qty_type = strategy.fixed, default_qty_value = 1, pyramiding = 0, process_orders_on_close = true, commission_type = strategy.commission.cash_per_contract, commission_value = 0.5, slippage = 1, margin_long = 0, margin_short = 0)

// =====================================================================
// BACKTEST - "Estrategia de Trading JM" (M5)
// Usa la MISMA lógica de detección del indicador de alertas (versión 6) y la
// convierte en operaciones para medir resultados en el Strategy Tester.
// Úsalo en un gráfico de 5 minutos, de UN solo activo (ej. MNQ1!).
//
// Reglas aplicadas:
// - Entrada: al cierre de la vela que forma el imbalance de M5 (señal A+ o A)
// - Stop A+ : bajo/sobre el VWAP o la toma de liquidez, el que quede más cerca
// - Stop A : en la toma de liquidez
// - Objetivo: 1:2 (configurable)
// - Riesgo fijo en dólares por operación; contratos = riesgo / (stop x valor del punto)
// - Máximo 2 operaciones por día, una posición a la vez
// - Solo escenario A+ (puedes activar también A en la sección 2)
// - Horario: solo tus sesiones, en hora de Nueva York (igual que el indicador):
// Asia 20-22 (domingo a jueves) · Londres 02-04 y NY 09-12 (lunes a viernes)
// - Bias: MANUAL por día (lista), o sin bias / solo compras / solo ventas.
// La sesión de Asia (domingo a jueves por la tarde) usa el bias del DÍA SIGUIENTE:
// Asia del domingo = lunes. Londres y NY usan el bias de su propio día.
// - Una posición a la vez; se cierra X minutos después de terminar la sesión en que se abrió
// - Al final del gráfico aparece una tabla con el resultado por sesión.
// - DIAGNÓSTICO (abajo a la derecha): separa las operaciones en las que, después de entrar,
// el imbalance M5 del gatillo SE INVIRTIÓ (cierre con cuerpo al otro lado de la zona) de las
// que no, con ganadas/perdidas y las velas M5 que pasaron hasta la inversión.
// OJO: es para estudiar. No se puede filtrar por algo que solo se sabe después de entrar.
// - STOP EN ATR: tercera opción de stop, a una distancia fija de N veces el ATR del precio de entrada.
// - STOP DEL A+: elige "El más cercano (VWAP o liquidez)" o "Siempre la toma de liquidez".
// - DISTANCIA MÁXIMA DEL STOP (en ATR): descarta señales cuyo stop queda demasiado lejos del precio.
// - ESTADÍSTICA DE STOP Y TP (abajo a la izquierda): por separado para ganadoras y perdedoras,
// cuánto retrocedió el precio en contra (MAE) y cuánto avanzó a favor (MFE) antes de cerrar,
// en múltiplos del riesgo (R) y en puntos, más el tamaño del stop en puntos y en ATR.
// - PREMIUM / DESCUENTO: compras solo en la mitad baja y ventas solo en la mitad alta de la estructura
// de M15 y/o H1 (último swing alto a último swing bajo de ese marco). Apagado por defecto.
// - CONTEXTO DE LA ESTRUCTURA (arriba a la izquierda): agrupa las operaciones por
// (1) ubicación en el rango del día (0 = lado favorable: abajo para compras, arriba para ventas),
// (2) pendiente del VWAP (plano / moderado / inclinado) y (3) distancia al VWAP en ATR,
// con operaciones, ganadas, % de acierto y neto de cada grupo.
// - CONFIRMACIÓN: "Esperar N velas M5" entra N velas después del A+ solo si el imbalance
// sigue sin invertirse y el precio no tocó el stop (0 = entrar al cierre del A+).
// - Para comparar "con bias" contra "sin bias": corre dos veces (Manual / Sin bias).
// =====================================================================

// ------------------------- ENTRADAS ----------------------------------
gLog = "1) Lógica del setup (igual que el indicador)"
gOp = "2) Operativa"
gBi = "3) Bias y horario"

zzATR = input.float(2.0, "Retroceso para confirmar un swing (en ATR)", minval = 0.5, step = 0.1, group = gLog)
sweepTol = input.float(1.0, "Tolerancia del barrido (en ATR)", minval = 0.0, step = 0.05, group = gLog)
minGap = input.float(0.1, "Tamaño mínimo del imbalance (en ATR)", minval = 0.0, step = 0.05, group = gLog)
maxBars = input.int(24, "Vigencia del barrido hasta la ruptura (velas M5)", minval = 3, group = gLog)
postBos = input.int(36, "Vigencia del imbalance tras la ruptura (velas M5)", minval = 1, group = gLog)
flipWin = input.int(12, "Cruce reciente del VWAP (velas)", minval = 1, group = gLog)
nearATR = input.float(1.0, "Precio 'cerca' del VWAP (en ATR)", minval = 0.1, step = 0.1, group = gLog)

riskUSD = input.float(250.0, "Riesgo por operación (USD)", minval = 1.0, group = gOp, tooltip = "Tus reglas: cuenta 25K challenge 250, fondeada 100; cuenta 50K challenge 500, fondeada 200.")
rr = input.float(2.0, "Objetivo (1:R)", minval = 0.5, step = 0.5, group = gOp)
maxTrades = input.int(2, "Máximo de operaciones por día", minval = 1, group = gOp)
onlyAplus = input.bool(true, "Operar solo escenario A+", group = gOp)
bufTicks = input.int(2, "Colchón del stop (ticks)", minval = 0, group = gOp, tooltip = "Distancia extra más allá del VWAP o de la toma de liquidez.")
minStopATR = input.float(0.5, "Stop mínimo (en ATR)", minval = 0.0, step = 0.1, group = gOp, tooltip = "Descarta operaciones con stop absurdamente corto (inflarían los contratos). Si el stop del A+ queda más corto, se usa la toma de liquidez.")
maxQty = input.int(50, "Máximo de contratos por operación", minval = 1, group = gOp)
stopMode = input.string("El más cercano (VWAP o liquidez)", "Stop del escenario A+", options = ["El más cercano (VWAP o liquidez)", "Siempre la toma de liquidez", "ATR fijo (múltiplo)"], group = gOp, tooltip = "El más cercano: debajo (largo) o encima (corto) del VWAP o de la toma de liquidez, el que quede más cerca. Siempre la toma de liquidez: bajo el mínimo (o sobre el máximo) que hizo el barrido, como en el escenario A.")
atrStopMult = input.float(2.0, "Stop en ATR: múltiplo del ATR (solo si el stop es ATR fijo)", minval = 0.5, step = 0.25, group = gOp, tooltip = "Con el stop en ATR fijo, el stop queda a esta cantidad de ATR del precio de entrada y el objetivo sigue siendo 1:R.")
maxStopATR = input.float(0.0, "Distancia máxima del stop (en ATR, 0 = sin límite)", minval = 0.0, step = 0.5, group = gOp, tooltip = "Descarta la señal si el stop queda a más de esta distancia del precio de entrada. Sirve para no entrar cuando el precio está muy lejos del mínimo (o máximo) de la estructura.")
pdMode = input.string("Apagado", "Filtro premium / descuento", options = ["Apagado", "M15", "H1", "M15 y H1"], group = gOp, tooltip = "Compras solo por debajo del nivel de la estructura del marco elegido (50% = mitad); ventas solo por encima del nivel espejo. La estructura es el rango entre el último swing alto y el último swing bajo confirmados en ese marco.")
pdLevel = input.float(0.5, "Nivel premium / descuento (0.5 = mitad de la estructura)", minval = 0.1, maxval = 0.9, step = 0.05, group = gOp)
pdZZ = input.float(2.0, "Retroceso para confirmar un swing de M15 / H1 (en ATR)", minval = 0.5, step = 0.5, group = gOp)
flatBars = input.int(24, "Contexto: velas M5 para medir la pendiente del VWAP (24 = 2 horas)", minval = 3, group = gOp)
confirmN = input.int(0, "Esperar N velas M5 de confirmación antes de entrar (0 = entrar al cierre del A+)", minval = 0, maxval = 6, group = gOp, tooltip = "Entra N velas M5 después del A+ solo si el imbalance M5 del gatillo no se ha invertido ni el precio ha tocado el stop. El stop se mantiene donde estaba; el objetivo 1:R y el tamaño se recalculan con el precio real de entrada.")

biasMode = input.string("Manual por día (lista)", "Bias", options = ["Manual por día (lista)", "Sin bias (ambas direcciones)", "Solo compras", "Solo ventas"], group = gBi, tooltip = "Manual: usa tu lista por día (A compras, B ventas, N no opera). Sin bias: opera ambas direcciones.")
biasList = input.text_area("", "Bias por día (para el modo Manual)", group = gBi, tooltip = "Una línea por día: fecha AAAA-MM-DD y una letra. A = alcista (solo compras), B = bajista (solo ventas), N = sin bias (no opera). Ejemplo: 2026-09-02 A. Los días que no estén en la lista cuentan como N.")
useAsia = input.bool(true, "Sesión de Asia (domingo a jueves)", group = gBi)
asiaS = input.int(20, "Asia: desde la hora (NY)", minval = 0, maxval = 23, group = gBi)
asiaE = input.int(22, "Asia: hasta la hora (NY)", minval = 1, maxval = 24, group = gBi)
useLon = input.bool(true, "Sesión de Londres (lunes a viernes)", group = gBi)
lonS = input.int(2, "Londres: desde la hora (NY)", minval = 0, maxval = 23, group = gBi)
lonE = input.int(4, "Londres: hasta la hora (NY)", minval = 1, maxval = 24, group = gBi)
useNY = input.bool(true, "Sesión de Nueva York (lunes a viernes)", group = gBi)
nyS = input.int(9, "Nueva York: desde la hora (NY)", minval = 0, maxval = 23, group = gBi)
nyE = input.int(12, "Nueva York: hasta la hora (NY)", minval = 1, maxval = 24, group = gBi)
graceMin = input.int(60, "Cerrar la posición X minutos después de terminar su sesión", minval = 0, group = gBi, tooltip = "Si la operación no llegó a TP ni a SL, se cierra a mercado pasado ese tiempo.")

// ------------------------- LÓGICA DE DETECCIÓN -----------------------
// Devuelve [código, mínimo del barrido (compras), máximo del barrido (ventas), VWAP, ATR]
// código: +1 compra A+, +2 compra A, -1 venta A+, -2 venta A, 0 nada.
core() =>
float atrV = ta.atr(14)

// VWAP (con respaldo si el activo no tiene volumen: promedio del día)
float vwB = ta.vwap(hlc3)
var float sP = 0.0
var int sN = 0
if timeframe.change("D")
    sP := 0.0
    sN := 0
sP += hlc3
sN += 1
float vw = na(vwB) ? sP / sN : vwB

// Cruces del precio sobre el VWAP
bool crossUp = ta.crossover(close, vw)
bool crossDn = ta.crossunder(close, vw)
int sinceUp = nz(ta.barssince(crossUp), 9999)
int sinceDn = nz(ta.barssince(crossDn), 9999)
float lo3 = ta.lowest(low, 3)
float hi3 = ta.highest(high, 3)

// Swings por retroceso (zigzag en ATR)
float dev = zzATR * atrV
var int zzDir = 0
var float legHi = na
var float legLo = na
var float swHi = na
var float swLo = na
if na(legHi)
    legHi := high
    legLo := low
if zzDir >= 0
    legHi := math.max(legHi, high)
    if not na(dev) and low <= legHi - dev
        swHi := legHi
        zzDir := -1
        legLo := low
else
    legLo := math.min(legLo, low)
    if not na(dev) and high >= legLo + dev
        swLo := legLo
        zzDir := 1
        legHi := high

float tol = sweepTol * atrV

// ---------------- COMPRAS ----------------
var float lSweepLow = na
var int lSweepBar = na
var float lBosLvl = na
var int lBosBar = na

bool dropL = (not na(lSweepBar) and na(lBosBar) and bar_index - lSweepBar > maxBars) or (not na(lBosBar) and (bar_index - lBosBar > postBos or close < lSweepLow))
if dropL
    lSweepBar := na
    lSweepLow := na
    lBosLvl := na
    lBosBar := na

float refLow = na(lSweepLow) ? swLo : math.min(swLo, lSweepLow)
bool touchL = zzDir == -1 and not na(swLo) and not na(swHi) and low <= refLow + tol and swHi > close and na(lBosBar)
if touchL
    lSweepLow := na(lSweepLow) ? low : math.min(lSweepLow, low)
    lSweepBar := bar_index
    lBosLvl := swHi

if not na(lSweepBar) and na(lBosBar) and bar_index > lSweepBar and close > lBosLvl
    lBosBar := bar_index

bool fvgBull = low > high[2] and (low - high[2]) >= minGap * atrV
bool trigL = not na(lBosBar) and fvgBull and close > lBosLvl

// ---------------- VENTAS ----------------
var float sSweepHigh = na
var int sSweepBar = na
var float sBosLvl = na
var int sBosBar = na

bool dropS = (not na(sSweepBar) and na(sBosBar) and bar_index - sSweepBar > maxBars) or (not na(sBosBar) and (bar_index - sBosBar > postBos or close > sSweepHigh))
if dropS
    sSweepBar := na
    sSweepHigh := na
    sBosLvl := na
    sBosBar := na

float refHigh = na(sSweepHigh) ? swHi : math.max(swHi, sSweepHigh)
bool touchS = zzDir == 1 and not na(swHi) and not na(swLo) and high >= refHigh - tol and swLo < close and na(sBosBar)
if touchS
    sSweepHigh := na(sSweepHigh) ? high : math.max(sSweepHigh, high)
    sSweepBar := bar_index
    sBosLvl := swLo

if not na(sSweepBar) and na(sBosBar) and bar_index > sSweepBar and close < sBosLvl
    sBosBar := bar_index

bool fvgBear = high < low[2] and (low[2] - high) >= minGap * atrV
bool trigS = not na(sBosBar) and fvgBear and close < sBosLvl

// ---------------- Clasificación A+ / A ----------------
float vwD = nearATR * atrV
int codeL = 0
int codeS = 0

if trigL and not na(vw)
    if (lo3 - vw) <= vwD and close >= vw and sinceUp <= flipWin
        codeL := 1
    else if close < vw and (vw - close) > vwD
        codeL := 2

if trigS and not na(vw)
    if (vw - hi3) <= vwD and close <= vw and sinceDn <= flipWin
        codeS := -1
    else if close > vw and (close - vw) > vwD
        codeS := -2

// Se guardan los extremos del barrido ANTES de reiniciar el setup
float stL = lSweepLow
float stS = sSweepHigh

if codeL != 0
    lSweepBar := na
    lSweepLow := na
    lBosLvl := na
    lBosBar := na

if codeS != 0
    sSweepBar := na
    sSweepHigh := na
    sBosLvl := na
    sBosBar := na

int code = codeL != 0 ? codeL : codeS

[code, stL, stS, vw, atrV]

[code, stL, stS, vw, atrV] = core()

// ------------------------- CONTEXTO DE LA ESTRUCTURA -------------------------
// Rango del día del activo (desde el cambio de día), pendiente y distancia al VWAP, en ATR.
var float dHi = na
var float dLo = na
if timeframe.change("D") or na(dHi)
dHi := high
dLo := low
else
dHi := math.max(dHi, high)
dLo := math.min(dLo, low)
float dRng = dHi - dLo
float posDay = dRng > 0 ? (close - dLo) / dRng : 0.5
float vwSlope = (not na(vw) and not na(vw[flatBars]) and atrV > 0) ? math.abs(vw - vw[flatBars]) / atrV : na
float vwDist = (not na(vw) and atrV > 0) ? math.abs(close - vw) / atrV : na

// ------------------------- BIAS, HORARIO Y LÍMITES -------------------

// Bias manual por día: se lee la lista una sola vez
var map<string, string> biasMap = map.new<string, string>()
if barstate.isfirst
string[] lines = str.split(biasList, "\n")
for i = 0 to array.size(lines) - 1
string t = str.trim(array.get(lines, i))
string[] p = str.split(t, " ")
if array.size(p) >= 2
map.put(biasMap, array.get(p, 0), str.upper(str.trim(array.get(p, array.size(p) - 1))))

// ---- Sesión de la vela (hora de apertura de la vela, en Nueva York) ----
string tzNY = "America/New_York"
int hhB = hour(time, tzNY)
int dwB = dayofweek(time, tzNY)
bool isAsia = useAsia and dwB >= dayofweek.sunday and dwB <= dayofweek.thursday and hhB >= asiaS and hhB < asiaE
bool isLon = useLon and dwB >= dayofweek.monday and dwB <= dayofweek.friday and hhB >= lonS and hhB < lonE
bool isNYs = useNY and dwB >= dayofweek.monday and dwB <= dayofweek.friday and hhB >= nyS and hhB < nyE
bool inSess = isAsia or isLon or isNYs
string sessName = isAsia ? "Asia" : (isLon ? "Londres" : (isNYs ? "NY" : ""))
int sessEndH = isAsia ? asiaE : (isLon ? lonE : (isNYs ? nyE : 0))
// Asia (tarde/noche) usa el bias del día siguiente; Londres y NY, el de su propio día
int biasT = isAsia ? time + 86400000 : time
string dayKey = str.format_time(biasT, "yyyy-MM-dd", tzNY)
string manualBias = map.contains(biasMap, dayKey) ? map.get(biasMap, dayKey) : "N"

bool okLong = biasMode == "Sin bias (ambas direcciones)" or biasMode == "Solo compras" or (biasMode == "Manual por día (lista)" and manualBias == "A")
bool okShort = biasMode == "Sin bias (ambas direcciones)" or biasMode == "Solo ventas" or (biasMode == "Manual por día (lista)" and manualBias == "B")

// El contador de operaciones se reinicia al empezar el día de bias (Asia + Londres + NY de ese día suman juntas)
var string lastKey = ""
var int tradesToday = 0
var int closeAt = na
if inSess and dayKey != lastKey
lastKey := dayKey
tradesToday := 0

// ------------------------- POSICIÓN DENTRO DE LA ESTRUCTURA (M15 / H1) ------
// Devuelve 0 = en el último swing bajo, 1 = en el último swing alto, 0.5 = mitad. Fuera de rango: <0 o >1.
// Se lee con la última vela cerrada del marco (sin mirar al futuro).
rangePos() =>
float atrR = ta.atr(14)
var int zDir = 0
var float lHi = na
var float lLo = na
var float sHi = na
var float sLo = na
if na(lHi)
lHi := high
lLo := low
float dv = pdZZ * atrR
if zDir >= 0
lHi := math.max(lHi, high)
if not na(dv) and low <= lHi - dv
sHi := lHi
zDir := -1
lLo := low
else
lLo := math.min(lLo, low)
if not na(dv) and high >= lLo + dv
sLo := lLo
zDir := 1
lHi := high
float pos = (not na(sHi) and not na(sLo) and sHi > sLo) ? (close - sLo) / (sHi - sLo) : na
pos[1]

float posM15 = request.security(syminfo.tickerid, "15", rangePos(), lookahead = barmerge.lookahead_on)
float posH1 = request.security(syminfo.tickerid, "60", rangePos(), lookahead = barmerge.lookahead_on)
bool pdLm15 = na(posM15) ? true : posM15 < pdLevel
bool pdLh1 = na(posH1) ? true : posH1 < pdLevel
bool pdSm15 = na(posM15) ? true : posM15 > 1 - pdLevel
bool pdSh1 = na(posH1) ? true : posH1 > 1 - pdLevel
bool pdL = pdMode == "Apagado" or (pdMode == "M15" and pdLm15) or (pdMode == "H1" and pdLh1) or (pdMode == "M15 y H1" and pdLm15 and pdLh1)
bool pdS = pdMode == "Apagado" or (pdMode == "M15" and pdSm15) or (pdMode == "H1" and pdSh1) or (pdMode == "M15 y H1" and pdSm15 and pdSh1)

// ------------------------- OPERACIONES -------------------------------
bool canTrade = strategy.position_size == 0 and tradesToday < maxTrades and inSess
bool gradeOk = code != 0 and (math.abs(code) == 1 or not onlyAplus)

// Señal en espera de confirmación y operación en curso (para el diagnóstico)
var int pDir = 0
var int pCnt = 0
var float pStop = na
var float pTop = na
var float pBot = na
var string pGrade = ""
var string pSess = ""
var float pU = -1.0
var float pSl = -1.0
var float pDs = -1.0
var float[] invBars = array.new_float()
var float[] sdArr = array.new_float()
var float[] uArr = array.new_float()
var float[] slArr = array.new_float()
var float[] dsArr = array.new_float()
var float[] atArr = array.new_float()
var int curDir = 0
var float curTop = na
var float curBot = na
var int curEntryBar = na

bool stopNear = stopMode == "El más cercano (VWAP o liquidez)"
bool stopAtr = stopMode == "ATR fijo (múltiplo)"
float minStop = minStopATR * atrV
float buf = bufTicks * syminfo.mintick

// 1) Señal en espera: cada vela nueva se revisa si el imbalance se invirtió, el precio tocó el stop o ya toca entrar
if pDir != 0
pCnt += 1
bool invP = (pDir == 1 and close < pBot and close < open) or (pDir == -1 and close > pTop and close > open)
bool stopHitP = (pDir == 1 and close <= pStop) or (pDir == -1 and close >= pStop)
if invP or stopHitP or not inSess
pDir := 0
else if pCnt >= confirmN
if strategy.position_size == 0 and tradesToday < maxTrades
float distP = pDir == 1 ? close - pStop : pStop - close
if distP >= minStop and (maxStopATR == 0 or distP <= maxStopATR * atrV)
float qP = math.min(math.floor(riskUSD / (distP * syminfo.pointvalue)), maxQty)
if qP >= 1
if pDir == 1
strategy.entry("Compra", strategy.long, qty = qP, comment = pSess + " " + pGrade)
strategy.exit("Salida C", from_entry = "Compra", stop = pStop, limit = close + rr * distP)
else
strategy.entry("Venta", strategy.short, qty = qP, comment = pSess + " " + pGrade)
strategy.exit("Salida V", from_entry = "Venta", stop = pStop, limit = close - rr * distP)
tradesToday += 1
closeAt := timestamp(tzNY, year(time, tzNY), month(time, tzNY), dayofmonth(time, tzNY), 0, 0) + sessEndH * 3600000 + graceMin * 60000
array.push(invBars, -1.0)
array.push(sdArr, distP)
array.push(atArr, atrV)
array.push(uArr, pU)
array.push(slArr, pSl)
array.push(dsArr, pDs)
curDir := pDir
curTop := pTop
curBot := pBot
curEntryBar := bar_index
pDir := 0

// 2) Señal nueva
if canTrade and gradeOk and not na(vw) and not na(atrV)
bool sigL = false
bool sigS = false
float sStop = na
if code > 0 and okLong and pdL
float sa = math.max(vw, stL) - buf
float sb = stL - buf
sStop := (code == 1 and stopNear) ? ((close - sa) >= minStop ? sa : sb) : sb
if stopAtr
sStop := close - atrStopMult * atrV
sigL := (close - sStop) >= minStop and (maxStopATR == 0 or (close - sStop) <= maxStopATR * atrV)
if code < 0 and okShort and pdS
float sc = math.min(vw, stS) + buf
float sd = stS + buf
sStop := (code == -1 and stopNear) ? ((sc - close) >= minStop ? sc : sd) : sd
if stopAtr
sStop := close + atrStopMult * atrV
sigS := (sStop - close) >= minStop and (maxStopATR == 0 or (sStop - close) <= maxStopATR * atrV)
if sigL or sigS
float zT = sigL ? low : low[2]
float zB = sigL ? high[2] : high
string gr = math.abs(code) == 1 ? "A+" : "A"
// Contexto en la vela del setup: u = 0 es el lado favorable del rango del día (-1 = sin dato o rango pequeño)
float uCtx = (dRng >= 3 * atrV) ? (sigL ? posDay : 1 - posDay) : -1.0
float slCtx = na(vwSlope) ? -1.0 : vwSlope
float dsCtx = na(vwDist) ? -1.0 : vwDist
if confirmN == 0
float dist0 = sigL ? close - sStop : sStop - close
float q0 = math.min(math.floor(riskUSD / (dist0 * syminfo.pointvalue)), maxQty)
if q0 >= 1
if sigL
strategy.entry("Compra", strategy.long, qty = q0, comment = sessName + " " + gr)
strategy.exit("Salida C", from_entry = "Compra", stop = sStop, limit = close + rr * dist0)
else
strategy.entry("Venta", strategy.short, qty = q0, comment = sessName + " " + gr)
strategy.exit("Salida V", from_entry = "Venta", stop = sStop, limit = close - rr * dist0)
tradesToday += 1
closeAt := timestamp(tzNY, year(time, tzNY), month(time, tzNY), dayofmonth(time, tzNY), 0, 0) + sessEndH * 3600000 + graceMin * 60000
array.push(invBars, -1.0)
array.push(sdArr, dist0)
array.push(atArr, atrV)
array.push(uArr, uCtx)
array.push(slArr, slCtx)
array.push(dsArr, dsCtx)
curDir := sigL ? 1 : -1
curTop := zT
curBot := zB
curEntryBar := bar_index
else
pDir := sigL ? 1 : -1
pCnt := 0
pStop := sStop
pTop := zT
pBot := zB
pGrade := gr
pSess := sessName
pU := uCtx
pSl := slCtx
pDs := dsCtx

// 3) Diagnóstico: con la operación abierta, ¿el imbalance del gatillo se invirtió? (cierre con cuerpo al otro lado)
if strategy.position_size != 0 and curDir != 0 and bar_index > curEntryBar and array.size(invBars) > 0
int lastI = array.size(invBars) - 1
if array.get(invBars, lastI) < 0
bool invC = (curDir == 1 and close < curBot and close < open) or (curDir == -1 and close > curTop and close > open)
if invC
array.set(invBars, lastI, bar_index - curEntryBar)

if strategy.position_size != 0 and not na(closeAt) and time >= closeAt
strategy.close_all(comment = "Fin de sesión")

plot(vw, "VWAP", color = color.new(color.blue, 40), linewidth = 1)

// ------------------------- RESULTADO POR SESIÓN -------------------------
// Se calcula sobre las operaciones cerradas. El neto puede diferir un poco del Strategy Tester (comisiones).
var table stT = table.new(position.top_right, 6, 6, bgcolor = color.new(color.black, 20), border_width = 1, border_color = color.gray)
if barstate.islast
float[] nTr = array.new_float(3, 0.0)
float[] nWin = array.new_float(3, 0.0)
float[] gP = array.new_float(3, 0.0)
float[] gL = array.new_float(3, 0.0)
int nClosed = strategy.closedtrades
if nClosed > 0
for i = 0 to nClosed - 1
string cm = strategy.closedtrades.entry_comment(i)
int k = str.contains(cm, "Asia") ? 0 : (str.contains(cm, "Londres") ? 1 : 2)
float pf = strategy.closedtrades.profit(i)
array.set(nTr, k, array.get(nTr, k) + 1)
if pf > 0
array.set(nWin, k, array.get(nWin, k) + 1)
array.set(gP, k, array.get(gP, k) + pf)
else
array.set(gL, k, array.get(gL, k) - pf)
string[] hd = array.from("Sesión", "Ops", "Ganadas", "% acierto", "Neto USD", "PF")
for c = 0 to 5
table.cell(stT, c, 0, array.get(hd, c), text_color = color.white, text_size = size.small)
string[] nm = array.from("Asia", "Londres", "NY")
float tN = 0.0
float tW = 0.0
float tP = 0.0
float tL = 0.0
for k = 0 to 2
float n = array.get(nTr, k)
float w = array.get(nWin, k)
float p = array.get(gP, k)
float l = array.get(gL, k)
tN += n
tW += w
tP += p
tL += l
table.cell(stT, 0, k + 1, array.get(nm, k), text_color = color.white, text_size = size.small)
table.cell(stT, 1, k + 1, str.tostring(n, "#"), text_color = color.white, text_size = size.small)
table.cell(stT, 2, k + 1, str.tostring(w, "#"), text_color = color.white, text_size = size.small)
table.cell(stT, 3, k + 1, n > 0 ? str.tostring(w / n * 100, "#.#") + "%" : "-", text_color = color.white, text_size = size.small)
table.cell(stT, 4, k + 1, str.tostring(p - l, "#,###.##"), text_color = (p - l) >= 0 ? color.lime : color.red, text_size = size.small)
table.cell(stT, 5, k + 1, l > 0 ? str.tostring(p / l, "#.##") : "-", text_color = color.white, text_size = size.small)
table.cell(stT, 0, 4, "TOTAL", text_color = color.yellow, text_size = size.small)
table.cell(stT, 1, 4, str.tostring(tN, "#"), text_color = color.yellow, text_size = size.small)
table.cell(stT, 2, 4, str.tostring(tW, "#"), text_color = color.yellow, text_size = size.small)
table.cell(stT, 3, 4, tN > 0 ? str.tostring(tW / tN * 100, "#.#") + "%" : "-", text_color = color.yellow, text_size = size.small)
table.cell(stT, 4, 4, str.tostring(tP - tL, "#,###.##"), text_color = (tP - tL) >= 0 ? color.lime : color.red, text_size = size.small)
table.cell(stT, 5, 4, tL > 0 ? str.tostring(tP / tL, "#.##") : "-", text_color = color.yellow, text_size = size.small)
table.cell(stT, 0, 5, "Bias: " + biasMode + (onlyAplus ? " · solo A+" : " · A+ y A"), text_color = color.silver, text_size = size.small)

// ------------------------- DIAGNÓSTICO: ¿SE INVIRTIÓ EL IMBALANCE M5? -------------------------
var table dgT = table.new(position.bottom_right, 5, 3, bgcolor = color.new(color.black, 20), border_width = 1, border_color = color.gray)
if barstate.islast
float[] dn = array.new_float(2, 0.0)
float[] dw = array.new_float(2, 0.0)
float[] dl = array.new_float(2, 0.0)
float[] db = array.new_float(2, 0.0)
int nC = strategy.closedtrades
if nC > 0 and array.size(invBars) > 0
for i = 0 to nC - 1
if i < array.size(invBars)
float v = array.get(invBars, i)
int g = v >= 0 ? 0 : 1
array.set(dn, g, array.get(dn, g) + 1)
if strategy.closedtrades.profit(i) > 0
array.set(dw, g, array.get(dw, g) + 1)
else
array.set(dl, g, array.get(dl, g) + 1)
if v >= 0
array.set(db, 0, array.get(db, 0) + v)
string[] h2 = array.from("Imbalance M5", "Ops", "Ganadas", "Perdidas", "Velas M5 hasta invertir")
for c2 = 0 to 4
table.cell(dgT, c2, 0, array.get(h2, c2), text_color = color.white, text_size = size.small)
table.cell(dgT, 0, 1, "Se invirtió", text_color = color.orange, text_size = size.small)
table.cell(dgT, 1, 1, str.tostring(array.get(dn, 0), "#"), text_color = color.white, text_size = size.small)
table.cell(dgT, 2, 1, str.tostring(array.get(dw, 0), "#"), text_color = color.lime, text_size = size.small)
table.cell(dgT, 3, 1, str.tostring(array.get(dl, 0), "#"), text_color = color.red, text_size = size.small)
table.cell(dgT, 4, 1, array.get(dn, 0) > 0 ? str.tostring(array.get(db, 0) / array.get(dn, 0), "#.#") : "-", text_color = color.white, text_size = size.small)
table.cell(dgT, 0, 2, "No se invirtió", text_color = color.aqua, text_size = size.small)
table.cell(dgT, 1, 2, str.tostring(array.get(dn, 1), "#"), text_color = color.white, text_size = size.small)
table.cell(dgT, 2, 2, str.tostring(array.get(dw, 1), "#"), text_color = color.lime, text_size = size.small)
table.cell(dgT, 3, 2, str.tostring(array.get(dl, 1), "#"), text_color = color.red, text_size = size.small)
table.cell(dgT, 4, 2, "-", text_color = color.white, text_size = size.small)

// ------------------------- ESTADÍSTICA DE STOP Y TP -------------------------
// MAE = máximo retroceso en contra durante la operación; MFE = máximo avance a favor.
// R = 1 es la distancia del stop. Los valores vienen del Strategy Tester (intravela), son aproximados.
// "Puntos" = unidades de precio del activo (en MNQ, 1 punto = 4 ticks).
var table exT = table.new(position.bottom_left, 3, 11, bgcolor = color.new(color.black, 20), border_width = 1, border_color = color.gray)
if barstate.islast
float[] wMaeR = array.new_float()
float[] wMaeP = array.new_float()
float[] wMfeR = array.new_float()
float[] wMfeP = array.new_float()
float[] wStP = array.new_float()
float[] wStA = array.new_float()
float[] lMaeR = array.new_float()
float[] lMaeP = array.new_float()
float[] lMfeR = array.new_float()
float[] lMfeP = array.new_float()
float[] lStP = array.new_float()
float[] lStA = array.new_float()
int nT = strategy.closedtrades
if nT > 0
for i = 0 to nT - 1
if i < array.size(sdArr)
float sd = array.get(sdArr, i)
float at = array.get(atArr, i)
float szPv = math.abs(strategy.closedtrades.size(i)) * syminfo.pointvalue
if sd > 0 and szPv > 0 and at > 0
float maeP = math.abs(strategy.closedtrades.max_drawdown(i)) / szPv
float mfeP = math.abs(strategy.closedtrades.max_runup(i)) / szPv
if strategy.closedtrades.profit(i) > 0
array.push(wMaeR, maeP / sd)
array.push(wMaeP, maeP)
array.push(wMfeR, mfeP / sd)
array.push(wMfeP, mfeP)
array.push(wStP, sd)
array.push(wStA, sd / at)
else
array.push(lMaeR, maeP / sd)
array.push(lMaeP, maeP)
array.push(lMfeR, mfeP / sd)
array.push(lMfeP, mfeP)
array.push(lStP, sd)
array.push(lStA, sd / at)
int nW = array.size(wStP)
int nL = array.size(lStP)
int w1 = 0
int l1 = 0
if nW > 0
for j = 0 to nW - 1
if array.get(wMfeR, j) >= 1.0
w1 += 1
if nL > 0
for j = 0 to nL - 1
if array.get(lMfeR, j) >= 1.0
l1 += 1
string[] lab = array.from("Medida", "Retroceso en contra (MAE), mediana en R", "MAE: P75 / P90 en R", "MAE, mediana en puntos", "Avance a favor (MFE), mediana en R", "MFE: % que llegó a 1R o más", "MFE, mediana en puntos", "Stop mediano en puntos", "Stop mediano en ATR", "Operaciones")
table.cell(exT, 0, 0, "Medida", text_color = color.white, text_size = size.small)
table.cell(exT, 1, 0, "Ganadoras", text_color = color.lime, text_size = size.small)
table.cell(exT, 2, 0, "Perdedoras", text_color = color.red, text_size = size.small)
for r = 1 to 9
table.cell(exT, 0, r, array.get(lab, r), text_color = color.white, text_size = size.small)
table.cell(exT, 0, 10, "Con pocas operaciones es solo una pista", text_color = color.silver, text_size = size.small)
table.cell(exT, 1, 1, nW > 0 ? str.tostring(array.median(wMaeR), "#.##") : "-", text_color = color.white, text_size = size.small)
table.cell(exT, 2, 1, nL > 0 ? str.tostring(array.median(lMaeR), "#.##") : "-", text_color = color.white, text_size = size.small)
table.cell(exT, 1, 2, nW > 0 ? str.tostring(array.percentile_nearest_rank(wMaeR, 75), "#.##") + " / " + str.tostring(array.percentile_nearest_rank(wMaeR, 90), "#.##") : "-", text_color = color.white, text_size = size.small)
table.cell(exT, 2, 2, nL > 0 ? str.tostring(array.percentile_nearest_rank(lMaeR, 75), "#.##") + " / " + str.tostring(array.percentile_nearest_rank(lMaeR, 90), "#.##") : "-", text_color = color.white, text_size = size.small)
table.cell(exT, 1, 3, nW > 0 ? str.tostring(array.median(wMaeP), "#.#") : "-", text_color = color.white, text_size = size.small)
table.cell(exT, 2, 3, nL > 0 ? str.tostring(array.median(lMaeP), "#.#") : "-", text_color = color.white, text_size = size.small)
table.cell(exT, 1, 4, nW > 0 ? str.tostring(array.median(wMfeR), "#.##") : "-", text_color = color.white, text_size = size.small)
table.cell(exT, 2, 4, nL > 0 ? str.tostring(array.median(lMfeR), "#.##") : "-", text_color = color.white, text_size = size.small)
table.cell(exT, 1, 5, nW > 0 ? str.tostring(w1 / nW * 100, "#") + "%" : "-", text_color = color.white, text_size = size.small)
table.cell(exT, 2, 5, nL > 0 ? str.tostring(l1 / nL * 100, "#") + "%" : "-", text_color = color.white, text_size = size.small)
table.cell(exT, 1, 6, nW > 0 ? str.tostring(array.median(wMfeP), "#.#") : "-", text_color = color.white, text_size = size.small)
table.cell(exT, 2, 6, nL > 0 ? str.tostring(array.median(lMfeP), "#.#") : "-", text_color = color.white, text_size = size.small)
table.cell(exT, 1, 7, nW > 0 ? str.tostring(array.median(wStP), "#.#") : "-", text_color = color.white, text_size = size.small)
table.cell(exT, 2, 7, nL > 0 ? str.tostring(array.median(lStP), "#.#") : "-", text_color = color.white, text_size = size.small)
table.cell(exT, 1, 8, nW > 0 ? str.tostring(array.median(wStA), "#.##") : "-", text_color = color.white, text_size = size.small)
table.cell(exT, 2, 8, nL > 0 ? str.tostring(array.median(lStA), "#.##") : "-", text_color = color.white, text_size = size.small)
table.cell(exT, 1, 9, str.tostring(nW), text_color = color.white, text_size = size.small)
table.cell(exT, 2, 9, str.tostring(nL), text_color = color.white, text_size = size.small)

// ------------------------- CONTEXTO: RESULTADOS POR GRUPO -------------------------
// Ubicación: favorable = compras en el tercio bajo del rango del día (ventas en el tercio alto).
var table cxT = table.new(position.top_left, 5, 12, bgcolor = color.new(color.black, 20), border_width = 1, border_color = color.gray)
if barstate.islast
float[] cn = array.new_float(10, 0.0)
float[] cw = array.new_float(10, 0.0)
float[] cp = array.new_float(10, 0.0)
int nC2 = strategy.closedtrades
if nC2 > 0
for i = 0 to nC2 - 1
if i < array.size(uArr)
float pr = strategy.closedtrades.profit(i)
float uu = array.get(uArr, i)
float ss = array.get(slArr, i)
float d2 = array.get(dsArr, i)
int b1 = uu < 0 ? 3 : (uu < 0.33 ? 0 : (uu < 0.67 ? 1 : 2))
int b2 = ss < 0 ? -1 : (ss < 0.75 ? 4 : (ss < 1.5 ? 5 : 6))
int b3 = d2 < 0 ? -1 : (d2 < 1.0 ? 7 : (d2 < 2.0 ? 8 : 9))
for b in array.from(b1, b2, b3)
if b >= 0
array.set(cn, b, array.get(cn, b) + 1)
if pr > 0
array.set(cw, b, array.get(cw, b) + 1)
array.set(cp, b, array.get(cp, b) + pr)
string[] cl = array.from("Ubicación favorable (<33%)", "Ubicación media", "Ubicación desfavorable (>67%)", "Sin rango claro del día", "VWAP plano (<0.75 ATR)", "VWAP moderado", "VWAP inclinado (>1.5 ATR)", "Cerca del VWAP (<1 ATR)", "A 1 a 2 ATR del VWAP", "Lejos del VWAP (>2 ATR)")
string[] ch = array.from("Grupo", "Ops", "Ganadas", "% acierto", "Neto USD")
for c3 = 0 to 4
table.cell(cxT, c3, 0, array.get(ch, c3), text_color = color.white, text_size = size.small)
for r3 = 0 to 9
float n3 = array.get(cn, r3)
float w3 = array.get(cw, r3)
float p3 = array.get(cp, r3)
table.cell(cxT, 0, r3 + 1, array.get(cl, r3), text_color = color.white, text_size = size.small)
table.cell(cxT, 1, r3 + 1, str.tostring(n3, "#"), text_color = color.white, text_size = size.small)
table.cell(cxT, 2, r3 + 1, str.tostring(w3, "#"), text_color = color.white, text_size = size.small)
table.cell(cxT, 3, r3 + 1, n3 > 0 ? str.tostring(w3 / n3 * 100, "#") + "%" : "-", text_color = color.white, text_size = size.small)
table.cell(cxT, 4, r3 + 1, str.tostring(p3, "#,###"), text_color = p3 >= 0 ? color.lime : color.red, text_size = size.small)
table.cell(cxT, 0, 11, "Cada operación cuenta en 3 grupos (uno por bloque)", text_color = color.silver, text_size = size.small)

19 Oct 2026, 21:39

Hola Jorge,

¡Gracias por compartir tu estrategia! La probamos en Foresight y funciona: compila sin errores y opera tal como describes, con las sesiones de Asia, Londres y Nueva York en hora de Nueva York, entradas A+ y salidas "Fin de sesión".

Dos cosas que te pueden ser útiles:

1. Con la configuración por defecto no opera. El bias por defecto es "Manual por día (lista)" y la lista viene vacía, así que no hay ninguna operación hasta que escribes tu bias diario. Con "Sin bias (ambas direcciones)" sí opera: en oro (XAUUSD, 5 minutos, junio a septiembre de 2026) hizo 117 operaciones. No teníamos datos de MNQ a mano, por eso lo probamos en oro.

2. El filtro premium / descuento mira al futuro. Foresight lo detectó al compilar: las dos llamadas request.security(..., rangePos(), lookahead = barmerge.lookahead_on) leen la vela de 15 minutos y de 1 hora antes de que termine. En el backtest el filtro ve precios que en tiempo real todavía no existen. La solución es leer la vela anterior ya cerrada:

request.security(syminfo.tickerid, "15", rangePos()[1], lookahead = barmerge.lookahead_on)
request.security(syminfo.tickerid, "60", rangePos()[1], lookahead = barmerge.lookahead_on)

La diferencia es grande. En el mismo oro, con el filtro en "M15 y H1":

Filtro Operaciones Neto Acierto
Como está (mira al futuro) 31 −335.66 48.4%
Corregido ([1]) 36 −2,010.82 36.1%

Es decir, el filtro parecía unos 1,675 USD mejor de lo que realmente es. Como viene apagado por defecto, tus resultados sin el filtro no se ven afectados.

Si quieres, cuéntanos qué te gustaría revisar o mejorar de la estrategia y te ayudamos.

Saludos,
Matthew


English: Thanks for sharing your strategy! We ran it in Foresight and it works: it compiles cleanly and trades as you describe (Asia, London and New York sessions in New York time, A+ entries, "Fin de sesión" exits). Two notes. (1) With the defaults it makes no trades: the bias mode is "Manual por día (lista)" and the list starts empty; with "Sin bias (ambas direcciones)" it made 117 trades on gold (XAUUSD 5-minute, June–September 2026; we had no MNQ data to hand). (2) The premium / discount filter looks ahead: Foresight flagged that the two request.security(..., lookahead = barmerge.lookahead_on) calls read the 15-minute and 1-hour bar before it has finished. Read the previous, finished bar with rangePos()[1]. On the same gold data with the filter on "M15 y H1" it went from 31 trades, −335.66, 48.4% winners as written to 36 trades, −2,010.82, 36.1% when fixed: the filter looked about $1,675 better than it really is. It's off by default, so results without the filter aren't affected. Happy to help with anything else you'd like to check.