// ═══════════════════════════════════════════════════════════════════════════════
//  SIGNAL DESK — strategy twin  ·  Pine Script v6  ·  v1.2.0
//  The SAME engine as signal_desk.pine (indicator) — eight factors, the learning strength model, the
//  same triggers / vetoes / gates / trade management — wired to strategy.* so the Strategy Tester can
//  measure it HONESTLY: ₹20 per order + 1 tick slippage, one lot, fills at the signal bar's close.
//  Use it to answer the only question that matters — "is the winning percentage better?" — on YOUR
//  chart, instead of taking anyone's word for it. Whatever it says is in-sample until you test it on
//  data the settings never saw.
//
//  Costs are ON from bar one. Turn them off and the results will flatter you.
//  © 2026 ProEA Lab · testable draft · not a validated edge
// ═══════════════════════════════════════════════════════════════════════════════
//@version=6
// initial_capital is set high and margin is 0 on purpose: on the INDEX chart one lot (75) is a
// ~₹18 lakh notional, and TradingView silently skips orders that exceed equity × margin. This test
// is about the signals, not position sizing — size your real option trade by premium × lot.
strategy("Signal Desk — strategy twin", "SigDesk ST", overlay = true, initial_capital = 5000000, currency = currency.NONE,
     default_qty_type = strategy.fixed, default_qty_value = 75, pyramiding = 0, margin_long = 0, margin_short = 0,
     commission_type = strategy.commission.cash_per_order, commission_value = 20, slippage = 1,
     process_orders_on_close = true, calc_on_every_tick = false, max_labels_count = 200)

