Signal Desk · Free & open-source
A signal needs more than a strong candle.
Count the checks. Apply the limits. Then see the entry and exit plan.
Draws on your price chart. The indicator does not place orders.

- Count agreement
Five setup factors give the grade.
- Skip the chase
Stretched triggers are refused by rule.
- Show the plan
Qualified entries and exits with reasons.
What is Signal Desk?
Signal Desk is a free TradingView indicator that evaluates a pullback or breakout trigger against an entry checklist. It grades five setup checks, applies chase and daily limits, and displays a reference entry and exit plan. The guide also explains its optional index-to-options labels and separate strategy script.
Why can a high-grade setup still have no entry?
The grade counts setup checks; it does not override the entry gates. The selected trigger, chase limits, session window, daily allowance, cooldown and existing trade state must also permit an entry on the confirmed candle. Read the entry vetoes
Is learned strength a win probability?
No. Learned strength is a relative score, not a measured win probability. With Grade from set to Setup count, it is informational. Selecting Learned strength uses that score to determine the grade used by entry filtering. Read the strength score limits
Do BUY CE and BUY PE labels use live option prices?
No. Options mapping formats a strike and expiry label from the index or futures chart. It does not read an option chain, premiums or Greeks. The displayed entry and risk levels remain prices of the charted instrument. Read the options label limits
Why can a good-looking candle be skipped?
Built for an options buyer who wanted fewer signals after oversized candles. Follow the checks below.
A big green candle catches your eye.
It closed higher than it opened. That alone does not tell you whether it meets the tool’s rules.
Fictional examples. The last step compares a different candle, not a candle shrinking over time. ATR, averages and momentum are supplied context.
How could this help a trade?
Test visible entry conditions and exit rules consistently. Profit still depends on price, trade size and costs. A passing grade cannot remove losses.
Where did the idea come from?
An options buyer wanted fewer signals, especially after oversized candles. We built a checklist, a limit on daily entries and clear exit rules. The default allows at most two entries a day; some days have none.
Same close. What changes the decision?
Switch cases at the same step.
Price returns toward the average.
The last two candles pull back. A touch alone is not an entry. Watch the next closed candle.
Same close: 110. Compare cases without losing your place. “Window closed” changes only the clock: 10:00 to 14:45 in Asia/Kolkata, using the candle’s opening time.
What is held fixed in this example?
ATR 8; fast/slow EMA 106/101; rising momentum, bullish Supertrend and confirmed structure, pivot low 99. One earlier trade, eight candles since its exit, no open position. The current bar touches the EMA area. These snapshots use the default setup-count rules; the short chart excerpt does not calculate that longer context. The full simulator below does more of the calculation.
Does grade A mean this trade will win?
No. In the default mode it means all five setup checks agree. It is a count, not a probability. The other entry limits still apply.
Find the checklist on a real chart.
Read label → plan → panel.

