//@version=6 strategy("BTCUSD AI 911 V3 PRIMO", overlay=true, initial_capital=20000, default_qty_type=strategy.percent_of_equity, default_qty_value=10, pyramiding=0, calc_on_order_fills=true, calc_on_every_tick=true, max_labels_count=500, max_lines_count=500) // ===== Groups ===== groupSig = "Signals" groupATR = "ATR / Risk" groupTrail = "Trailing" groupSafe = "Safety Exits" groupQuick = "Quick ATR Exits" groupExec = "Execution Control" groupAuto = "Auto-Trade (Webhook JSON)" groupUSD = "USD Display" groupAlert = "Alert Throttle" groupSend = "Webhook SL/TP (ATRx)" // ===== R:R Inputs ===== useRR = input.bool(true, "Use Risk:Reward for TP", group=groupATR) rr = input.float(1.5, "Risk:Reward (TP = RR * risk)", step=0.1, group=groupATR) // ===== Inputs ===== channelLen = input.int(5, "Channel Length", group=groupSig) lrLen = input.int(90, "LR Filter Length", group=groupSig) atrLen = input.int(9, "ATR Length", group=groupATR) atrMultSL = input.float(0.6, "ATR x for Stop Loss (pos mgmt)", step=0.1, group=groupATR) atrMultTP = input.float(1.0, "ATR x for Take Profit (pos mgmt)", step=0.1, group=groupATR) useTrailing= input.bool(true, "Use Trailing Stop (ATR-based)", group=groupTrail) trailMult = input.float(0.8, "ATR x for Trailing Step", step=0.1, group=groupTrail) maxAdverseATR = input.float(2.5, "Force Close if Adverse > X*ATR", step=0.1, group=groupSafe) maxBarsHold = input.int(180, "Time Stop: Max Bars In Trade", group=groupSafe) beATR = input.float(0.8, "Breakeven when Gain >= X*ATR", step=0.1, group=groupSafe) takeATR = input.float(0.4, "Quick TP (ATRx)", step=0.1, group=groupQuick) stopATR = input.float(1.0, "Quick SL (ATRx)", step=0.1, group=groupQuick) cooldownBars = input.int(0, "Cooldown bars after CLOSE (0 = flip same bar)", minval=0, group=groupExec) oneActionPerBar = input.bool(true, "One action per bar (alerts)", group=groupExec) coolCloseSec = input.int(3, "HARD cooldown for CLOSE (seconds)", minval=0, group=groupExec) keyInp = input.string("MY_SECRET_911", "Webhook Key", group=groupAuto) lotInp = input.float(2.00, "Lot to send", step=0.01, group=groupAuto) devInp = input.int(1000, "Deviation (points)", group=groupAuto) usdPerPointPerLot = input.float(1.0, "USD per 1 point per 1 lot", step=0.1, group=groupUSD) maxAlertsPer3min = input.int(12, "Max alerts / 3 min (<=15)", minval=1, maxval=15, group=groupAlert) minAlertSec = input.int(5, "Min seconds between alerts", minval=0, group=groupAlert) onePerBarLimit = input.bool(true, "At most 1 alert per bar", group=groupAlert) // ===== Webhook SL/TP (ATRx) ===== sendLevels = input.bool(true, "Send TP/SL with OPEN alerts", group=groupSend) // เดิมตั้ง SL = 8*ATR ทำให้พอคูณ RR แล้ว TP ไกลมาก -> ปรับดีฟอลต์ให้อยู่ใกล้มือ sendSLx = input.float(2.0, "SL = X * ATR (for webhook)", step=0.1, group=groupSend) sendTPx = input.float(0.2, "TP = X * ATR (if not using RR)", step=0.1, group=groupSend) // ใส่ฝาจำกัดระยะ TP สูงสุดต่อออเดอร์ maxWebhookATRx = input.float(3.0, "CAP max distance = X * ATR", step=0.1, group=groupSend) // ===== Signals ===== upBound = ta.highest(high, channelLen)[1] downBound = ta.lowest(low, channelLen)[1] lrLine = ta.linreg(close, lrLen, 0) atr = ta.atr(atrLen) isBullishTrend = lrLine > lrLine[1] and close > lrLine isBearishTrend = lrLine < lrLine[1] and close < lrLine plot(lrLine, "LR Trend", color=color.rgb(0,150,255), linewidth=2) plot(upBound, "Upper", color=color.new(color.lime, 0)) plot(downBound,"Lower", color=color.new(color.red, 0)) // ===== Helpers ===== f_clamp_long(float sl, float tp) => _sl = math.min(sl, close) _tp = math.max(tp, close) _sl := _sl >= _tp ? _tp - atr : _sl [_sl, _tp] f_clamp_short(float sl, float tp) => _sl = math.max(sl, close) _tp = math.min(tp, close) _tp := _tp >= _sl ? _sl - atr : _tp [_sl, _tp] // ปัดราคาให้เข้ากับ tick round_tick(x) => t = syminfo.mintick math.round(x / t) * t // ===== JSON builder (typed) ===== f_json(string action, float slp, float tpp) => // ป้องกันค่า NA -> ส่ง "null" sl_str = (sendLevels) ? (na(slp) ? "null" : str.tostring(slp, format.mintick)) : "null" tp_str = (sendLevels) ? (na(tpp) ? "null" : str.tostring(tpp, format.mintick)) : "null" _s = '{"key":"' + keyInp + '","attach":true' _s += ',"symbol":"' + syminfo.ticker + '"' _s += ',"action":"' + action + '"' _s += ',"lot":' + str.tostring(lotInp) _s += ',"deviation":' + str.tostring(devInp) _s += ',"sl_price":' + sl_str _s += ',"tp_price":' + tp_str _s += '}' _s // ===== Alert throttle STATE ===== var int __bucket3m = na var int __bucketCount = 0 var int __lastAlertMs = na var int __lastAlertBar = na if barstate.isrealtime _now = timenow _bk = na(_now) ? __bucket3m : int(math.floor(_now / 180000)) if na(__bucket3m) or (not na(_bk) and _bk != __bucket3m) __bucket3m := _bk __bucketCount := 0 f_can_alert() => if not barstate.isrealtime true else _now = timenow _coolOk = na(_now) or na(__lastAlertMs) or (_now - __lastAlertMs) >= minAlertSec * 1000 _quotaOk = __bucketCount < maxAlertsPer3min _barOk = not onePerBarLimit or na(__lastAlertBar) or __lastAlertBar != bar_index _coolOk and _quotaOk and _barOk // ===== Trade State ===== var int lastCloseBar = na var bool sentCloseThisFlat = false var bool wasFlat = strategy.position_size == 0 var int lastCloseMs = na flatNow = strategy.position_size == 0 canHardCooldownClose() => na(timenow) or na(lastCloseMs) or (timenow - lastCloseMs) >= coolCloseSec * 1000 canSendCloseNow() => barstate.isrealtime and canHardCooldownClose() and (na(lastCloseBar) or lastCloseBar != bar_index) and not sentCloseThisFlat // ===== Runtime ===== var float entryATR = na var float currentSL = na var float currentTP = na var int lastActionBar = na var string pendingSide = na // "L" or "S" // (Re)seed SL/TP on new position or when missing — useRR newPos = strategy.position_size != 0 and strategy.position_size[1] == 0 lostLevels = strategy.position_size != 0 and (na(currentSL) or na(currentTP)) if (newPos or lostLevels) entryATR := atr risk = entryATR * atrMultSL if strategy.position_size > 0 currentSL := strategy.position_avg_price - risk currentTP := useRR ? (strategy.position_avg_price + risk * rr) : (strategy.position_avg_price + entryATR * atrMultTP) else currentSL := strategy.position_avg_price + risk currentTP := useRR ? (strategy.position_avg_price - risk * rr) : (strategy.position_avg_price - entryATR * atrMultTP) if newPos sentCloseThisFlat := false canActThisBar = not oneActionPerBar or na(lastActionBar) or bar_index > lastActionBar notCooldown = na(lastActionBar) or (bar_index - lastActionBar >= cooldownBars) // ===== Close conditions (mark & flip) ===== closeSignal = false closedThisBar = false if canActThisBar if strategy.position_size > 0 and isBearishTrend strategy.close("L", comment="ReverseClose") closeSignal := true closedThisBar := true pendingSide := "S" lastActionBar := bar_index else if strategy.position_size < 0 and isBullishTrend strategy.close("S", comment="ReverseClose") closeSignal := true closedThisBar := true pendingSide := "L" lastActionBar := bar_index adverse = strategy.position_size > 0 ? (strategy.position_avg_price - close) / atr : strategy.position_size < 0 ? (close - strategy.position_avg_price) / atr : 0.0 barsInPos = ta.barssince(strategy.position_size == 0) if strategy.position_size > 0 and adverse > maxAdverseATR strategy.close("L", comment="ATR-Kill") closeSignal := true closedThisBar := true lastActionBar := bar_index if strategy.position_size < 0 and adverse > maxAdverseATR strategy.close("S", comment="ATR-Kill") closeSignal := true closedThisBar := true lastActionBar := bar_index if strategy.position_size != 0 and barsInPos >= maxBarsHold strategy.close(strategy.position_size > 0 ? "L" : "S", comment="TimeStop") closeSignal := true closedThisBar := true lastActionBar := bar_index gain = strategy.position_size > 0 ? (close - strategy.position_avg_price) / atr : strategy.position_size < 0 ? (strategy.position_avg_price - close) / atr : 0.0 // Breakeven bump if strategy.position_size > 0 and gain >= beATR currentSL := math.max(currentSL, strategy.position_avg_price) if strategy.position_size < 0 and gain >= beATR currentSL := math.min(currentSL, strategy.position_avg_price) // Quick exits by ATR gainATR = strategy.position_size > 0 ? (close - strategy.position_avg_price) / atr : strategy.position_size < 0 ? (strategy.position_avg_price - close) / atr : 0.0 if strategy.position_size != 0 and gainATR >= takeATR strategy.close(strategy.position_size > 0 ? "L" : "S", comment="QuickTP") closeSignal := true closedThisBar := true lastActionBar := bar_index if strategy.position_size != 0 and (-gainATR) >= stopATR strategy.close(strategy.position_size > 0 ? "L" : "S", comment="QuickSL") closeSignal := true closedThisBar := true lastActionBar := bar_index // ===== Manage exits (SL/TP + trailing) ===== if strategy.position_size > 0 if useTrailing and not na(currentSL) trailStop = close - (atr * trailMult) currentSL := math.max(currentSL, trailStop) [lsl, ltp] = f_clamp_long(currentSL, currentTP) strategy.exit("XL_Exit", from_entry="L", stop=lsl, limit=ltp) else if strategy.position_size < 0 if useTrailing and not na(currentSL) trailStop = close + (atr * trailMult) currentSL := math.min(currentSL, trailStop) [ssl, stp] = f_clamp_short(currentSL, currentTP) strategy.exit("XS_Exit", from_entry="S", stop=ssl, limit=stp) // ===== OPEN (with webhook TP/SL planned from entry stop price) ===== allowRealtimeOpen = barstate.isrealtime okBarConfirm = barstate.isconfirmed or allowRealtimeOpen canOpenNow = (strategy.position_size == 0) and (notCooldown or closedThisBar or not na(pendingSide)) and canActThisBar and okBarConfirm // planned entry prices (stop entries ที่ขอบ channel) plannedLong = na(upBound) ? na : (upBound + syminfo.mintick) plannedShort = na(downBound) ? na : (downBound - syminfo.mintick) // ---- ระยะความเสี่ยง/กำไรของ webhook (คุมด้วย RR + CAP) ---- riskDist = atr * sendSLx tpDistRR = riskDist * rr tpDistFix = atr * sendTPx tpDistRaw = useRR ? tpDistRR : tpDistFix capDist = atr * maxWebhookATRx tpDist = math.min(tpDistRaw, capDist) // ---- สร้างราคา SL/TP (และปัดเป็น tick) wl_sl = na(plannedLong) ? na : round_tick(plannedLong - riskDist) ws_sl = na(plannedShort) ? na : round_tick(plannedShort + riskDist) wl_tp = na(plannedLong) ? na : round_tick(plannedLong + tpDist) ws_tp = na(plannedShort) ? na : round_tick(plannedShort - tpDist) if canOpenNow strategy.cancel_all() if (pendingSide == "L") and isBullishTrend and not na(plannedLong) strategy.entry("L", strategy.long, stop=plannedLong) if f_can_alert() alert(f_json("LONG", wl_sl, wl_tp), alert.freq_once_per_bar) if not na(timenow) __lastAlertMs := timenow __bucketCount := __bucketCount + 1 __lastAlertBar := bar_index pendingSide := na lastActionBar := bar_index else if (pendingSide == "S") and isBearishTrend and not na(plannedShort) strategy.entry("S", strategy.short, stop=plannedShort) if f_can_alert() alert(f_json("SHORT", ws_sl, ws_tp), alert.freq_once_per_bar) if not na(timenow) __lastAlertMs := timenow __bucketCount := __bucketCount + 1 __lastAlertBar := bar_index pendingSide := na lastActionBar := bar_index else if isBullishTrend and not na(plannedLong) strategy.entry("L", strategy.long, stop=plannedLong) if f_can_alert() alert(f_json("LONG", wl_sl, wl_tp), alert.freq_once_per_bar) if not na(timenow) __lastAlertMs := timenow __bucketCount := __bucketCount + 1 __lastAlertBar := bar_index lastActionBar := bar_index else if isBearishTrend and not na(plannedShort) strategy.entry("S", strategy.short, stop=plannedShort) if f_can_alert() alert(f_json("SHORT", ws_sl, ws_tp), alert.freq_once_per_bar) if not na(timenow) __lastAlertMs := timenow __bucketCount := __bucketCount + 1 __lastAlertBar := bar_index lastActionBar := bar_index // ===== Transition: position -> flat (mark CLOSE once) ===== justClosed = (not wasFlat) and strategy.position_size == 0 and strategy.position_size[1] != 0 wasFlat := (strategy.position_size == 0) if justClosed closeSignal := true // ===== SINGLE dispatch for CLOSE ===== if closeSignal and canSendCloseNow() if f_can_alert() float naFloat = na alert(f_json("CLOSE", naFloat, naFloat), alert.freq_once_per_bar) if not na(timenow) __lastAlertMs := timenow __bucketCount := __bucketCount + 1 __lastAlertBar := bar_index lastCloseBar := bar_index sentCloseThisFlat := true if barstate.isrealtime and not na(timenow) lastCloseMs := timenow // ===== USD HUD ===== inPos = strategy.position_size != 0 dir = strategy.position_size > 0 ? 1.0 : strategy.position_size < 0 ? -1.0 : 0.0 avgPrice = strategy.position_avg_price pnl_points = inPos ? (close - avgPrice) * dir : 0.0 pnl_usd = pnl_points * usdPerPointPerLot * lotInp atr_usd = atr * usdPerPointPerLot * lotInp tp_usd = takeATR * atr_usd sl_usd = stopATR * atr_usd to_tp_usd = inPos ? math.max(0, (takeATR - (dir > 0 ? (close - avgPrice)/atr : (avgPrice - close)/atr)) * atr_usd) : na to_sl_usd = inPos ? math.max(0, (stopATR + (dir > 0 ? (close - avgPrice)/atr : (avgPrice - close)/atr)) * atr_usd) : na var label _hud = na if barstate.islast if not na(_hud) label.delete(_hud) dirTxt = strategy.position_size > 0 ? "LONG" : strategy.position_size < 0 ? "SHORT" : "FLAT" hud = "Dir: " + dirTxt + " | CooldownOK: " + (notCooldown ? "YES" : "NO") hud += "\nATR ≈ $" + str.tostring(atr_usd, format.mintick) hud += "\nQuick TP/SL: +" + str.tostring(tp_usd, format.mintick) + " / -" + str.tostring(sl_usd, format.mintick) hud += "\nPnL$ now: " + str.tostring(pnl_usd, format.mintick) if inPos hud += "\nTo TP: $" + str.tostring(to_tp_usd, format.mintick) + " To SL: $" + str.tostring(to_sl_usd, format.mintick) if not na(pendingSide) hud += "\nPending: " + pendingSide _hud := label.new(bar_index, high, hud, yloc=yloc.abovebar, style=label.style_label_down)