// ─────────────────────────────────────────────────────────────────── INPUTS (mirror of the indicator)
gT = "Trade"
inDir      = input.string("Both", "Direction", options = ["Both", "Long only", "Short only"], group = gT, display = display.none)
inMode     = input.string("Intraday", "Mode", options = ["Intraday", "Positional"], group = gT, display = display.none)
inTrigger  = input.string("Pullback", "Entry trigger", options = ["Pullback", "Breakout", "Both"], group = gT, display = display.none)
inMinGrade = input.string("B", "Trade only when grade ≥", options = ["A", "B", "C"], group = gT, display = display.none)
inGateMode = input.string("Setup count", "Grade from", options = ["Setup count", "Learned strength"], group = gT, display = display.none)
inGradeA   = input.int(80, "A = strength ≥ (learned mode)", minval = 50, maxval = 95, group = gT, display = display.none)
inGradeB   = input.int(65, "B = strength ≥ (learned mode)", minval = 40, maxval = 90, group = gT, display = display.none)
inMaxDay   = input.int(2, "Max entries per day", minval = 1, maxval = 20, group = gT, display = display.none)
inCooldown = input.int(6, "Cooldown after an exit (bars)", minval = 0, maxval = 200, group = gT, display = display.none)
gL = "Strength model (learns on this chart)"
inLearn = input.bool(true, "Learn factor weights on this chart", group = gL, display = display.none)
inLr    = input.float(0.02, "Learning rate", minval = 0.0, maxval = 0.2, step = 0.005, group = gL, display = display.none)
inShadowBars = input.int(30, "Shadow bracket timeout (bars)", minval = 5, maxval = 300, group = gL, display = display.none)
gSn = "Session (exchange time)"
inTZ       = input.string("Asia/Kolkata", "Exchange timezone", group = gSn, display = display.none)
inEntryWin = input.session("0930-1445", "Entry window", group = gSn, display = display.none)
inSqOff    = input.session("1500-1515", "Square-off window (Intraday)", group = gSn, display = display.none)
gR = "Risk levels"
inSlMode    = input.string("Structure", "Stop-loss", options = ["Structure", "ATR"], group = gR, display = display.none)
inSlAtr     = input.float(1.5, "ATR multiple (ATR stop · trail · far-swing cap)", minval = 0.3, maxval = 6, step = 0.1, group = gR, display = display.none)
inTp1       = input.float(1.0, "TP1 (R)", minval = 0.2, maxval = 10, step = 0.1, group = gR, display = display.none)
inTp2       = input.float(2.0, "TP2 (R)", minval = 0.3, maxval = 20, step = 0.1, group = gR, display = display.none)
inTp3       = input.float(3.0, "TP3 (R) — final target", minval = 0.5, maxval = 30, step = 0.1, group = gR, display = display.none)
inBE        = input.bool(true, "TP1 → stop to breakeven", group = gR, display = display.none)
inTrail     = input.bool(true, "TP2 → stop to TP1, then ATR trail", group = gR, display = display.none)
inExitTrend = input.bool(true, "Exit on trend flip (Supertrend)", group = gR, display = display.none)
inMomExit   = input.string("RSI vs average", "Momentum-loss exit", options = ["RSI vs average", "RSI through 50", "Off"], group = gR, display = display.none)
inTimeStop  = input.int(0, "Time stop (bars, 0 = off)", minval = 0, maxval = 500, group = gR, display = display.none)
inExitOpp   = input.bool(true, "Exit (and reverse) on an opposite signal", group = gR, display = display.none)
gF = "Filters (fewer, stronger)"
inBigBar    = input.float(2.5, "Veto: trigger bar range > ATR ×", minval = 1, maxval = 8, step = 0.1, group = gF, display = display.none)
inStretch   = input.float(2.0, "Veto: distance from fast EMA > ATR ×", minval = 0.5, maxval = 8, step = 0.1, group = gF, display = display.none)
inRunAtr    = input.float(3.0, "Veto: 3-bar run > ATR ×", minval = 1, maxval = 12, step = 0.5, group = gF, display = display.none)
inTouch     = input.float(0.3, "Pullback: touch tolerance (ATR ×)", minval = 0, maxval = 2, step = 0.1, group = gF, display = display.none)
inTouchBars = input.int(3, "Pullback: touch within N bars", minval = 1, maxval = 20, group = gF, display = display.none)
gE = "Engine"
inEmaF   = input.int(20, "Fast EMA", minval = 2, group = gE, display = display.none)
inEmaS   = input.int(50, "Slow EMA", minval = 3, group = gE, display = display.none)
inRsiLen = input.int(14, "RSI length", minval = 2, group = gE, display = display.none)
inRsiMa  = input.int(14, "RSI average length", minval = 2, group = gE, display = display.none)
inStAtr  = input.int(10, "Supertrend ATR length", minval = 1, group = gE, display = display.none)
inStFac  = input.float(3.0, "Supertrend factor", minval = 0.5, maxval = 10, step = 0.1, group = gE, display = display.none)
inPiv    = input.int(5, "Swing pivot length", minval = 2, maxval = 30, group = gE, display = display.none)
inAtrLen = input.int(14, "ATR length", minval = 1, group = gE, display = display.none)
inHtf    = input.timeframe("15", "Higher timeframe (context factor)", group = gE, display = display.none)
gV = "Style"
inShowLvls = input.bool(true, "Show EMAs + active stop / target", group = gV, display = display.none)

// ═══════════════════════════════════════════════════════════════════ ENGINE (identical to the indicator)
float atr   = ta.atr(inAtrLen)
float atrU  = math.max(atr, syminfo.mintick)
float emaF  = ta.ema(close, inEmaF)
float emaS  = ta.ema(close, inEmaS)
float rsi   = ta.rsi(close, inRsiLen)
float rsiMa = ta.sma(rsi, inRsiMa)
bool  rsiXup = ta.crossover(rsi, rsiMa)
bool  rsiXdn = ta.crossunder(rsi, rsiMa)
bool  rsi50Dn = ta.crossunder(rsi, 50)
bool  rsi50Up = ta.crossover(rsi, 50)
[stLine, stDir] = ta.supertrend(inStFac, inStAtr)
bool  stUp     = stDir < 0
bool  stFlipDn = stDir > 0 and stDir[1] < 0
bool  stFlipUp = stDir < 0 and stDir[1] > 0
float ph = ta.pivothigh(high, inPiv, inPiv)
float pl = ta.pivotlow(low, inPiv, inPiv)
int   sinceTouchF = ta.barssince(low <= emaF + inTouch * atrU)
int   sinceTouchS = ta.barssince(high >= emaF - inTouch * atrU)
float run3 = ta.highest(close, 3) - ta.lowest(close, 3)
float lo5  = ta.lowest(low, 5)
float hi5  = ta.highest(high, 5)
bool  winOK  = not na(time(timeframe.period, inEntryWin, inTZ))
bool  sqOff  = not na(time(timeframe.period, inSqOff, inTZ))
int   dayNum = dayofmonth(time, inTZ)
bool  newDay = ta.change(dayNum) != 0