1 The entry label
The letter records setup agreement when the entry was marked.
2 The colored zones
Entry-to-stop is the planned risk distance. Targets show the plan in the other direction.
3 The dashboard
Setup and Grade explain agreement. Session and Today explain timing and daily capacity.
Explore the full price simulator
Change the close, wick or clock. This simulator recalculates averages, momentum and ATR using a longer fictional history; some earlier market state is supplied.
Can this candle pass the chase check?
Change the close or wicks. ATR, momentum and entry checks recalculate.
How the calculation works & more controls
Factors: trend yes, momentum yes, structure yes, location yes, candle yes. RSI 63.60, average 43.99; ATR 14 is 2.64.
Supplied Supertrend: up, bands 100/118; pivots 99/113. Current bands and strict flips recalculate. Strength/context gates off.
Entry 110.00; stop 106.04 uses the 1.5-ATR fallback because the swing is too far. Targets: 113.96 / 117.91 / 121.87.
Illustrative inputs · changing a slider recalculates this example · no market feed
Paste it into TradingView.
Start on an intraday chart. Both complete Pine v6 source files are free.
- 1
Open Pine Editor
Create a new indicator on your chart.
- 2
Paste the code
Copy below and replace the sample code.
- 3
Add to chart
Save it. Keep the defaults initially.
The indicator
Signals, zones, checklist, Co-Pilot and alerts.
Copy the code as it is. No coding needed.
View the full code
// ═══════════════════════════════════════════════════════════════════════════════
// SIGNAL DESK — Options Buyer · Pine Script v6 · v1.2.0
// Long / short entries and exits for option-premium and index charts — FEWER, GRADED, and LEARNING.
//
// WHY IT EXISTS A Nifty options buyer rents two signal tools that fire many times a day and hit the
// stop as often as the target — the worst ones right after a big candle. He asked for
// "one or two strong calls a day". This is that: every trigger is scored by eight factors
// (5 setup + 3 context) into a STRENGTH 0–100, the factor weights are LEARNED from how
// trades actually closed on THIS chart (online logistic regression, causal), a chase-veto
// skips entries on stretched candles, and a per-day cap + cooldown keep it quiet.
// RUNS ON any chart; built for NIFTY option contracts (5-minute) and the index / futures chart
// with Options mapping (long = BUY CE, short = BUY PE, ATM strike + expiry in the alert).
// STATUS testable draft — compiles clean · confirmed-bar signals (no repaint) · paper P&L has
// NO costs · nothing here is a validated edge (see docs/build-report.md). The strategy
// twin (signal_desk_strategy.pine) measures the win-% honestly with costs.
//
// © 2026 ProEA Lab · decision support, never a prediction · built for a Reddit reviewer's feedback
// ═══════════════════════════════════════════════════════════════════════════════
//@version=6
indicator("Signal Desk — Options Buyer", "SigDesk", overlay = true, max_labels_count = 500, max_lines_count = 100, max_boxes_count = 100)
// ─────────────────────────────────────────────────────────────────── INPUTS · trade
gT = "Trade"
inDir = input.string("Both", "Direction", options = ["Both", "Long only", "Short only"], group = gT,
tooltip = "On an OPTION premium chart use Long only (you buy the option whose chart signals a long). On the index / futures chart use Both — with Options mapping on, a long = BUY CE and a short = BUY PE.", display = display.none)
inMode = input.string("Intraday", "Mode", options = ["Intraday", "Positional"], group = gT,
tooltip = "Intraday = any open position exits in the square-off window. Positional = no forced square-off (entries still respect the entry window).", display = display.none)
inTrigger = input.string("Pullback", "Entry trigger", options = ["Pullback", "Breakout", "Both"], group = gT,
tooltip = "Pullback = trend up, price came back to the fast EMA, then a strong close back above it with momentum turning up (calm: fewer, later). Breakout = a confirmed BOS / CHoCH in the trend direction (earlier, more). Both = either.", display = display.none)
inMinGrade = input.string("B", "Signal only when grade ≥", options = ["A", "B", "C"], group = gT,
tooltip = "Grade comes from STRENGTH (0–100), the learned model's read of the eight factors: 50 = a typical trigger on this chart, higher = a better-than-typical mix of what has paid here. A ≥ the A threshold · B ≥ the B threshold · C ≥ 50. Lower never fires; in the Pro view they show as faint ghost marks so you can see what was filtered.", display = display.none)
inGateMode = input.string("Setup count", "Grade from", options = ["Setup count", "Learned strength"], group = gT,
tooltip = "Setup count (default) = the five setup factors: A = 5/5 · B = 4/5 · C = 3/5 — the plain, measured-best rule; the context factors and the learned strength are shown and sent in alerts as information. Learned strength = the grade comes from the learning model's STRENGTH instead (A / B thresholds below). Measured on NIFTY 5m, May–Sep 2026: setup count → 93 trades, PF 1.28; learned strength → 51 trades, negative expectancy. Test both on YOUR charts before trusting either.", 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,
tooltip = "The 'one or two calls a day' rule — counted per exchange day, both directions together.", display = display.none)
inCooldown = input.int(6, "Cooldown after an exit (bars)", minval = 0, maxval = 200, group = gT, display = display.none)
inQty = input.int(75, "Quantity (lot size)", minval = 1, group = gT,
tooltip = "Used only for the paper P&L on the dashboard and the alert payload. NIFTY lot = 75.", display = display.none)
gL = "Strength model (learns on this chart)"
inLearn = input.bool(true, "Learn factor weights on this chart", group = gL,
tooltip = "EVERY trigger — taken or filtered — is followed as a shadow 1R bracket (stop-first, never peeks). When it resolves, one online logistic-regression step nudges the eight factor weights toward what actually paid on THIS chart. Learning from all triggers, not just the taken trades, is what lets the model see which factors matter. Off = fixed equal weights (a plain factor count).", display = display.none)
inLr = input.float(0.02, "Learning rate", minval = 0.0, maxval = 0.2, step = 0.005, group = gL,
tooltip = "How hard each resolved trigger pulls the weights. 0.02 = a weight needs ~50 resolutions to move by one full step. Higher learns faster and forgets faster.", display = display.none)
inShadowBars = input.int(30, "Shadow bracket timeout (bars)", minval = 5, maxval = 300, group = gL,
tooltip = "A shadow trigger that reaches neither its 1R target nor its stop within this many bars counts as a half-win (0.5).", display = display.none)
gO = "Options mapping (index / futures chart)"
inOptMap = input.bool(false, "Map long → BUY CE · short → BUY PE", group = gO,
tooltip = "Turn on when the indicator sits on the INDEX or futures chart. Labels and alerts then say BUY CE / BUY PE with the strike below.", display = display.none)
inStrikeStep = input.int(50, "Strike step", minval = 1, group = gO, tooltip = "NIFTY = 50 · BANKNIFTY = 100.", display = display.none)
inStrikeOff = input.int(0, "Strikes OTM (+) / ITM (−)", minval = -10, maxval = 10, group = gO,
tooltip = "0 = at the money. +1 = one strike out of the money, −1 = one strike in the money.", display = display.none)
inExpiry = input.string("", "Expiry label for the alert (e.g. 09SEP26)", group = gO,
tooltip = "Free text added to the alert payload as the instrument, e.g. NIFTY 09SEP26 24450 CE — so your webhook knows exactly what to buy. Leave empty to omit.", display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · session
gSn = "Session (exchange time)"
inTZ = input.string("Asia/Kolkata", "Exchange timezone", group = gSn,
tooltip = "IANA name. NSE = Asia/Kolkata · NYSE = America/New_York · Bangkok = Asia/Bangkok.", display = display.none)
inEntryWin = input.session("0930-1445", "Entry window", group = gSn,
tooltip = "New entries only inside this window. The default skips the first 15 minutes — the opening spike is where most chase entries come from.", display = display.none)
inSqOff = input.session("1500-1515", "Square-off window (Intraday)", group = gSn,
tooltip = "Intraday mode: an open position exits on the first closed bar inside this window.", display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · risk levels
gR = "Risk levels"
inSlMode = input.string("Structure", "Stop-loss", options = ["Structure", "ATR"], group = gR,
tooltip = "Structure = beyond the recent swing low / high plus a small buffer (falls back to the ATR stop when the swing is too far). ATR = entry ∓ ATR × multiple.", 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,
tooltip = "The explicit intraday exit. RSI vs average = a long closes when RSI crosses back under its own average (cuts losers early, takes small wins — theta-friendly). RSI through 50 = only a real momentum turn (fewer, later exits). Off = ride until stop / target / trend flip / square-off.", display = display.none)
inTimeStop = input.int(0, "Time stop (bars, 0 = off)", minval = 0, maxval = 500, group = gR,
tooltip = "Exit a trade that has not reached TP1 after N bars — dead trades cost theta. 0 = off.", display = display.none)
inExitOpp = input.bool(true, "Exit (and reverse) on an opposite signal", group = gR, display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · filters
gF = "Filters (fewer, stronger)"
inBigBar = input.float(2.5, "Veto: trigger bar range > ATR ×", minval = 1, maxval = 8, step = 0.1, group = gF,
tooltip = "The chase filter — a signal on a candle this big is skipped. That is the 'big candle closes, I enter, it reverses' trade.", 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)
// ─────────────────────────────────────────────────────────────────── INPUTS · engine
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, tooltip = "The rented tool uses 72 / 1.5 (very slow). 10 / 3 reacts within the session.", 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,
tooltip = "Bars on each side that confirm a swing. BOS / CHoCH and the structure stop come from these swings; a swing is known only N bars after it forms — no repainting, some lag.", 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,
tooltip = "The H factor: EMA 20 vs 50 on this timeframe, read from its LAST CLOSED bar (no repainting). Must be higher than the chart timeframe or the factor is neutral.", display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · style / panels / alerts
gV = "Style"
inView = input.string("Clean", "View preset", options = ["Minimal", "Clean", "Pro", "Full"], group = gV,
tooltip = "Minimal = signals + trade zones only. Clean = + EMA trend cloud. Pro = + Supertrend, VWAP, BOS / CHoCH tags, filtered-signal ghosts. Full = + strength-coloured candles + session shading.", display = display.none)
inTheme = input.string("Aurora Neon", "Theme", options = ["Aurora Neon", "Royal Gold", "Colorblind-safe"], group = gV, display = display.none)
inCloud = input.bool(true, "EMA trend cloud (gradient)", group = gV, tooltip = "A gradient between the fast and slow EMA — dense at the fast line, fading to the slow one; colour = trend, opacity = strength.", display = display.none)
inZones = input.bool(true, "Trade zones (gradient risk / reward)", group = gV, tooltip = "Every trade paints its risk (entry → stop) and reward (entry → TP3) as gradients that fade away from the entry. The risk zone shrinks as the stop moves to breakeven / trails.", display = display.none)
inBarCol = input.bool(false, "Colour candles by strength", group = gV, tooltip = "Grey = weak, full trend colour = strong. Also on in the Full preset.", display = display.none)
gD = "Dashboard"
inDash = input.bool(true, "Show dashboard", group = gD, display = display.none)
inDashPos = input.string("Top Right", "Position", options = ["Top Right", "Top Left", "Bottom Right", "Bottom Left", "Middle Right", "Middle Left"], group = gD, display = display.none)
inSize = input.string("Medium", "Size", options = ["Small", "Medium", "Large"], group = gD, display = display.none)
inZebra = input.bool(true, "Row stripes", group = gD, display = display.none)
gN = "Narrative · Co-Pilot"
inNarr = input.string("Standard", "Narrative depth", options = ["Off", "Brief", "Standard", "Detailed"], group = gN,
tooltip = "Plain-language read of what the desk is doing now. Brief = headline · Standard = + why + plan · Detailed = + confidence, blind spots, track record. Display only.", display = display.none)
inNarrPos = input.string("Bottom Left", "Position", options = ["Bottom Left", "Bottom Right", "Top Left", "Top Right", "Middle Left", "Middle Right"], group = gN, display = display.none)
inNarrSize = input.string("Auto", "Size", options = ["Auto", "Small", "Medium", "Large"], group = gN, display = display.none)
inNarrWrap = input.int(64, "Line width (characters)", minval = 30, maxval = 120, group = gN, display = display.none)
gA = "Alerts"
inAlertJson = input.bool(true, "alert() payload as JSON (webhooks)", group = gA,
tooltip = "Off = a plain sentence. Either way, choose 'Any alert() function call' when you create the alert. The alertcondition entries below it are the classic per-event alerts.", display = display.none)
inAlertMile = input.bool(true, "alert() on TP1 / TP2 milestones", group = gA, display = display.none)
// ─────────────────────────────────────────────────────────────────── THEME (colour by role)
type Theme
color bg = #0B0E14
color frame = #B8923A
color accent = #D8B14A
color accentHi = #E8C25A
color up = #2BE7C7
color down = #FF3B5C
color flat = #6B7280
color txt = #D7DCE6
color txtHi = #ECEFF5
themeOf(string name) =>
Theme t = Theme.new()
if name == "Aurora Neon"
t.bg := #0E1118
t.frame := #2C3A5A
t.accent := #7C9CFF
t.accentHi := #A9B8FF
t.up := #2AF0C8
t.down := #FF4D6D
t.flat := #5B6472
t.txt := #E6E9EF
t.txtHi := #F2F4F9
else if name == "Colorblind-safe"
t.bg := #0E1014
t.frame := #3A4150
t.accent := #56B4E9
t.accentHi := #8ED0F5
t.up := #E69F00
t.down := #0072B2
t.flat := #8C92A0
t.txt := #E8EAED
t.txtHi := #FFFFFF
t
Theme th = themeOf(inTheme)
bool vClean = inView != "Minimal"
bool vPro = inView == "Pro" or inView == "Full"
bool vFull = inView == "Full"
bool showCloud = vClean and inCloud
bool showZones = inZones
bool showSt = vPro
bool showStruct = vPro
bool showGhost = vPro
bool showSess = vFull
bool showBars = inBarCol or vFull
fmt(float x) => str.tostring(x, format.mintick)
clamp01(float x) => math.max(0.0, math.min(1.0, x))
// ═══════════════════════════════════════════════════════════════════ ENGINE (all ta.* / request.* at global scope)
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 // ta.supertrend: −1 = uptrend, +1 = downtrend
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
// ── context: VWAP side · higher-timeframe bias (last CLOSED HTF bar → no repaint) · room in the day's range
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
// ── structure: confirmed swings → BOS / CHoCH (commits on closed bars only)
var float swHigh = na
var float swLow = na
var float pivHigh = na
var float pivLow = na
var int structDir = 0
var int structBar = na
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
structBar := bar_index
swHigh := na
if not na(swLow) and close < swLow
if structDir == 1
chochDn := true
else
bosDn := true
structDir := -1
structBar := bar_index
swLow := na
// ── the eight factors, per side (1 = agrees, 0 = not, 0.5 = not available on this chart)
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) // not buying straight into the day's high
bool roomS = dayBars < 3 or not (toLo > 0.1 * atrU and toLo < 1.0 * atrU)
// feature vectors (index: 0 T · 1 M · 2 S · 3 L · 4 C · 5 V · 6 H · 7 R)
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
// ── STRENGTH model — online logistic regression over the eight factors (seeded as an equal-weight count)
// Features are CENTRED on their running mean over the triggers seen here (mu, starts at 0.5). A factor that
// is always present on triggers (the trigger requires it) therefore contributes nothing and cannot soak up
// the base rate — the bias learns the base rate, the weights learn the RELATIVE value of the factors that vary.
// STRENGTH 50 = a typical trigger on this chart; above = better-than-typical mix of what has paid here.
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
// one causal gradient step on the FROZEN features of a trigger that has already resolved
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)
int ctxMax = (vwapOK ? 1 : 0) + (htfValid ? 1 : 0) + 1
int ctxL = (vwapOK and close > vwapPx ? 1 : 0) + (htfValid and htfBias == 1 ? 1 : 0) + (roomL ? 1 : 0)
int ctxS = (vwapOK and close < vwapPx ? 1 : 0) + (htfValid and htfBias == -1 ? 1 : 0) + (roomS ? 1 : 0)
// grade — default from the five setup factors (measured best); optionally from the learned strength
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)
// ── triggers
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"
// ═══════════════════════════════════════════════════════════════════ TRADE STATE (confirmed bars only)
type Trade
int dir = 0
float entry = na
float sl = na
float sl0 = na
float tp1 = na
float tp2 = na
float tp3 = na
float r = na
string grade = ""
float strength = na
int bar = na
bool tp1Hit = false
bool tp2Hit = false
array<float> fx
type Stat
int n = 0
int wins = 0
int scratch = 0
float sumR = 0.0
float pnlTotal = 0.0
float pnlToday = 0.0
int today = 0
int lastExit = -100000
int nA = 0
int wA = 0
int nB = 0
int wB = 0
int nC = 0
int wC = 0
var Trade t = Trade.new(fx = array.new<float>(8, 0.5))
var Stat st = Stat.new()
if newDay
st.today := 0
st.pnlToday := 0.0
bool gateCommon = barstate.isconfirmed and not na(atr) and winOK and st.today < inMaxDay and (bar_index - st.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
bool entryL = false
bool entryS = false
bool exitNow = false
int exitDir = 0
float exitPx = na
string exitWhy = ""
float exitR = na
bool tp1Now = false
bool tp2Now = false
// 1 · manage the open trade — the stop is checked FIRST (a bar that straddles stop and target books the stop)
if barstate.isconfirmed and t.dir != 0
int d = t.dir
bool stopHit = d == 1 ? low <= t.sl : high >= t.sl
if stopHit
exitNow := true
exitPx := t.sl
exitWhy := t.tp2Hit ? "TRAIL" : (t.tp1Hit and inBE) ? "BE" : "SL"
else
bool tp3Hit = d == 1 ? high >= t.tp3 : low <= t.tp3
if tp3Hit
exitNow := true
exitPx := t.tp3
exitWhy := "TP3"
else
bool h1 = d == 1 ? high >= t.tp1 : low <= t.tp1
bool h2 = d == 1 ? high >= t.tp2 : low <= t.tp2
if h1 and not t.tp1Hit
t.tp1Hit := true
tp1Now := true
if inBE
t.sl := t.entry
if h2 and not t.tp2Hit
t.tp2Hit := true
tp2Now := true
if inTrail
t.sl := t.tp1
if inTrail and t.tp2Hit
float trailPx = d == 1 ? close - inSlAtr * atrU : close + inSlAtr * atrU
t.sl := d == 1 ? math.max(t.sl, trailPx) : math.min(t.sl, trailPx)
bool flip = inExitTrend and (d == 1 ? stFlipDn : stFlipUp)
bool momLoss = (bar_index - t.bar) >= 2 and (inMomExit == "RSI vs average" ? (d == 1 ? rsiXdn : rsiXup) : inMomExit == "RSI through 50" ? (d == 1 ? rsi50Dn : rsi50Up) : false)
bool opp = inExitOpp and (d == 1 ? candS : candL)
bool sq = inMode == "Intraday" and sqOff
bool timeUp = inTimeStop > 0 and not t.tp1Hit and (bar_index - t.bar) >= inTimeStop
if flip or momLoss or opp or sq or timeUp
exitNow := true
exitPx := close
exitWhy := sq ? "SQ-OFF" : flip ? "TREND" : opp ? "OPP" : momLoss ? "MOM" : "TIME"
if exitNow
float pts = (exitPx - t.entry) * d
exitR := pts / t.r
exitDir := d
bool isWin = exitR >= 0.05
bool isScratch = math.abs(exitR) < 0.05
st.n := st.n + 1
st.wins := st.wins + (isWin ? 1 : 0)
st.scratch := st.scratch + (isScratch ? 1 : 0)
if t.grade == "A"
st.nA := st.nA + 1
st.wA := st.wA + (isWin ? 1 : 0)
else if t.grade == "B"
st.nB := st.nB + 1
st.wB := st.wB + (isWin ? 1 : 0)
else
st.nC := st.nC + 1
st.wC := st.wC + (isWin ? 1 : 0)
st.sumR := st.sumR + exitR
st.pnlTotal := st.pnlTotal + pts * inQty
st.pnlToday := st.pnlToday + pts * inQty
st.lastExit := bar_index
t.dir := 0
// 2 · open a trade (flat; a same-bar re-entry only when the exit was a reversal)
bool canOpen = t.dir == 0 and (not exitNow or exitWhy == "OPP")
if candL and canOpen
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)
float r = close - slPx
t.dir := 1
t.entry := close
t.sl := slPx
t.sl0 := slPx
t.r := r
t.tp1 := close + inTp1 * r
t.tp2 := close + inTp2 * r
t.tp3 := close + inTp3 * r
t.grade := gradeL
t.strength := strengthL
t.bar := bar_index
t.tp1Hit := false
t.tp2Hit := false
t.fx := array.copy(fxL)
st.today := st.today + 1
entryL := true
else if candS and canOpen
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)
float r = slPx - close
t.dir := -1
t.entry := close
t.sl := slPx
t.sl0 := slPx
t.r := r
t.tp1 := close - inTp1 * r
t.tp2 := close - inTp2 * r
t.tp3 := close - inTp3 * r
t.grade := gradeS
t.strength := strengthS
t.bar := bar_index
t.tp1Hit := false
t.tp2Hit := false
t.fx := array.copy(fxS)
st.today := st.today + 1
entryS := true
// ═══════════════════════════════════════════════════════════════════ SHADOW LEARNING
// Every trigger — taken or filtered — is followed as a shadow 1R bracket (stop first, resolved only on
// LATER bars). Learning from the whole population of triggers, not just the gated trades, is what lets
// the model see which factors separate winners from losers. Taken trades are a subset; no double count.
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)
// ═══════════════════════════════════════════════════════════════════ OPTIONS MAPPING + ALERT PAYLOADS
optAction(int d) => inOptMap ? (d == 1 ? "BUY CE" : "BUY PE") : (d == 1 ? "BUY" : "SELL")
strikeOf(int d) =>
float base = math.round(close / inStrikeStep) * inStrikeStep
d == 1 ? base + inStrikeOff * inStrikeStep : base - inStrikeOff * inStrikeStep
strikeTxt(int d) => inOptMap ? str.tostring(strikeOf(d), "0") : ""
instrumentTxt(int d) => inOptMap ? syminfo.ticker + (inExpiry != "" ? " " + inExpiry : "") + " " + strikeTxt(d) + (d == 1 ? " CE" : " PE") : syminfo.ticker
payloadEntry(int d, string g) =>
string act = optAction(d)
string sk = strikeTxt(d)
int sc = math.round(d == 1 ? strengthL : strengthS)
string js = '{"src":"SignalDesk","symbol":"' + syminfo.ticker + '","tf":"' + timeframe.period + '","event":"ENTRY","side":"' + (d == 1 ? "LONG" : "SHORT") + '","action":"' + act + '","instrument":"' + instrumentTxt(d) + '"' + (inOptMap ? ',"strike":' + sk : '') + ',"grade":"' + g + '","strength":' + str.tostring(sc) + ',"setup":' + str.tostring(d == 1 ? setupL : setupS) + ',"context":' + str.tostring(d == 1 ? ctxL : ctxS) + ',"entry":' + fmt(t.entry) + ',"sl":' + fmt(t.sl) + ',"tp1":' + fmt(t.tp1) + ',"tp2":' + fmt(t.tp2) + ',"tp3":' + fmt(t.tp3) + ',"qty":' + str.tostring(inQty) + '}'
string tx = "Signal Desk · " + instrumentTxt(d) + " " + timeframe.period + " · " + act + (inOptMap ? " " + sk : "") + " · grade " + g + " · strength " + str.tostring(sc) + " · entry " + fmt(t.entry) + " · SL " + fmt(t.sl) + " · TP " + fmt(t.tp1) + " / " + fmt(t.tp2) + " / " + fmt(t.tp3) + " · qty " + str.tostring(inQty)
inAlertJson ? js : tx
payloadExit(int d, string why, float px, float rr) =>
string js = '{"src":"SignalDesk","symbol":"' + syminfo.ticker + '","tf":"' + timeframe.period + '","event":"EXIT","side":"' + (d == 1 ? "LONG" : "SHORT") + '","instrument":"' + instrumentTxt(d) + '","reason":"' + why + '","price":' + fmt(px) + ',"r":' + str.tostring(rr, "0.00") + ',"qty":' + str.tostring(inQty) + '}'
string tx = "Signal Desk · " + instrumentTxt(d) + " " + timeframe.period + " · EXIT " + (d == 1 ? "LONG" : "SHORT") + " · " + why + " at " + fmt(px) + " · " + (rr >= 0 ? "+" : "") + str.tostring(rr, "0.0") + "R"
inAlertJson ? js : tx
payloadMile(string which) =>
string js = '{"src":"SignalDesk","symbol":"' + syminfo.ticker + '","tf":"' + timeframe.period + '","event":"' + which + '","side":"' + (t.dir == 1 ? "LONG" : "SHORT") + '","price":' + fmt(close) + ',"sl_now":' + fmt(t.sl) + '}'
string tx = "Signal Desk · " + syminfo.ticker + " " + timeframe.period + " · " + which + " reached · stop now " + fmt(t.sl)
inAlertJson ? js : tx
// ═══════════════════════════════════════════════════════════════════ CHART
int viewSide = t.dir != 0 ? t.dir : fTrend != 0 ? fTrend : 1
float viewStr = viewSide == 1 ? strengthL : strengthS
string viewGrade = viewSide == 1 ? gradeL : gradeS
color sideCol = viewSide == 1 ? th.up : th.down
strCol(float s, int d) => color.from_gradient(s, 40, 85, color.new(d == 1 ? th.up : th.down, 60), d == 1 ? th.up : th.down)
float trendStr = fTrend == 1 ? strengthL : fTrend == -1 ? strengthS : 50.0
color cloudCol = fTrend == 1 ? th.up : fTrend == -1 ? th.down : th.flat
int cloudTop = 96 - int(22 * clamp01((trendStr - 40) / 45)) // stronger → denser (74..96 transparency)
// EMA trend cloud: dense at the fast EMA, fading to the slow one (the only native gradient Pine has)
plEmaF = plot(emaF, "EMA fast", color = vClean ? color.new(th.accent, 15) : na, linewidth = 1)
plEmaS = plot(emaS, "EMA slow", color = vClean ? color.new(th.flat, 35) : na, linewidth = 1)
fill(plEmaF, plEmaS, top_value = math.max(emaF, emaS), bottom_value = math.min(emaF, emaS),
top_color = showCloud ? color.new(cloudCol, emaF >= emaS ? cloudTop : 99) : color.new(th.bg, 100),
bottom_color = showCloud ? color.new(cloudCol, emaF >= emaS ? 99 : cloudTop) : color.new(th.bg, 100), title = "Trend cloud")
// trade zones — every trade paints its risk (entry → stop) and reward (entry → TP3), fading away from the entry
bool zoneOn = showZones and (t.dir != 0 or exitNow)
float zEntry = zoneOn ? t.entry : na
float zStop = zoneOn ? t.sl : na
float zTp1 = zoneOn ? t.tp1 : na
float zTp2 = zoneOn ? t.tp2 : na
float zTp3 = zoneOn ? t.tp3 : na
int zDir = t.dir != 0 ? t.dir : exitDir
plEntry = plot(zEntry, "Trade · entry", color = color.new(th.txtHi, 15), linewidth = 1, style = plot.style_linebr, display = display.pane)
plStop = plot(zStop, "Trade · stop", color = color.new(th.down, 15), linewidth = 1, style = plot.style_linebr, display = display.pane)
plTp1 = plot(zTp1, "Trade · TP1", color = color.new(th.up, 60), linewidth = 1, style = plot.style_linebr, display = display.pane)
plTp2 = plot(zTp2, "Trade · TP2", color = color.new(th.up, 45), linewidth = 1, style = plot.style_linebr, display = display.pane)
plTp3 = plot(zTp3, "Trade · TP3", color = color.new(th.up, 15), linewidth = 1, style = plot.style_linebr, display = display.pane)
fill(plEntry, plStop, top_value = math.max(zEntry, zStop), bottom_value = math.min(zEntry, zStop),
top_color = zDir == 1 ? color.new(th.down, 70) : color.new(th.down, 94), bottom_color = zDir == 1 ? color.new(th.down, 94) : color.new(th.down, 70), title = "Risk zone", fillgaps = false)
fill(plEntry, plTp3, top_value = math.max(zEntry, zTp3), bottom_value = math.min(zEntry, zTp3),
top_color = zDir == 1 ? color.new(th.up, 94) : color.new(th.up, 72), bottom_color = zDir == 1 ? color.new(th.up, 72) : color.new(th.up, 94), title = "Reward zone", fillgaps = false)
plot(stLine, "Supertrend", color = showSt ? (stUp ? color.new(th.up, 55) : color.new(th.down, 55)) : na, linewidth = 1, style = plot.style_linebr, display = display.pane)
plot(vwapOK ? vwapPx : na, "VWAP", color = showSt ? color.new(th.accentHi, 45) : na, linewidth = 1, style = plot.style_circles, display = display.pane)
bgcolor(showSess and winOK ? color.new(th.accent, 96) : showSess and sqOff ? color.new(th.down, 93) : na, title = "Session shading")
barcolor(showBars ? color.from_gradient(viewStr, 35, 80, color.new(th.flat, 20), sideCol) : na, title = "Candles by strength")
// ghosts: a trigger fired but was filtered (grade / veto / window / cap / cooldown) — Pro view
plotshape(showGhost and trigL and not candL and barstate.isconfirmed, "Filtered long", shape.circle, location.belowbar, color = color.new(th.up, 72), size = size.tiny, display = display.pane)
plotshape(showGhost and trigS and not candS and barstate.isconfirmed, "Filtered short", shape.circle, location.abovebar, color = color.new(th.down, 72), size = size.tiny, display = display.pane)
// entry / exit labels (dynamic text → labels)
if entryL
label.new(bar_index, low, optAction(1) + (inOptMap ? " " + strikeTxt(1) : "") + " · " + t.grade + " " + str.tostring(t.strength, "0"), style = label.style_label_up, color = strCol(t.strength, 1), textcolor = th.bg, size = size.small,
tooltip = "LONG · grade " + t.grade + " · strength " + str.tostring(t.strength, "0") + "\nsetup " + str.tostring(setupL) + "/5 · context " + str.tostring(ctxL) + "/" + str.tostring(ctxMax) + "\nentry " + fmt(t.entry) + "\nstop " + fmt(t.sl) + " (" + fmt(t.r) + " = 1R)\nTP1 " + fmt(t.tp1) + " · TP2 " + fmt(t.tp2) + " · TP3 " + fmt(t.tp3))
if entryS
label.new(bar_index, high, optAction(-1) + (inOptMap ? " " + strikeTxt(-1) : "") + " · " + t.grade + " " + str.tostring(t.strength, "0"), style = label.style_label_down, color = strCol(t.strength, -1), textcolor = th.bg, size = size.small,
tooltip = "SHORT · grade " + t.grade + " · strength " + str.tostring(t.strength, "0") + "\nsetup " + str.tostring(setupS) + "/5 · context " + str.tostring(ctxS) + "/" + str.tostring(ctxMax) + "\nentry " + fmt(t.entry) + "\nstop " + fmt(t.sl) + " (" + fmt(t.r) + " = 1R)\nTP1 " + fmt(t.tp1) + " · TP2 " + fmt(t.tp2) + " · TP3 " + fmt(t.tp3))
if exitNow
string rTxt = (exitR >= 0 ? "+" : "") + str.tostring(exitR, "0.0") + "R"
label.new(bar_index, exitDir == 1 ? high : low, (exitDir == 1 ? "XL" : "XS") + " · " + exitWhy + " " + rTxt, style = exitDir == 1 ? label.style_label_down : label.style_label_up, color = color.new(th.bg, 15), textcolor = exitR >= 0 ? th.up : th.down, size = size.tiny,
tooltip = "exit " + fmt(exitPx) + " · " + exitWhy + " · " + rTxt + " · paper " + (exitR >= 0 ? "+" : "") + str.tostring(exitR * t.r * inQty, "0") + " (no costs)")
if showStruct and (bosUp or chochUp)
label.new(bar_index, high, chochUp ? "CHoCH" : "BOS", style = label.style_label_down, color = color.new(th.up, 82), textcolor = th.up, size = size.tiny)
if showStruct and (bosDn or chochDn)
label.new(bar_index, low, chochDn ? "CHoCH" : "BOS", style = label.style_label_up, color = color.new(th.down, 82), textcolor = th.down, size = size.tiny)
// ═══════════════════════════════════════════════════════════════════ WORDS (one source of truth)
int viewSetup = viewSide == 1 ? setupL : setupS
int viewCtx = viewSide == 1 ? ctxL : ctxS
string okG = viewSide == 1 ? "▲" : "▼"
fx(string letter, bool ok) => letter + (ok ? okG : "·") + " "
string setupGlyphs = fx("T", viewSide == 1 ? fTrend == 1 : fTrend == -1) + fx("M", viewSide == 1 ? fMom == 1 : fMom == -1) + fx("S", viewSide == 1 ? fStruct == 1 : fStruct == -1) + fx("L", viewSide == 1 ? locL : locS) + fx("C", viewSide == 1 ? candleL : candleS)
string ctxGlyphs = (vwapOK ? fx("V", viewSide == 1 ? close > vwapPx : close < vwapPx) : "V– ") + (htfValid ? fx("H", viewSide == 1 ? htfBias == 1 : htfBias == -1) : "H– ") + fx("R", viewSide == 1 ? roomL : roomS)
wGlyph(int i) =>
float w = array.get(mdl.w, i)
w >= 0.95 ? "+" : w <= 0.55 ? "−" : "·"
string learnedGlyphs = "T" + wGlyph(0) + " M" + wGlyph(1) + " S" + wGlyph(2) + " L" + wGlyph(3) + " C" + wGlyph(4) + " V" + wGlyph(5) + " H" + wGlyph(6) + " R" + wGlyph(7)
string learnedTip = "Learned weights on this chart (start 0.75 each; + ≥ 0.95 = this factor paid here, − ≤ 0.55 = it hurt, · = about neutral). T " + str.tostring(array.get(mdl.w, 0), "0.00") + " · M " + str.tostring(array.get(mdl.w, 1), "0.00") + " · S " + str.tostring(array.get(mdl.w, 2), "0.00") + " · L " + str.tostring(array.get(mdl.w, 3), "0.00") + " · C " + str.tostring(array.get(mdl.w, 4), "0.00") + " · V " + str.tostring(array.get(mdl.w, 5), "0.00") + " · H " + str.tostring(array.get(mdl.w, 6), "0.00") + " · R " + str.tostring(array.get(mdl.w, 7), "0.00") + " · bias " + str.tostring(mdl.w0, "0.00") + " · learned from " + str.tostring(mdl.trained) + " resolved triggers (taken or not). Small samples move slowly on purpose."
string posWord = t.dir == 1 ? "▲ LONG · " + t.grade + " " + str.tostring(t.strength, "0") : t.dir == -1 ? "▼ SHORT · " + t.grade + " " + str.tostring(t.strength, "0") : "● FLAT"
color posColor = t.dir == 1 ? th.up : t.dir == -1 ? th.down : th.txt
bool coolActive = (bar_index - st.lastExit) < inCooldown
string stateWord = t.dir != 0 ? "IN TRADE" : sqOff ? "SQUARE-OFF" : not winOK ? "NO ENTRIES" : st.today >= inMaxDay ? "CAP " + str.tostring(st.today) + "/" + str.tostring(inMaxDay) : coolActive ? "COOLDOWN" : "WAITING"
color stateColor = t.dir != 0 ? posColor : winOK and st.today < inMaxDay and not coolActive ? th.accentHi : th.flat
string clockTxt = str.format_time(time, "HH:mm", inTZ)
string trendWord = fTrend == 1 ? "up" : fTrend == -1 ? "down" : "mixed"
// ═══════════════════════════════════════════════════════════════════ DASHBOARD
posOf(string s) =>
s == "Top Left" ? position.top_left : s == "Bottom Right" ? position.bottom_right : s == "Bottom Left" ? position.bottom_left : s == "Middle Right" ? position.middle_right : s == "Middle Left" ? position.middle_left : position.top_right
szLead() => inSize == "Small" ? size.small : inSize == "Large" ? size.large : size.normal
szBody() => inSize == "Small" ? size.tiny : inSize == "Large" ? size.normal : size.small
szSub() => inSize == "Small" ? size.tiny : inSize == "Large" ? size.small : size.tiny
gauge(float p01, int segs) =>
string out = ""
int filled = na(p01) ? 0 : int(math.round(clamp01(p01) * segs))
for i = 0 to segs - 1
out := out + (i < filled ? "▰" : "▱")
out
kv(table tb, int row, string k, string v, color vc, string tip) =>
color stripe = inZebra and row % 2 == 1 ? color.new(th.accent, 93) : th.bg
table.cell(tb, 0, row, k, text_color = th.flat, text_size = szBody(), text_halign = text.align_left, bgcolor = stripe, tooltip = tip)
table.cell(tb, 1, row, v, text_color = vc, text_size = szBody(), text_halign = text.align_right, text_font_family = font.family_monospace, bgcolor = stripe, tooltip = tip)
kvBg(table tb, int row, string k, string v, color vc, color bgc, string tip) =>
table.cell(tb, 0, row, k, text_color = th.flat, text_size = szBody(), text_halign = text.align_left, bgcolor = th.bg, tooltip = tip)
table.cell(tb, 1, row, v, text_color = vc, text_size = szBody(), text_halign = text.align_right, text_font_family = font.family_monospace, bgcolor = bgc, tooltip = tip)
var table dash = table.new(posOf(inDashPos), 2, 18, bgcolor = th.bg, frame_color = th.frame, frame_width = 1, border_width = 0) // pre-sized with headroom (rows written: 16)
if barstate.islast and inDash
table.clear(dash, 0, 0, 1, 17)
int r = 0
table.cell(dash, 0, r, "SIGNAL DESK", text_color = th.accentHi, text_size = szLead(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "Graded, learning long / short signals for option buyers. Confirmed bars only.")
table.cell(dash, 1, r, syminfo.ticker + " · " + timeframe.period, text_color = th.accent, text_size = szSub(), text_halign = text.align_right, bgcolor = th.bg)
r += 1
table.cell(dash, 0, r, posWord, text_color = posColor, text_size = szLead(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "Position, its grade and strength at entry.")
table.cell(dash, 1, r, stateWord, text_color = stateColor, text_size = szBody(), text_halign = text.align_right, bgcolor = th.bg, tooltip = "WAITING = flat, inside the entry window, cap not reached. CAP = today's entry limit used. COOLDOWN = bars since the last exit < cooldown. NO ENTRIES = outside the entry window. SQUARE-OFF = the intraday exit window.")
r += 1
color strBg = color.from_gradient(viewStr, 30, 85, color.new(th.down, 75), color.new(th.up, 55))
kvBg(dash, r, (viewSide == 1 ? "Long" : "Short") + " strength", gauge(viewStr / 100.0, 10) + " " + str.tostring(viewStr, "0") + (viewGrade != "" ? " · " + viewGrade : ""), th.txtHi, strBg, "STRENGTH 0–100 for the side shown = the learned model's read of the eight factors on this bar. Cell colour runs red (weak) → green (strong). A ≥ " + str.tostring(inGradeA) + " · B ≥ " + str.tostring(inGradeB) + " · C ≥ 50.")
r += 1
kv(dash, r, "Setup", setupGlyphs + " " + str.tostring(viewSetup) + "/5", viewSetup >= 4 ? sideCol : th.txt, "SETUP factors — T Trend (Supertrend + EMA 20/50) · M Momentum (RSI vs its average, above/below 50) · S Structure (last BOS / CHoCH) · L Location (within 1 ATR of the fast EMA) · C Candle (a clean close in the signal direction, not oversized). ▲/▼ = present · = missing.")
r += 1
kv(dash, r, "Context", ctxGlyphs + " " + str.tostring(viewCtx) + "/" + str.tostring(ctxMax), viewCtx == ctxMax ? sideCol : th.txt, "CONTEXT factors — V VWAP side (long above / short below) · H Higher-timeframe bias (EMA 20/50 on " + inHtf + ", last closed bar) · R Room (not buying straight into the day's high / selling into its low). – = not available here (fed to the model as neutral).")
r += 1
kv(dash, r, "Learned", learnedGlyphs, inLearn ? th.txt : th.flat, learnedTip + (inLearn ? "" : " Learning is OFF — fixed equal weights."))
r += 1
kv(dash, r, "Grade", (viewGrade != "" ? viewGrade : "—") + " · " + (gradeRank(viewGrade) >= gradeRank(inMinGrade) and viewGrade != "" ? "fires" : "below ≥" + inMinGrade) + (inGateMode == "Learned strength" ? " · learned" : " · setup count"), viewGrade == "A" ? sideCol : viewGrade == "B" ? th.accentHi : th.txt, "Grade of the current bar for the side shown. Setup count mode (default): A = 5/5 setup factors · B = 4/5 · C = 3/5. Learned-strength mode: A ≥ " + str.tostring(inGradeA) + " · B ≥ " + str.tostring(inGradeB) + " · C ≥ 50. 'fires' = a trigger on this bar would be allowed to signal (grade ≥ your minimum).")
r += 1
kv(dash, r, "Entry", t.dir != 0 ? fmt(t.entry) : "—", th.txtHi, "Reference entry = the close of the signal bar.")
r += 1
kv(dash, r, "Stop", t.dir != 0 ? fmt(t.sl) + (t.tp2Hit ? " · TRAIL" : t.tp1Hit and inBE ? " · BE" : "") : "—", th.down, "Current stop. BE = moved to breakeven after TP1 · TRAIL = ATR trail after TP2.")
r += 1
kv(dash, r, "Targets", t.dir != 0 ? fmt(t.tp1) + " · " + fmt(t.tp2) + " · " + fmt(t.tp3) : "—", th.up, "TP1 / TP2 / TP3 in R-multiples of the initial stop distance.")
r += 1
string htfWord = htfValid ? (htfBias == 1 ? "up" : htfBias == -1 ? "down" : "—") : "n/a"
string vwapWord = vwapOK ? (close > vwapPx ? "above" : "below") : "n/a"
kv(dash, r, "Trend", trendWord + " · " + inHtf + "m " + htfWord + " · VWAP " + vwapWord + " · RSI " + str.tostring(rsi, "0"), fTrend == 1 ? th.up : fTrend == -1 ? th.down : th.txt, "Chart trend (Supertrend + EMA alignment) · higher-timeframe bias (" + inHtf + ", last closed bar) · price vs session VWAP · RSI.")
r += 1
kv(dash, r, "Session", clockTxt + " · " + (winOK ? "OPEN" : sqOff ? "SQ-OFF" : "CLOSED"), winOK ? th.txt : th.flat, "Exchange time (" + inTZ + "). Entry window " + inEntryWin + " · square-off " + inSqOff + ".")
r += 1
kv(dash, r, "Today", str.tostring(st.today) + "/" + str.tostring(inMaxDay) + " · " + (st.pnlToday >= 0 ? "+" : "") + str.tostring(st.pnlToday, "0"), st.pnlToday > 0 ? th.up : st.pnlToday < 0 ? th.down : th.txt, "Entries used today / cap · paper P&L today = points × qty, NO costs.")
r += 1
int decided = st.n - st.scratch
string recTxt = st.n < 5 ? str.tostring(st.n) + " closed · too few" : str.tostring(st.n) + " closed · " + (decided > 0 ? str.tostring(math.round(st.wins / decided * 100)) + "% win" : "—") + " · avg " + (st.sumR / st.n >= 0 ? "+" : "") + str.tostring(st.sumR / st.n, "0.00") + "R"
kv(dash, r, "Record", recTxt, th.txt, "Closed signals on this chart: count · win% of decided trades (breakeven scratches excluded: " + str.tostring(st.scratch) + ") · average R over all. Paper, no costs, small sample — a description of the past on THIS chart, not an edge.")
r += 1
string txA = st.nA > 0 ? "A " + str.tostring(math.round(st.wA / st.nA * 100)) + "% (" + str.tostring(st.nA) + ")" : "A —"
string txB = st.nB > 0 ? "B " + str.tostring(math.round(st.wB / st.nB * 100)) + "% (" + str.tostring(st.nB) + ")" : "B —"
string txC = st.nC > 0 ? " · C " + str.tostring(math.round(st.wC / st.nC * 100)) + "% (" + str.tostring(st.nC) + ")" : ""
kv(dash, r, "By grade", txA + " · " + txB + txC, th.txt, "Win% per entry grade (wins ÷ all trades of that grade, scratches count as non-wins). If A is not clearly better than B on your chart, the grade is not earning its keep here — tell us.")
r += 1
table.cell(dash, 0, r, inMode + " · " + inTrigger + " · ≥" + inMinGrade, text_color = th.flat, text_size = szSub(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "Mode · trigger · minimum grade.")
table.cell(dash, 1, r, (st.pnlTotal >= 0 ? "+" : "") + str.tostring(st.pnlTotal, "0") + " paper · no costs", text_color = th.flat, text_size = szSub(), text_halign = text.align_right, bgcolor = th.bg, tooltip = "Total paper P&L over the loaded history. Use the strategy twin for costs.")
// ═══════════════════════════════════════════════════════════════════ NARRATIVE · Co-Pilot (translates state; never a new signal)
wrap(string s, int width) =>
array<string> words = str.split(s, " ")
string out = ""
string line = ""
for wd in words
if line != "" and str.length(line) + str.length(wd) + 1 > width
out := out + (out == "" ? "" : "\n") + line
line := wd
else
line := line == "" ? wd : line + " " + wd
out == "" ? line : out + "\n" + line
narrNow() =>
string s = ""
if t.dir != 0
string side = t.dir == 1 ? "long" : "short"
string nextT = not t.tp1Hit ? "TP1 " + fmt(t.tp1) : not t.tp2Hit ? "TP2 " + fmt(t.tp2) : "TP3 " + fmt(t.tp3)
s := "in a " + side + " from " + fmt(t.entry) + " (grade " + t.grade + ", strength " + str.tostring(t.strength, "0") + ") — stop " + fmt(t.sl) + (t.tp2Hit ? " (trailing)" : t.tp1Hit and inBE ? " (breakeven)" : "") + ", next target " + nextT
else if sqOff
s := "square-off window — no new entries, open positions are closed here in Intraday mode"
else if not winOK
s := "outside the entry window (" + inEntryWin + " " + inTZ + ") — the desk does not enter here"
else if st.today >= inMaxDay
s := "flat — today's cap of " + str.tostring(inMaxDay) + " entries is used; the next call is tomorrow"
else if coolActive
s := "flat — cooling down for " + str.tostring(inCooldown - (bar_index - st.lastExit)) + " more bars after the last exit"
else if fTrend == 0
s := "flat — trend is mixed (Supertrend and the EMAs disagree); no side to trade until they line up"
else
string side = fTrend == 1 ? "long" : "short"
string need = inTrigger == "Breakout" ? "a confirmed structure break in the trend direction" : "a pullback to the fast EMA and a strong close back in the trend direction with momentum turning"
s := "flat — trend " + trendWord + ", " + side + " strength " + str.tostring(viewStr, "0") + " (grade " + (viewGrade != "" ? viewGrade : "—") + "); waiting for " + need
s
narrWhy() =>
string t1 = fTrend == 1 ? "trend up (Supertrend + EMA 20 > 50)" : fTrend == -1 ? "trend down (Supertrend + EMA 20 < 50)" : "trend mixed"
string m1 = "RSI " + str.tostring(rsi, "0") + (rsi > rsiMa ? " above" : " below") + " its average"
string s1 = structDir == 1 ? "structure bullish (last break up" + (na(structBar) ? ")" : ", " + str.tostring(bar_index - structBar) + " bars ago)") : structDir == -1 ? "structure bearish (last break down" + (na(structBar) ? ")" : ", " + str.tostring(bar_index - structBar) + " bars ago)") : "no structure break yet"
string l1 = "price " + str.tostring(math.abs(distF), "0.0") + " ATR " + (distF >= 0 ? "above" : "below") + " the fast EMA" + (math.abs(distF) > inStretch ? " (stretched — chase veto)" : "")
string c1 = bigBar ? "this candle is oversized (" + str.tostring(rng / atrU, "0.0") + " ATR — veto)" : (viewSide == 1 ? bullBar : bearBar) ? "a clean candle in the signal direction" : "no clean signal candle"
string v1 = vwapOK ? "price " + (close > vwapPx ? "above" : "below") + " VWAP" : "no VWAP (no volume on this feed)"
string h1 = htfValid ? inHtf + "m bias " + (htfBias == 1 ? "up" : htfBias == -1 ? "down" : "unclear") : "HTF factor neutral (" + inHtf + " is not above the chart timeframe)"
string r1 = dayBars < 3 ? "day range too young for the room check" : (viewSide == 1 ? (roomL ? "room to the day's high " + str.tostring(toHi / atrU, "0.0") + " ATR" : "only " + str.tostring(toHi / atrU, "0.0") + " ATR to the day's high — no room") : (roomS ? "room to the day's low " + str.tostring(toLo / atrU, "0.0") + " ATR" : "only " + str.tostring(toLo / atrU, "0.0") + " ATR to the day's low — no room"))
string w1 = inLearn and mdl.trained > 0 ? " The model has learned from " + str.tostring(mdl.trained) + " resolved triggers here (weights: " + learnedGlyphs + ")." : ""
"Setup: " + t1 + " · " + m1 + " · " + s1 + " · " + l1 + " · " + c1 + ". Context: " + v1 + " · " + h1 + " · " + r1 + "." + w1
narrPlan() =>
string s = ""
if t.dir != 0
s := "stop is checked first on every closed bar; TP1 " + fmt(t.tp1) + " moves the stop to breakeven, TP2 " + fmt(t.tp2) + " to TP1 (then an ATR trail), TP3 " + fmt(t.tp3) + " is the final target. Also out on a Supertrend flip" + (inMomExit == "RSI vs average" ? ", on RSI losing its average" : inMomExit == "RSI through 50" ? ", on RSI back through 50" : "") + (inTimeStop > 0 ? ", after " + str.tostring(inTimeStop) + " bars without TP1" : "") + (inMode == "Intraday" ? ", and at square-off " + inSqOff : "") + "."
else if fTrend != 0
float refSl = fTrend == 1 ? math.max(math.min(nz(pivLow, lo5), lo5) - 0.25 * atrU, close - inSlAtr * 1.5 * atrU) : math.min(math.max(nz(pivHigh, hi5), hi5) + 0.25 * atrU, close + inSlAtr * 1.5 * atrU)
float refR = math.abs(close - refSl)
s := "if a " + (fTrend == 1 ? "long" : "short") + " fires near " + fmt(close) + ": stop ≈ " + fmt(refSl) + " (" + fmt(refR) + " = 1R), TP1 / TP2 / TP3 ≈ " + fmt(close + (fTrend == 1 ? 1 : -1) * inTp1 * refR) + " / " + fmt(close + (fTrend == 1 ? 1 : -1) * inTp2 * refR) + " / " + fmt(close + (fTrend == 1 ? 1 : -1) * inTp3 * refR) + ". Only grade " + inMinGrade + " or better fires; max " + str.tostring(inMaxDay) + " a day."
s
narrConf() =>
string g = t.dir != 0 ? t.grade : viewGrade
float sv = t.dir != 0 ? t.strength : viewStr
string word = g == "A" ? "high" : g == "B" ? "moderate" : g == "C" ? "low" : "no setup"
"Confidence: " + word + " (strength " + str.tostring(sv, "0") + ") · can't see: news, the option's theta / IV, order flow, the next tick, and whether your fill matches the signal close."
narrTrack() =>
int decided = st.n - st.scratch
st.n < 5 ? "Track record here: " + str.tostring(st.n) + " closed — too few to mean anything yet." : "Track record here: " + str.tostring(st.n) + " closed, " + (decided > 0 ? str.tostring(math.round(st.wins / decided * 100)) + "% won of " + str.tostring(decided) + " decided (" + str.tostring(st.scratch) + " breakeven)" : "all breakeven") + ", avg " + (st.sumR / st.n >= 0 ? "+" : "") + str.tostring(st.sumR / st.n, "0.00") + "R (paper, no costs, small sample — NOT an edge)."
narrPosOf(string s) =>
s == "Top Left" ? position.top_left : s == "Top Right" ? position.top_right : s == "Bottom Right" ? position.bottom_right : s == "Middle Left" ? position.middle_left : s == "Middle Right" ? position.middle_right : position.bottom_left
narrBodySz() => inNarrSize == "Small" ? size.small : inNarrSize == "Medium" ? size.normal : inNarrSize == "Large" ? size.large : szBody()
narrHeadSz() => inNarrSize == "Small" ? size.normal : inNarrSize == "Medium" ? size.large : inNarrSize == "Large" ? size.large : szLead()
var table narrT = na
if barstate.islast
if not na(narrT)
table.delete(narrT)
narrT := na
if inNarr != "Off"
array<string> labs = array.new<string>()
array<string> vals = array.new<string>()
array.push(labs, "NOW")
array.push(vals, wrap(narrNow() + " (decision support, not a trade instruction)", inNarrWrap))
bool deep = inNarr == "Standard" or inNarr == "Detailed"
if deep
array.push(labs, "WHY")
array.push(vals, wrap(narrWhy(), inNarrWrap))
string p = narrPlan()
if p != ""
array.push(labs, "PLAN")
array.push(vals, wrap(p, inNarrWrap))
if inNarr == "Detailed"
array.push(labs, "CONFIDENCE")
array.push(vals, wrap(narrConf(), inNarrWrap))
array.push(labs, "TRACK REC")
array.push(vals, wrap(narrTrack(), inNarrWrap))
int nr = array.size(labs)
bool atBottom = inNarrPos == "Bottom Left" or inNarrPos == "Bottom Right"
int rows = nr + 2 + (atBottom ? 1 : 0)
narrT := table.new(narrPosOf(inNarrPos), 2, rows, bgcolor = th.bg, frame_color = th.frame, frame_width = 1, border_color = color.new(th.frame, 70), border_width = 1)
table.cell(narrT, 0, 0, "◆ CO-PILOT", text_color = th.accentHi, bgcolor = color.new(th.accent, 88), text_size = narrHeadSz(), text_halign = text.align_left)
table.cell(narrT, 1, 0, stateWord + " · " + str.upper(inNarr), text_color = th.accent, bgcolor = color.new(th.accent, 88), text_size = narrBodySz(), text_halign = text.align_right)
for i = 0 to nr - 1
table.cell(narrT, 0, i + 1, array.get(labs, i), text_color = th.accent, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left, text_valign = text.align_top)
table.cell(narrT, 1, i + 1, array.get(vals, i), text_color = th.txt, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left, text_valign = text.align_top)
table.cell(narrT, 0, nr + 1, "", bgcolor = th.bg)
table.cell(narrT, 1, nr + 1, "Honest: graded signals are context for your decision. Paper P&L has no costs; the strategy twin measures it with costs.", text_color = th.flat, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left)
if atBottom
table.cell(narrT, 0, nr + 2, " ", bgcolor = th.bg)
table.cell(narrT, 1, nr + 2, " ", bgcolor = th.bg)
// ═══════════════════════════════════════════════════════════════════ ALERTS (confirmed bars only by construction)
alertcondition(entryL, "Long entry (grade ≥ minimum)", "Signal Desk: LONG entry")
alertcondition(entryS, "Short entry (grade ≥ minimum)", "Signal Desk: SHORT entry")
alertcondition(entryL and t.grade == "A", "Long entry · A grade only", "Signal Desk: LONG entry · grade A")
alertcondition(entryS and t.grade == "A", "Short entry · A grade only", "Signal Desk: SHORT entry · grade A")
alertcondition(exitNow and exitDir == 1, "Exit long", "Signal Desk: EXIT LONG")
alertcondition(exitNow and exitDir == -1, "Exit short", "Signal Desk: EXIT SHORT")
alertcondition(tp1Now, "TP1 reached", "Signal Desk: TP1 reached — stop moves per settings")
alertcondition(tp2Now, "TP2 reached", "Signal Desk: TP2 reached — stop moves per settings")
if entryL
alert(payloadEntry(1, t.grade), alert.freq_once_per_bar_close)
if entryS
alert(payloadEntry(-1, t.grade), alert.freq_once_per_bar_close)
if exitNow
alert(payloadExit(exitDir, exitWhy, exitPx, exitR), alert.freq_once_per_bar_close)
if inAlertMile and tp1Now
alert(payloadMile("TP1"), alert.freq_once_per_bar_close)
if inAlertMile and tp2Now
alert(payloadMile("TP2"), alert.freq_once_per_bar_close)
The strategy twin · optional
Measure the trading rules with costs on a second chart. Read the workflow chapter first.
Copy the code as it is. No coding needed.
View the full code
// ═══════════════════════════════════════════════════════════════════════════════
// 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)
No signal yet?
Check the session timezone and entry window, available history, minimum grade, vetoes, daily allowance and cooldown. The tool can correctly show no entry.
Go deeper, one question at a time.
The full rules and settings are here when you need them.
What starts a possible entry?
Pullback, the default: price visits the fast average, then gives a strong close beyond it as momentum turns. Trend also needs Supertrend and EMA agreement. A touch alone is insufficient.
Breakout: a close breaks a confirmed swing in the trend direction. BOS marks continuation; CHoCH marks a change from the prior structure direction. Both mode accepts either trigger.
Then come the grade, chase limits, entry window, available daily allowance and position checks. There is one position at a time and normally a six-candle cooldown after exit. A qualifying opposite-signal reversal can reopen on the exit candle, subject to that bar’s gates.
What do the five checks and the grade mean?
- Trend: Supertrend and fast/slow EMA direction agree.
- Momentum: RSI is on the trade’s side of its average and 50.
- Structure: the last confirmed swing break agrees.
- Location: close is near the fast EMA; long range is half an ATR below to one ATR above. Shorts mirror it.
- Candle: a strong directional body, close near its extreme, and no oversized range.
Default grades: A = five, B = four, C = three. Minimum B. Raising the minimum reduces qualifying setups; it does not establish better outcomes.
VWAP side, higher-timeframe direction and proximity to the day’s high/low are three context factors. They and learned strength are information by default, not extra entry gates.
Exactly what blocks a chase entry?
Any one blocks it: candle high–low range above 2.5 ATR; directional stretch from fast EMA above 2 ATR; or range of the last three closes above 3 ATR, with the close beyond the close three candles ago in the entry direction. Exact equality passes.
ATR measures recent price movement, including gaps. For a default pullback, the current candle or one of the previous three must touch within 0.3 ATR of the fast EMA. A strong body occupies at least forty percent of its candle; the wick beyond the close is at most thirty-five percent.
What does the learning score actually learn?
Strength 0–100 is a relative model score, not a chance of winning. Fifty represents the model’s typical factor mix. It affects the grade only if Grade from is changed to Learned strength.
- Open one practice bracketA trigger is sampled when no shadow bracket is active.
- Wait for its outcomeLater candles reach its stop, configured TP1 distance, or timeout.
- Adjust the factor weightsThe model updates using the sampled trigger’s saved readings.
Only one shadow runs at a time; intervening triggers are not separately sampled. Stop wins a same-candle collision. TP1 is 1R by default; thirty candles without resolution trains an outcome of 0.5. This differs from the full managed trade.
The Learned row’s T/M/S/L/C/V/H/R weights cover the five setup factors and three context factors. Plus/minus marks describe learned weights, not proof of a lasting advantage.
How do the stop and three targets work?
1R is the initial entry-to-stop distance. Default targets are 1R, 2R and 3R. TP1 moves the stop to entry; TP2 moves it to TP1 and starts an ATR trail; TP3 ends the plan. TP1 and TP2 do not take partial profits.
The default structure stop uses the farther recent swing or five-candle extreme, plus 0.25 ATR. If its distance exceeds 2.25 ATR, it falls back to 1.5 ATR. The ATR multiple setting changes that fallback, cap and trail.
Each later candle checks the existing stop before targets. A newly moved stop does not apply retroactively to that candle’s earlier wick.
Other exits include Supertrend reversal, RSI crossing its average after at least two candles held, a qualifying opposite signal, Intraday square-off, or the optional time stop before TP1. Exit labels record the reason.
How do I read the dashboard and Co-Pilot?
Setup / Grade show agreement; Context / Strength / Learned show supporting readings. Entry, Stop and Targets describe the plan; Trend, Session and Today show direction, time and daily use. Record / By grade summarize closed paper outcomes without costs.
Co-Pilot’s NOW describes the state, WHY gives the readings and PLAN names what follows under the rules. It explains the same engine, not an independent prediction.
Clean shows the trend cloud. Pro adds Supertrend, VWAP, swing labels and faint filtered-trigger marks. Full adds strength coloring and session shading. Minimal reduces the drawings.

What changes on an options chart?
For studying purchases on a call or put premium chart, the tool’s intended configuration is Long only, mapping off. On an index/futures chart, Both with mapping on labels longs BUY CE and shorts BUY PE.
Mapping formats a strike and expiry label. It does not read an option chain, premiums, Greeks or available expiries. Index-derived levels remain index prices. Quantity is an input, not automatic risk sizing.
An EXIT message recalculates its strike from the current price rather than preserving the entry strike. Treat it as descriptive data; it cannot by itself identify the original option contract for execution.
Which alerts can I create?
Eight named conditions: long/short entries, their A-only variants, long/short exits, TP1 and TP2. Choose Once Per Bar Close.
Any alert() function call sends ENTRY and EXIT, plus TP1/TP2 milestones by default. Choose JSON or readable text. Payloads include symbol, timeframe and event; entry messages include grade, strength, plan and supplied quantity.
These are indicator events, not broker fill confirmations. The strategy twin has no matching alert configuration.
What does each setting change?
Indicator defaults below. Change one input at a time.
Trade
Choose direction, trigger and grading method. Daily entries are a maximum. Quantity scales paper totals and alert data; it does not size broker orders or recommend current exchange lots.
- Direction
- Both
- Mode
- Intraday
- Trigger
- Pullback
- Minimum grade
- B
- Grade from
- Setup count
- Learned A threshold
- 80
- Learned B threshold
- 65
- Maximum entries per day
- 2
- Cooldown after exit
- 6 candles
- Paper P&L / alert quantity
- 75
Strength model
Turn learning on, control its adjustment speed and limit how long a shadow example can remain unresolved.
- Learn on this chart
- On
- Learning rate
- 0.02
- Shadow timeout
- 30 candles
Options mapping
Format call/put labels on an index or futures chart. These controls do not read an option chain or place orders.
- Options mapping
- Off
- Strike spacing
- 50
- Strike offset
- 0
- Expiry label
- Blank
Session
Both windows use the chosen timezone and candle opening times. Intraday mode uses square-off; Positional still respects the entry window.
- Timezone
- Asia/Kolkata
- Entry window
- 09:30–14:45
- Square-off window
- 15:00–15:15
Risk levels
Structure or ATR stop; targets in risk multiples. Milestones can move the stop. Optional exits can finish a trade before its final target.
- Stop-loss
- Structure
- ATR multiple: stop / trail / cap
- 1.5
- TP1
- 1R
- TP2
- 2R
- TP3 · final
- 3R
- TP1 → stop to entry
- On
- TP2 → stop to TP1, then trail
- On
- Exit on Supertrend flip
- On
- Momentum-loss exit
- RSI vs average
- Time stop before TP1
- 0 · off
- Exit / reverse on opposite signal
- On
Filters
Chase limits block values strictly above the threshold. Touch controls define how near and how recent the return to the average must be.
- Maximum candle range
- ATR × 2.5
- Maximum stretch from fast EMA
- ATR × 2
- Maximum three-close range
- ATR × 3
- Touch tolerance
- ATR × 0.3
- Maximum candles since touch
- 3
Engine
EMA averages emphasize recent prices. RSI measures momentum. Supertrend tracks direction using ATR, a measure of recent price movement including gaps.
- Fast EMA length
- 20
- Slow EMA length
- 50
- RSI length
- 14
- RSI average length
- 14
- Supertrend ATR length
- 10
- Supertrend factor
- 3
- Swing pivot length
- 5
- ATR length
- 14
- Higher timeframe: context only
- 15 minutes
Style
Minimal, Clean, Pro or Full changes the drawings. Themes: Aurora Neon, Royal Gold or Colorblind-safe.
- View preset
- Clean
- Theme
- Aurora Neon
- EMA trend cloud
- On
- Risk / reward zones
- On
- Color candles by strength
- Off
Dashboard
Show, place and resize the readings panel.
- Show dashboard
- On
- Position
- Top Right
- Size
- Medium
- Row stripes
- On
Co-Pilot
Brief gives a headline. Standard adds reasons and a plan. Detailed adds context and limitations. Off hides it.
- Narrative depth
- Standard
- Position
- Bottom Left
- Size
- Auto
- Line width
- 64 characters
Alerts
Choose JSON or readable text, and whether the dynamic alert also sends first and second target milestones.
- JSON payload
- On
- TP1 / TP2 milestone messages
- On
The twin shares Trade except quantity, Strength, Session, Risk, Filters and Engine inputs. It has no options, panel or alert settings.
- Show EMAs + active stop / target
- On
How do I measure it on my own chart?
- Keep defaults and watch a full session, including skipped candidates.
- Load the twin on a second chart. Set realistic quantity, fees and slippage in its properties.
- Compare one change at a time, then check a separate period. Include losing examples.
Twin header defaults: capital 5,000,000, quantity 75, cash fee 20 per order and one tick of slippage. They are code defaults, not current broker terms. Its fill emulator can differ from the indicator’s stop-first paper record.

What did our measurements show?
We measured one index chart with real costs on. Counting all eight factors, using learned strength as the gate and a stricter momentum exit made that sample worse. The five-factor setup count remained the default.
One symbol, one window, settings chosen while viewing that chart: in-sample evidence. The lab notes include failures and context. They do not establish the next trade’s outcome.
Read the lab notesWhat can change the reading?
Entries commit on confirmed candles. Live readings can move before the close. Changing history, feed, timeframe or inputs can recalculate results. Pivots need five later candles by default; higher-timeframe context uses its last closed candle.
Default entry times are 09:30 inclusive to 14:45 exclusive, Asia/Kolkata, based on candle opening times. Intraday square-off happens on the first closed candle whose opening time falls inside 15:00–15:15.
The tool sees chart bars, not news, option Greeks, order flow or actual fills. Its paper record omits costs. It does not predict prices. This is research tooling, not financial advice.
Every change, dated
What shipped and what changed in Signal Desk, newest first. Ask for the next change below; what gets built lands here with a date, and on the lab queue on the front page.
Signal Desk v1.2.0 Shipped
Checks entry quality, daily limits and whether you're chasing price.
Fewer, stronger calls, no chase entry Request Signal Desk
The ask Signal Desk was built from. Shipped as v1.2.0 with the grade, the chase veto and the strength score.
- Received
- Building
- Done
“Fewer signals, only the strong ones — one or two alerts a day would be fine. And stop the trade where a big candle closes, I enter, and it reverses.”
Measured before it shipped Lab note Signal Desk
Run with real costs on. The three smarter versions of the gate lost to the plain count, so the plain count is the default.
Something to change in Signal Desk?
A small friction or a big idea. Tell us what would make this tool work better for you.
Requests for a new tool go on the same queue: see what is asked for and what is being built.