//@version=6 // Signal Forge, part 3: stop, targets, dashboard. By Jayadev Rana. indicator("Signal Forge", overlay = true) // 1. Inputs: how far the trail sits, and the trend baseline atrLen = input.int(10, "ATR length", minval = 1) atrMult = input.float(3.0, "ATR multiplier", minval = 0.5, step = 0.1) emaLen = input.int(200, "Trend EMA length", minval = 1) useEma = input.bool(true, "Only signal with the EMA trend") bullCol = input.color(#26a69a, "Bull colour") bearCol = input.color(#ef5350, "Bear colour") // 2. Volatility and trend atr = ta.atr(atrLen) ema = ta.ema(close, emaLen) // 3. The trail: it only ever moves in the trend's favour longStop = close - atrMult * atr shortStop = close + atrMult * atr var float trail = na var int dir = 1 if na(trail) trail := longStop else if dir == 1 trail := math.max(trail, longStop) if close < trail dir := -1 trail := shortStop else trail := math.min(trail, shortStop) if close > trail dir := 1 trail := longStop // 4. Draw the trail by direction, and shade it to price upLine = dir == 1 ? trail : na dnLine = dir == -1 ? trail : na pMid = plot(hl2, "Mid", display = display.none) pUp = plot(upLine, "Bull trail", bullCol, 2, plot.style_linebr) pDn = plot(dnLine, "Bear trail", bearCol, 2, plot.style_linebr) fill(pMid, pUp, color.new(bullCol, 85), "Bull zone") fill(pMid, pDn, color.new(bearCol, 85), "Bear zone") plot(ema, "Trend EMA", color.orange, 2) // 5. Candles: coloured when trail and EMA agree, grey when not bullOk = dir == 1 and (not useEma or close > ema) bearOk = dir == -1 and (not useEma or close < ema) barcolor(bullOk ? bullCol : bearOk ? bearCol : color.gray) // 6. Raw signals: a flip, with the trend, on a CLOSED candle only flipUp = dir == 1 and dir[1] == -1 flipDown = dir == -1 and dir[1] == 1 rawBuy = flipUp and bullOk and barstate.isconfirmed rawSell = flipDown and bearOk and barstate.isconfirmed // 7. Filter switches and settings useChop = input.bool(true, "Chop filter (ADX)", group = "Filters") adxLen = input.int(14, "ADX length", minval = 1, group = "Filters") adxMin = input.float(20, "Minimum ADX", minval = 0, group = "Filters") useVol = input.bool(true, "Volume filter", group = "Filters") volLen = input.int(20, "Volume average length", group = "Filters") volMult = input.float(1.0, "Volume multiple", step = 0.1, group = "Filters") useHtf = input.bool(true, "Higher timeframe filter", group = "Filters") htfTf = input.timeframe("240", "Higher timeframe", group = "Filters") htfLen = input.int(50, "Higher timeframe EMA", group = "Filters") // 8. Chop filter: no signal when ADX says there is no trend [diPlus, diMinus, adx] = ta.dmi(adxLen, adxLen) chopOk = not useChop or adx >= adxMin // 9. Volume filter: the flip candle must beat average volume volAvg = ta.sma(volume, volLen) volOk = not useVol or volume > volAvg * volMult // 10. Higher timeframe: last CLOSED candle only, never the live one [htfC, htfE] = request.security(syminfo.tickerid, htfTf, [close[1], ta.ema(close, htfLen)[1]], lookahead = barmerge.lookahead_on) htfBuyOk = not useHtf or htfC > htfE htfSellOk = not useHtf or htfC < htfE // 11. Final signals: the raw signal AND every filter that is on buy = rawBuy and chopOk and volOk and htfBuyOk sell = rawSell and chopOk and volOk and htfSellOk plotshape(buy, "BUY", shape.labelup, location.belowbar, bullCol, text = "BUY", textcolor = color.white, size = size.small) plotshape(sell, "SELL", shape.labeldown, location.abovebar, bearCol, text = "SELL", textcolor = color.white, size = size.small) alertcondition(buy, "Signal Forge BUY", "BUY {{ticker}} at {{close}}") alertcondition(sell, "Signal Forge SELL", "SELL {{ticker}} at {{close}}") // 12. Ghosts: a small grey cross wherever a raw signal was killed killed = (rawBuy or rawSell) and not (buy or sell) plotshape(killed, "Filtered out", shape.xcross, location.abovebar, color.gray, size = size.tiny) // 13. Count on history: each kill is blamed on the FIRST filter var int nRaw = 0 var int nChop = 0 var int nVol = 0 var int nHtf = 0 var int nKept = 0 if rawBuy or rawSell htfOk = rawBuy ? htfBuyOk : htfSellOk nRaw += 1 if not chopOk nChop += 1 else if not volOk nVol += 1 else if not htfOk nHtf += 1 else nKept += 1 // 14. Risk: the stop is a multiple of ATR, targets are 1R, 2R, 3R slMult = input.float(1.5, "Stop, in ATRs", minval = 0.1, step = 0.1, group = "Trade") showLvl = input.bool(true, "Show stop and targets", group = "Trade") // 15. The trade tracker's memory var int side = 0 var int entryBar = na var float entry = na var float stop = na var float tp1 = na var float tp2 = na var float tp3 = na var int hits = 0 var int nTp1 = 0 var int nTp2 = 0 var int nTp3 = 0 var int nStop = 0 evt = "" // 16. Manage the open trade: stop first, then targets in order reached(price) => side == 1 ? high >= price : low <= price live = side != 0 and bar_index > entryBar if live if side == 1 ? low <= stop : high >= stop evt := hits == 0 ? "STOP" : "BREAKEVEN" nStop += hits == 0 ? 1 : 0 side := 0 else if hits < 1 and reached(tp1) hits := 1, nTp1 += 1, evt := "TP1", stop := entry if hits < 2 and reached(tp2) hits := 2, nTp2 += 1, evt := "TP2" if hits < 3 and reached(tp3) hits := 3, nTp3 += 1, evt := "TP3", side := 0 // 17. A new signal opens a new trade at the close of its candle if buy or sell risk = slMult * atr side := buy ? 1 : -1 entryBar := bar_index entry := close stop := close - side * risk tp1 := close + side * risk tp2 := close + side * 2 * risk tp3 := close + side * 3 * risk hits := 0 // 18. Draw the levels only while a trade is live lvl(x) => showLvl and live ? x : na plot(lvl(entry), "Entry", color.gray, 1, plot.style_linebr) plot(lvl(stop), "Stop", bearCol, 2, plot.style_linebr) plot(lvl(tp1), "TP1", color.new(bullCol, 40), 1, plot.style_linebr) plot(lvl(tp2), "TP2", color.new(bullCol, 20), 1, plot.style_linebr) plot(lvl(tp3), "TP3", bullCol, 2, plot.style_linebr) // 19. The dashboard: live state on top, history underneath var table tb = table.new(position.top_right, 2, 11, bgcolor = color.new(#131722, 5), border_color = #434651, border_width = 1) row(r, txt, val, col) => table.cell(tb, 0, r, txt, text_color = color.silver, text_halign = text.align_left, text_size = size.normal) table.cell(tb, 1, r, val, text_color = col, text_size = size.normal) num(x) => str.tostring(x) if barstate.islast pos = side == 1 ? "LONG" : side == -1 ? "SHORT" : "FLAT" trend = bullOk ? "BULL" : bearOk ? "BEAR" : "MIXED" row(0, "Signal Forge", trend, bullOk ? bullCol : bearCol) row(1, "Position", pos, side == 0 ? color.silver : color.white) row(2, "Raw signals", num(nRaw), color.white) row(3, "Removed by filters", num(nChop + nVol + nHtf), color.orange) row(4, "Signals kept", num(nKept), color.white) row(5, "Reached TP1", num(nTp1), bullCol) row(6, "Reached TP2", num(nTp2), bullCol) row(7, "Reached TP3", num(nTp3), bullCol) row(8, "Stopped before TP1", num(nStop), bearCol) row(9, "Stop now", side == 0 ? "-" : num(stop), color.silver) row(10, "Next target", side == 0 ? "-" : num(hits == 0 ? tp1 : hits == 1 ? tp2 : tp3), color.silver) // 20. One dynamic alert, as JSON, only when a candle has closed jStr(k, v) => '"' + k + '":"' + v + '"' jNum(k, v) => '"' + k + '":' + str.tostring(v, format.mintick) if buy or sell or evt != "" msg = "{" + jStr("symbol", syminfo.ticker) msg += "," + jStr("signal", buy ? "BUY" : sell ? "SELL" : "NONE") msg += "," + jStr("event", evt) msg += "," + jNum("entry", entry) + "," + jNum("stop", stop) msg += "," + jNum("tp1", tp1) + "," + jNum("tp2", tp2) msg += "," + jNum("tp3", tp3) + "}" alert(msg, alert.freq_once_per_bar_close)