float vwapPx = ta.vwap(hlc3)
bool  vwapOK = not na(vwapPx) and nz(volume, 0) > 0
htfBiasFn() =>
    ta.ema(close, 20) > ta.ema(close, 50) ? 1 : -1
bool  htfValid   = timeframe.in_seconds(inHtf) > timeframe.in_seconds()
// confirmed-HTF pattern: the [1] offset + lookahead_on together = the last CLOSED higher-timeframe bar (never the forming one)
int   htfBiasRaw = request.security(syminfo.tickerid, inHtf, htfBiasFn()[1], lookahead = barmerge.lookahead_on)
int   htfBias    = htfValid ? nz(htfBiasRaw, 0) : 0
var float dayHi   = na
var float dayLo   = na
var int   dayBars = 0
if newDay or na(dayHi)
    dayHi   := high
    dayLo   := low
    dayBars := 1
else
    dayHi   := math.max(dayHi, high)
    dayLo   := math.min(dayLo, low)
    dayBars += 1

var float swHigh    = na
var float swLow     = na
var float pivHigh   = na
var float pivLow    = na
var int   structDir = 0
bool bosUp   = false
bool bosDn   = false
bool chochUp = false
bool chochDn = false
if not na(ph)
    swHigh  := ph
    pivHigh := ph
if not na(pl)
    swLow  := pl
    pivLow := pl
if barstate.isconfirmed
    if not na(swHigh) and close > swHigh
        if structDir == -1
            chochUp := true
        else
            bosUp := true
        structDir := 1
        swHigh := na
    if not na(swLow) and close < swLow
        if structDir == 1
            chochDn := true
        else
            bosDn := true
        structDir := -1
        swLow := na

int   fTrend  = stUp and emaF > emaS ? 1 : (not stUp and emaF < emaS) ? -1 : 0
int   fMom    = rsi > rsiMa and rsi > 50 ? 1 : rsi < rsiMa and rsi < 50 ? -1 : 0
int   fStruct = structDir
float distF   = (close - emaF) / atrU
bool  locL    = distF >= -0.5 and distF <= 1.0
bool  locS    = distF <= 0.5 and distF >= -1.0
float rng     = high - low
float body    = math.abs(close - open)
bool  bigBar  = rng > inBigBar * atrU
bool  bullBar = close > open and rng > 0 and (high - close) <= 0.35 * rng and body >= 0.4 * rng
bool  bearBar = close < open and rng > 0 and (close - low) <= 0.35 * rng and body >= 0.4 * rng
bool  runUp   = run3 > inRunAtr * atrU and close > close[3]
bool  runDn   = run3 > inRunAtr * atrU and close < close[3]
bool  vetoL   = bigBar or distF > inStretch or runUp
bool  vetoS   = bigBar or distF < -inStretch or runDn
bool  candleL = bullBar and not bigBar
bool  candleS = bearBar and not bigBar
float toHi    = dayHi - close
float toLo    = close - dayLo
bool  roomL   = dayBars < 3 or not (toHi > 0.1 * atrU and toHi < 1.0 * atrU)
bool  roomS   = dayBars < 3 or not (toLo > 0.1 * atrU and toLo < 1.0 * atrU)
featL() =>
    array<float> f = array.new<float>(8, 0.0)
    array.set(f, 0, fTrend == 1 ? 1.0 : 0.0)
    array.set(f, 1, fMom == 1 ? 1.0 : 0.0)
    array.set(f, 2, fStruct == 1 ? 1.0 : 0.0)
    array.set(f, 3, locL ? 1.0 : 0.0)
    array.set(f, 4, candleL ? 1.0 : 0.0)
    array.set(f, 5, vwapOK ? (close > vwapPx ? 1.0 : 0.0) : 0.5)
    array.set(f, 6, htfValid ? (htfBias == 1 ? 1.0 : 0.0) : 0.5)
    array.set(f, 7, roomL ? 1.0 : 0.0)
    f
