//@version=6 strategy("XAUUSD PRIMO V5", 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)" groupDaily = "Daily PnL Kill-Switch" // ===== R:R Inputs ===== useRR = input.bool(true, "Use Risk:Reward for TP", group=groupATR) rr = input.float(0.3, "Risk:Reward (TP = RR * risk)", step=0.1, group=groupATR) // ===== Inputs ===== channelLen = input.int(20, "Channel Length", group=groupSig) lrLen = input.int(50, "LR Filter Length", group=groupSig) atrLen = input.int(9, "ATR Length", group=groupATR) // ---- Auto-normalize SL by USD risk (ใหม่) ---- useAutoSL = input.bool(true, "Auto adjust SL by $ risk", group=groupATR) targetRiskUSD = input.float(600.0, "Target SL in USD per trade", step=0.5, group=groupATR) // (จะคำนวณค่าอัตโนมัติหลังจากรู้ ATR ด้านล่าง) // Trailing useTrailing= input.bool(true, "Use Trailing Stop (ATR-based)", group=groupTrail) trailMult = input.float(3.0, "ATR x for Trailing Step", step=0.1, group=groupTrail) // Safety maxAdverseATR = input.float(3.5, "Force Close if Adverse > X*ATR", step=0.1, group=groupSafe) maxBarsHold = input.int(400, "Time Stop: Max Bars In Trade", group=groupSafe) beATR = input.float(4.5, "Breakeven when Gain >= X*ATR", step=0.1, group=groupSafe) useReverseClose = input.bool(false, "Use ReverseClose", group=groupSafe) // Quick exits takeATR = input.float(0.3, "Quick TP (ATRx)", step=0.1, group=groupQuick) stopATR = input.float(0, "Quick SL (ATRx)", step=0.1, group=groupQuick) // Exec 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) // ===== Daily PnL Kill-Switch ===== enableDailyStop = input.bool(true, "Enable Daily Profit Target", group=groupDaily) dailyTargetUSD = input.float(1500, "Daily Target (USD)", minval=1000, maxval=20000, step=50, group=groupDaily) includeOpenInDaily = input.bool(false, "Include open PnL in daily calc", group=groupDaily) closeOnDailyHit = input.bool(true, "Close position when hit", group=groupDaily) // ===== Auto / USD ===== keyInp = input.string("MY_SECRET_911", "Webhook Key", group=groupAuto) lotInp = input.float(0.50, "Lot to send", step=0.01, group=groupAuto) devInp = input.int(800, "Deviation (points)", group=groupAuto) // NOTE (XAUUSD): ส่วนใหญ่ 1 จุด * 1 lot ≈ $100 (โบรก MT5 มาตรฐาน) usdPerPointPerLot = input.float(100.0, "USD per 1 point per 1 lot (XAU)", step=1.0, group=groupUSD) // ===== Alerts throttle ===== 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) // ===== Alert Anti-Spam (Arm / Retry / Hysteresis) ===== rearmATR = input.float(0.8, "Re-arm when price moves away ≥ X*ATR", step=0.1, group=groupAlert) rearmBars = input.int(5, "Re-arm after N bars (time based)", minval=0, group=groupAlert) retryEveryBars = input.int(12, "Retry every N bars if still valid", minval=1, group=groupAlert) maxRetriesSide = input.int(2, "Max retries per side per setup", minval=0, group=groupAlert) // ===== Webhook SL/TP (ATRx) ===== sendLevels = input.bool(true, "Send TP/SL with OPEN alerts", group=groupSend) sendSLx = input.float(3.5, "SL = X * ATR (for webhook)", step=0.1, group=groupSend) sendTPx = input.float(1.5, "TP = X * ATR (if not using RR)", step=0.1, group=groupSend) maxWebhookATRx = input.float(0.8, "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) => 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 // ===== Arm/Disarm Anti-Spam STATE ===== var string _armedSide = na // "L"|"S"|na var float _armedLevel = na var int _armedBar = na var int _lastSendBar = na var int _retriesLong = 0 var int _retriesShort = 0 // ===== Anti-Spam helpers (no global writes inside) ===== f_far_enough(float level) => na(level) ? true : math.abs(close - level) >= (rearmATR * atr) f_can_send_for(string side, float level) => baseOk = f_can_alert() notArmed = na(_armedSide) sameSide = _armedSide == side timeRearmOk = na(_armedBar) or (bar_index - _armedBar >= rearmBars) distRearmOk = f_far_enough(_armedLevel) retryWindowOk = na(_lastSendBar) or (bar_index - _lastSendBar >= retryEveryBars) retriesOk = side == "L" ? (_retriesLong < maxRetriesSide) : (_retriesShort < maxRetriesSide) gateOk = notArmed or (sameSide and timeRearmOk and (distRearmOk or retryWindowOk) and retriesOk) baseOk and gateOk // ===== 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" // ===== Auto SL calc (หลังรู้ ATR) ===== atr_usd_unit = atr * usdPerPointPerLot * lotInp sendSLx_auto = (not na(atr_usd_unit) and atr_usd_unit > 0) ? (targetRiskUSD / atr_usd_unit) : sendSLx slx = useAutoSL ? sendSLx_auto : sendSLx // Quick SL ควรไม่ต่ำกว่า SL หลัก stopATR_eff = math.max(stopATR, slx) // ===== (แทนชุดที่ 1) Seed SL/TP ให้ตรงสูตร webhook (ใช้ slx อัตโนมัติ) ===== 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 riskDist = entryATR * slx tpDistRR = riskDist * rr tpDistFix = entryATR * sendTPx tpDistRaw = useRR ? tpDistRR : tpDistFix capDist = entryATR * maxWebhookATRx tpDist = math.min(tpDistRaw, capDist) if strategy.position_size > 0 currentSL := strategy.position_avg_price - riskDist currentTP := strategy.position_avg_price + tpDist else currentSL := strategy.position_avg_price + riskDist currentTP := strategy.position_avg_price - tpDist 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 and useReverseClose 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 // ใช้ stopATR_eff (กันโดนกัดก่อน SL หลัก) if (stopATR > 0) and strategy.position_size != 0 and (-gainATR) >= stopATR_eff 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) // ====== DAILY PnL KILL-SWITCH ====== var float dayStartNet = na var int dayBucket = na var bool dailyStopped = false curDay = time("D") if na(dayBucket) or curDay != dayBucket dayBucket := curDay dayStartNet := strategy.netprofit dailyStopped := false todayClosedPnl = strategy.netprofit - nz(dayStartNet, strategy.netprofit) todayOpenPnl = includeOpenInDaily ? strategy.openprofit : 0.0 todayPnlTotal = todayClosedPnl + todayOpenPnl hitDaily = enableDailyStop and todayPnlTotal >= dailyTargetUSD if hitDaily and not dailyStopped dailyStopped := true strategy.cancel_all() if closeOnDailyHit and strategy.position_size != 0 strategy.close(strategy.position_size > 0 ? "L" : "S", comment="DailyTargetHit") if canSendCloseNow() and f_can_alert() alert(f_json("CLOSE", na, na), 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 // ===== OPEN (with webhook TP/SL planned from entry stop price) ===== allowRealtimeOpen = barstate.isrealtime okBarConfirm = barstate.isconfirmed or allowRealtimeOpen // *** บล็อกการเปิด เมื่อ dailyStopped = true *** canOpenNow = (strategy.position_size == 0) and (notCooldown or closedThisBar or not na(pendingSide)) and canActThisBar and okBarConfirm and not dailyStopped plannedLong = na(upBound) ? na : (upBound + syminfo.mintick) plannedShort = na(downBound) ? na : (downBound - syminfo.mintick) // ระยะ webhook (ให้ตรงกับ seed ด้านบน) — ใช้ slx riskDist_w = atr * slx tpDistRR_w = riskDist_w * rr tpDistFix_w = atr * sendTPx tpDistRaw_w = useRR ? tpDistRR_w : tpDistFix_w capDist_w = atr * maxWebhookATRx tpDist_w = math.min(tpDistRaw_w, capDist_w) // SL/TP webhook + ปัด tick wl_sl = na(plannedLong) ? na : round_tick(plannedLong - riskDist_w) ws_sl = na(plannedShort) ? na : round_tick(plannedShort + riskDist_w) wl_tp = na(plannedLong) ? na : round_tick(plannedLong + tpDist_w) ws_tp = na(plannedShort) ? na : round_tick(plannedShort - tpDist_w) // ===== Anti-Spam: reset arms on regime change / close / level drift ===== justClosed = (not wasFlat) and strategy.position_size == 0 and strategy.position_size[1] != 0 wasFlat := (strategy.position_size == 0) if (isBullishTrend and _armedSide == "S") or (isBearishTrend and _armedSide == "L") // reset (inline) _armedSide := na _armedLevel := na _armedBar := na _lastSendBar := na _retriesLong := 0 _retriesShort:= 0 if justClosed _armedSide := na _armedLevel := na _armedBar := na _lastSendBar := na _retriesLong := 0 _retriesShort:= 0 if not na(_armedSide) curLevel = _armedSide == "L" ? plannedLong : plannedShort if f_far_enough(curLevel) _armedSide := na _armedLevel := na _armedBar := na _lastSendBar := na _retriesLong := 0 _retriesShort:= 0 // ===== Send OPEN with anti-spam gate ===== if canOpenNow strategy.cancel_all() if (pendingSide == "L") and isBullishTrend and not na(plannedLong) strategy.entry("L", strategy.long, stop=plannedLong) if not dailyStopped and f_can_send_for("L", plannedLong) alert(f_json("LONG", wl_sl, wl_tp), alert.freq_once_per_bar) if not na(timenow) __lastAlertMs := timenow __bucketCount += 1 __lastAlertBar := bar_index _armedSide := "L" _armedLevel := plannedLong _armedBar := bar_index _lastSendBar := bar_index _retriesLong := _retriesLong + 1 pendingSide := na lastActionBar := bar_index else if (pendingSide == "S") and isBearishTrend and not na(plannedShort) strategy.entry("S", strategy.short, stop=plannedShort) if not dailyStopped and f_can_send_for("S", plannedShort) alert(f_json("SHORT", ws_sl, ws_tp), alert.freq_once_per_bar) if not na(timenow) __lastAlertMs := timenow __bucketCount += 1 __lastAlertBar := bar_index _armedSide := "S" _armedLevel := plannedShort _armedBar := bar_index _lastSendBar := bar_index _retriesShort:= _retriesShort + 1 pendingSide := na lastActionBar := bar_index else if isBullishTrend and not na(plannedLong) strategy.entry("L", strategy.long, stop=plannedLong) if not dailyStopped and f_can_send_for("L", plannedLong) alert(f_json("LONG", wl_sl, wl_tp), alert.freq_once_per_bar) if not na(timenow) __lastAlertMs := timenow __bucketCount += 1 __lastAlertBar := bar_index _armedSide := "L" _armedLevel := plannedLong _armedBar := bar_index _lastSendBar := bar_index _retriesLong := _retriesLong + 1 lastActionBar := bar_index else if isBearishTrend and not na(plannedShort) strategy.entry("S", strategy.short, stop=plannedShort) if not dailyStopped and f_can_send_for("S", plannedShort) alert(f_json("SHORT", ws_sl, ws_tp), alert.freq_once_per_bar) if not na(timenow) __lastAlertMs := timenow __bucketCount += 1 __lastAlertBar := bar_index _armedSide := "S" _armedLevel := plannedShort _armedBar := bar_index _lastSendBar := bar_index _retriesShort:= _retriesShort + 1 lastActionBar := bar_index // ===== Transition: position -> flat (mark CLOSE once) ===== justClosed2 = (not wasFlat) and strategy.position_size == 0 and strategy.position_size[1] != 0 wasFlat := (strategy.position_size == 0) if justClosed2 closeSignal := true // ===== SINGLE dispatch for CLOSE ===== if closeSignal and canSendCloseNow() if f_can_alert() alert(f_json("CLOSE", na, na), 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 // ===== If position filled -> reset arm (กันแขนงค้าง) ===== if strategy.position_size != 0 and not na(_armedSide) _armedSide := na _armedLevel := na _armedBar := na _lastSendBar := na _retriesLong := 0 _retriesShort:= 0 // ===== 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_eff * 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_eff + (dir > 0 ? (close - avgPrice)/atr : (avgPrice - close)/atr)) * atr_usd) : na // === HUD EXTRAS: แสดง Risk/TP/SL-ATR/ATR$ === // หน่วยเงินต่อ 1 ATR (ตาม lot) // ตัวคูณ SL หลัก (ATR x) จาก targetRiskUSD slx_eff = useAutoSL ? sendSLx_auto : sendSLx // เงิน TP หลัก (ไม่ใช่ Quick) ตาม RR หรือ TPx tp_usd_main = useRR ? (targetRiskUSD * rr) : (math.min(sendTPx, maxWebhookATRx) * atr_usd_unit) quick_tp_usd = (takeATR > 0) ? takeATR * atr_usd : 0.0 quick_sl_usd = (stopATR > 0) ? stopATR * atr_usd : 0.0 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 += "\nRisk (SL) = $" + str.tostring(targetRiskUSD, format.mintick) + " | RR " + str.tostring(rr, format.mintick) + " ⇒ TP ≈ $" + str.tostring(tp_usd_main, format.mintick) hud += "\nSL ≈ " + str.tostring(slx_eff, format.mintick) + " ATR | ATR$ ≈ $" + str.tostring(atr_usd_unit, format.mintick) hud += "\nQuick TP/SL: +" + str.tostring(quick_tp_usd, format.mintick) + " / -" + str.tostring(quick_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 += "\nDaily PnL: $" + str.tostring(todayPnlTotal, format.mintick) + " / Target $" + str.tostring(dailyTargetUSD, format.mintick) hud += "\nDailyStopped: " + (dailyStopped ? "YES" : "NO") hud += "\nARM: " + (na(_armedSide) ? "none" : (_armedSide == "L" ? "L" : "S")) + " @ " + (na(_armedLevel) ? "na" : str.tostring(_armedLevel, format.mintick)) _hud := label.new(bar_index, high, hud, yloc=yloc.abovebar, style=label.style_label_down)