featS() =>
    array<float> f = array.new<float>(8, 0.0)
    array.set(f, 0, fTrend == -1 ? 1.0 : 0.0)
    array.set(f, 1, fMom == -1 ? 1.0 : 0.0)
    array.set(f, 2, fStruct == -1 ? 1.0 : 0.0)
    array.set(f, 3, locS ? 1.0 : 0.0)
    array.set(f, 4, candleS ? 1.0 : 0.0)
    array.set(f, 5, vwapOK ? (close < vwapPx ? 1.0 : 0.0) : 0.5)
    array.set(f, 6, htfValid ? (htfBias == -1 ? 1.0 : 0.0) : 0.5)
    array.set(f, 7, roomS ? 1.0 : 0.0)
    f

type Model
    array<float> w
    array<float> mu
    float w0     = 0.0
    int   trained = 0
var Model mdl = Model.new(w = array.new<float>(8, 0.75), mu = array.new<float>(8, 0.5))
squash(float z) => 1.0 / (1.0 + math.exp(-z))
mdlLogit(Model m, array<float> f) =>
    float z = 0.0
    for i = 0 to 7
        z += array.get(m.w, i) * (array.get(f, i) - array.get(m.mu, i))
    z
mdlTrain(Model m, array<float> f, float y) =>
    float p   = squash(m.w0 + mdlLogit(m, f))
    float err = p - y
    m.w0 := m.w0 - inLr * err
    for i = 0 to 7
        float xc = array.get(f, i) - array.get(m.mu, i)
        float wi = array.get(m.w, i) - inLr * err * xc
        array.set(m.w, i, math.max(-1.0, math.min(2.5, wi)))
        array.set(m.mu, i, array.get(m.mu, i) + 0.02 * (array.get(f, i) - array.get(m.mu, i)))
    m.trained := m.trained + 1
mdlStrength(Model m, array<float> f) => squash(mdlLogit(m, f))
array<float> fxL = featL()
array<float> fxS = featS()
float strengthL = 100.0 * mdlStrength(mdl, fxL)
float strengthS = 100.0 * mdlStrength(mdl, fxS)
int   setupL  = (fTrend == 1 ? 1 : 0) + (fMom == 1 ? 1 : 0) + (fStruct == 1 ? 1 : 0) + (locL ? 1 : 0) + (candleL ? 1 : 0)
int   setupS  = (fTrend == -1 ? 1 : 0) + (fMom == -1 ? 1 : 0) + (fStruct == -1 ? 1 : 0) + (locS ? 1 : 0) + (candleS ? 1 : 0)
gradeOf(float s)    => s >= inGradeA ? "A" : s >= inGradeB ? "B" : s >= 50 ? "C" : ""
gradeCount(int s)   => s >= 5 ? "A" : s == 4 ? "B" : s == 3 ? "C" : ""
gradeRank(string g) => g == "A" ? 3 : g == "B" ? 2 : g == "C" ? 1 : 0
string gradeL = inGateMode == "Learned strength" ? gradeOf(strengthL) : gradeCount(setupL)
string gradeS = inGateMode == "Learned strength" ? gradeOf(strengthS) : gradeCount(setupS)

bool momUpTurn = rsiXup or (rsi > rsiMa and rsi > rsi[1])
bool momDnTurn = rsiXdn or (rsi < rsiMa and rsi < rsi[1])
bool pullL = fTrend == 1  and sinceTouchF <= inTouchBars and bullBar and close > emaF and momUpTurn
bool pullS = fTrend == -1 and sinceTouchS <= inTouchBars and bearBar and close < emaF and momDnTurn
bool brkL  = fTrend == 1  and (bosUp or chochUp)
bool brkS  = fTrend == -1 and (bosDn or chochDn)
bool trigL = inTrigger == "Pullback" ? pullL : inTrigger == "Breakout" ? brkL : (pullL or brkL)
bool trigS = inTrigger == "Pullback" ? pullS : inTrigger == "Breakout" ? brkS : (pullS or brkS)
bool dirL  = inDir != "Short only"
bool dirS  = inDir != "Long only"

// ═══════════════════════════════════════════════════════════════════ ORDERS
var int    today    = 0
var int    lastExit = -100000
var float  tEntry = na
var float  tSl  = na
var float  tR   = na
var float  tTp1 = na
var float  tTp2 = na
var float  tTp3 = na
var int    tDir = 0
var bool   tp1Hit = false
var bool   tp2Hit = false
var string tGrade = ""
var array<float> tFx = array.new<float>(8, 0.5)
if newDay
    today := 0

int posDir = strategy.position_size > 0 ? 1 : strategy.position_size < 0 ? -1 : 0
bool closedNow = strategy.closedtrades > strategy.closedtrades[1]
if closedNow
    lastExit := bar_index

// shadow learning — identical to the indicator: every trigger is followed as a 1R bracket, stop first, later bars only
type Shadow
    bool  on  = false
    int   dir = 0
    float sl  = na
    float tp  = na
    int   bar = na
    array<float> fx
var Shadow sh = Shadow.new(fx = array.new<float>(8, 0.5))
stopFor(int d) =>
    float s = d == 1 ? math.min(nz(pivLow, lo5), lo5) - 0.25 * atrU : math.max(nz(pivHigh, hi5), hi5) + 0.25 * atrU
    float a = d == 1 ? close - inSlAtr * atrU : close + inSlAtr * atrU
    float p = inSlMode == "ATR" ? a : (math.abs(close - s) > inSlAtr * 1.5 * atrU ? a : s)
    d == 1 ? math.min(p, close - syminfo.mintick) : math.max(p, close + syminfo.mintick)
if barstate.isconfirmed and sh.on and bar_index > sh.bar
    bool shStop = sh.dir == 1 ? low <= sh.sl : high >= sh.sl
    bool shTgt  = sh.dir == 1 ? high >= sh.tp : low <= sh.tp
    bool shOut  = bar_index - sh.bar >= inShadowBars
    if shStop or shTgt or shOut
        if inLearn
            mdlTrain(mdl, sh.fx, shStop ? 0.0 : shTgt ? 1.0 : 0.5)
        sh.on := false
if barstate.isconfirmed and not sh.on and not na(atr) and (trigL or trigS)
    int   sd  = trigL ? 1 : -1
    float ssl = stopFor(sd)
    float sr  = math.abs(close - ssl)
    sh.on  := true
    sh.dir := sd
    sh.sl  := ssl
    sh.tp  := sd == 1 ? close + inTp1 * sr : close - inTp1 * sr
    sh.bar := bar_index
    sh.fx  := array.copy(sd == 1 ? fxL : fxS)

bool gateCommon = barstate.isconfirmed and not na(atr) and winOK and today < inMaxDay and (bar_index - lastExit) >= inCooldown
bool candL = trigL and not vetoL and gradeRank(gradeL) >= gradeRank(inMinGrade) and dirL and gateCommon
bool candS = trigS and not vetoS and gradeRank(gradeS) >= gradeRank(inMinGrade) and dirS and gateCommon

// manage the open position: milestones move the stop, discretionary exits close at the bar's close
if barstate.isconfirmed and posDir != 0
    float entry = strategy.position_avg_price
    bool h1 = posDir == 1 ? high >= tTp1 : low <= tTp1
    bool h2 = posDir == 1 ? high >= tTp2 : low <= tTp2
    if h1 and not tp1Hit
        tp1Hit := true
        if inBE
            tSl := entry
    if h2 and not tp2Hit
        tp2Hit := true
        if inTrail
            tSl := tTp1
    if inTrail and tp2Hit
        float trailPx = posDir == 1 ? close - inSlAtr * atrU : close + inSlAtr * atrU
        tSl := posDir == 1 ? math.max(tSl, trailPx) : math.min(tSl, trailPx)
    int  barsIn  = bar_index - strategy.opentrades.entry_bar_index(strategy.opentrades - 1)
    bool flip    = inExitTrend and (posDir == 1 ? stFlipDn : stFlipUp)
    bool momLoss = barsIn >= 2 and (inMomExit == "RSI vs average" ? (posDir == 1 ? rsiXdn : rsiXup) : inMomExit == "RSI through 50" ? (posDir == 1 ? rsi50Dn : rsi50Up) : false)
    bool opp     = inExitOpp and (posDir == 1 ? candS : candL)
    bool sq      = inMode == "Intraday" and sqOff
    bool timeUp  = inTimeStop > 0 and not tp1Hit and barsIn >= inTimeStop
    if flip or momLoss or opp or sq or timeUp
        strategy.close_all(comment = sq ? "SQ-OFF" : flip ? "TREND" : opp ? "OPP" : momLoss ? "MOM" : "TIME")
    else
        strategy.exit(posDir == 1 ? "XL" : "XS", posDir == 1 ? "L" : "S", stop = tSl, limit = tTp3, comment_loss = "SL", comment_profit = "TP3", comment_trailing = "TRAIL")

// entries (flat, or reversing on an opposite signal)
bool canOpen = posDir == 0 or (inExitOpp and (posDir == 1 ? candS : candL))
if candL and canOpen and (posDir != 1)
    float slStruct = math.min(nz(pivLow, lo5), lo5) - 0.25 * atrU
    float slAtr    = close - inSlAtr * atrU
    float slPx     = inSlMode == "ATR" ? slAtr : ((close - slStruct) > inSlAtr * 1.5 * atrU ? slAtr : slStruct)
    slPx := math.min(slPx, close - syminfo.mintick)
    tEntry := close
    tDir := 1
    tR   := close - slPx
    tSl  := slPx
    tTp1 := close + inTp1 * tR
    tTp2 := close + inTp2 * tR
    tTp3 := close + inTp3 * tR
    tp1Hit := false
    tp2Hit := false
    tGrade := gradeL
    tFx := array.copy(fxL)
    today := today + 1
    strategy.entry("L", strategy.long, comment = "L·" + gradeL + " " + str.tostring(strengthL, "0"))
    strategy.exit("XL", "L", stop = tSl, limit = tTp3, comment_loss = "SL", comment_profit = "TP3")
else if candS and canOpen and (posDir != -1)
    float slStruct = math.max(nz(pivHigh, hi5), hi5) + 0.25 * atrU
    float slAtr    = close + inSlAtr * atrU
    float slPx     = inSlMode == "ATR" ? slAtr : ((slStruct - close) > inSlAtr * 1.5 * atrU ? slAtr : slStruct)
    slPx := math.max(slPx, close + syminfo.mintick)
    tEntry := close
    tDir := -1
    tR   := slPx - close
    tSl  := slPx
    tTp1 := close - inTp1 * tR
    tTp2 := close - inTp2 * tR
    tTp3 := close - inTp3 * tR
    tp1Hit := false
    tp2Hit := false
    tGrade := gradeS
    tFx := array.copy(fxS)
    today := today + 1
    strategy.entry("S", strategy.short, comment = "S·" + gradeS + " " + str.tostring(strengthS, "0"))
    strategy.exit("XS", "S", stop = tSl, limit = tTp3, comment_loss = "SL", comment_profit = "TP3")

// ═══════════════════════════════════════════════════════════════════ CHART (minimal — the tester is the report)
plot(emaF, "EMA fast", color = inShowLvls ? color.new(#7C9CFF, 15) : na)
plot(emaS, "EMA slow", color = inShowLvls ? color.new(#5B6472, 30) : na)
plot(inShowLvls and posDir != 0 ? tSl : na,  "Active stop",   color = #FF4D6D, style = plot.style_linebr, display = display.pane)
plot(inShowLvls and posDir != 0 ? tTp3 : na, "Active target", color = #2AF0C8, style = plot.style_linebr, display = display.pane)

// POS label on the last CONFIRMED bar (strategies do not run on the forming bar)
var label pos = na
if barstate.islastconfirmedhistory
    string txt = posDir == 1 ? "LONG · " + tGrade + " · stop " + str.tostring(tSl, format.mintick) : posDir == -1 ? "SHORT · " + tGrade + " · stop " + str.tostring(tSl, format.mintick) : "FLAT · " + str.tostring(today) + "/" + str.tostring(inMaxDay) + " today · learned " + str.tostring(mdl.trained)
    if na(pos)
        pos := label.new(bar_index, high, txt, style = label.style_label_left, color = color.new(#0E1118, 10), textcolor = #E6E9EF, size = size.small)
    label.set_xy(pos, bar_index, high)
    label.set_text(pos, txt)
