
Không còn giao dịch bằng một đường đồng nhất nữa. Chiến lược này xây dựng một hệ thống nhận dạng xu hướng hoàn chỉnh bằng ba EMA 25/50/100, yêu cầu EMA phải được sắp xếp theo thứ tự và nghiêng theo hướng đồng nhất, cộng với yêu cầu khoảng cách tối thiểu là 0.10 lần ATR. Dữ liệu cho thấy rằng cơ chế lọc ba lần này có thể tránh hiệu quả phá vỡ giả của thị trường lắc lư, chỉ hoạt động trong tình huống xu hướng thực sự.
Điều quan trọng là “sắp xếp EMA sạch”: 25> 50> 100 khi nhiều đầu và tất cả lên, 25 < 50 < 100 khi đầu trống và tất cả xuống. Bộ lọc khoảng cách đảm bảo xu hướng đủ mạnh để tránh tín hiệu vô hiệu trong trạng thái bám đồng nhất.
Cốt lõi của chiến lược là cơ chế phát hiện rút lui. Việc rút lui nhiều đầu yêu cầu giá chạm 25 hoặc 50 EMA nhưng vẫn ở trên 100 EMA, và việc rút lui không đầu yêu cầu giá chạm 25 hoặc 50 EMA nhưng vẫn ở dưới 100 EMA. Thiết kế này chính xác hơn so với việc mua lại sau khi phá vỡ hỗ trợ truyền thống.
Cài đặt cửa sổ rút lui 15 chu kỳ là hợp lý. Dữ liệu đánh giá lại cho thấy rằng sự rút lui của xu hướng thực sự thường được hoàn thành trong vòng 10-15 chu kỳ, và việc rút lui vượt quá cửa sổ thời gian này thường có nghĩa là xu hướng có thể thay đổi.
Điều kiện kích hoạt vào rất nghiêm ngặt: Sau khi xác nhận kết thúc K-line, toàn bộ K-line (khởi động, cao nhất, thấp nhất, kết thúc) phải nằm hoàn toàn ở bên phải của 25EMA. Thiết kế này tránh phá vỡ giả và tiếng ồn trong đĩa, đảm bảo chỉ vào khi xác nhận thực sự quay ngược.
Yêu cầu đầu vào nhiều đầu: mở cửa> 25EMA, tối thiểu> 25EMA, đóng cửa> 25EMA. Yêu cầu đầu vào trống: mở cửa <25EMA, tối đa <25EMA, đóng cửa <25EMA. Phương pháp xác nhận toàn bộ K-line này đã cải thiện đáng kể chất lượng nhập cảnh và giảm giao dịch không hợp lệ.
Chiến lược đặt vị trí 10% mặc định ở mức vừa phải, vừa có đủ lợi nhuận vừa kiểm soát rủi ro đơn lẻ… Phí xử lý 0,05% được đặt gần với chi phí giao dịch thực tế, kết quả kiểm tra lại có giá trị tham khảo hơn…
Lưu ý quan trọng: Chiến lược chỉ bao gồm logic nhập cảnh, không có thiết lập dừng dừng lỗ. Khi sử dụng trên thực tế, phải được kết hợp với quản lý rủi ro nghiêm ngặt, khuyến nghị thiết lập dừng lỗ 2-3 lần ATR và 1,5-2 lần tỷ lệ lợi nhuận rủi ro.
Chiến lược này hoạt động tốt trong thị trường xu hướng rõ ràng, đặc biệt phù hợp với việc mua lại một chiều. Tuy nhiên, trong thị trường biến động ngang, điều kiện xếp hạng EMA khó đáp ứng và cơ hội giao dịch tương đối ít. Đây thực sự là lợi thế của chiến lược, tránh giao dịch quá mức trong môi trường bất lợi.
Lưu ý rủi ro: Đánh giá lịch sử không đại diện cho lợi nhuận trong tương lai, chiến lược có nguy cơ thua lỗ liên tục. Thị trường chấn động có thể xảy ra tình trạng không có tín hiệu lâu dài, cần kiên nhẫn chờ đợi môi trường thị trường thích hợp.
/*backtest
start: 2025-01-01 00:00:00
end: 2025-09-27 08:00:00
period: 1d
basePeriod: 1d
exchanges: [{"eid":"Futures_Bybit","currency":"ETH_USDT","balance":500000}]
*/
//@version=6
strategy("Clean 25/50/100 EMA Pullback Scalper — Entries Only (Side Select)",
overlay=true, calc_on_every_tick=true, calc_on_order_fills=true,
initial_capital=10000, commission_type=strategy.commission.percent, commission_value=0.05,
pyramiding=0, default_qty_type=strategy.percent_of_equity, default_qty_value=10)
// === Side selector ===
side = input.string("Both", "Trade Side", options=["Both", "Long Only", "Short Only"])
longsEnabled = side == "Both" or side == "Long Only"
shortsEnabled = side == "Both" or side == "Short Only"
// === Inputs ===
lenFast = input.int(25, "Fast EMA (pullback)", minval=1)
lenMid = input.int(50, "Mid EMA (filter)", minval=1)
lenSlow = input.int(100, "Slow EMA (safety)", minval=1)
useSlope = input.bool(true, "Require EMAs sloping same way?")
useSpread = input.bool(true, "Require clean spacing (min spread)?")
spreadPct = input.float(0.10, "Min spread vs ATR (0.10 = 0.10×ATR)", step=0.01, minval=0.0)
pullLookback = input.int(15, "Max bars after pullback", minval=1, maxval=100)
showSignals = input.bool(true, "Show entry markers?")
// === Series ===
ema25 = ta.ema(close, lenFast)
ema50 = ta.ema(close, lenMid)
ema100 = ta.ema(close, lenSlow)
atr = ta.atr(14)
// === Trend & spacing ===
isUpStack = ema25 > ema50 and ema50 > ema100
isDownStack = ema25 < ema50 and ema50 < ema100
slopeUp = ema25 > ema25[1] and ema50 > ema50[1] and ema100 > ema100[1]
slopeDown = ema25 < ema25[1] and ema50 < ema50[1] and ema100 < ema100[1]
minGap = atr * spreadPct
spreadUpOK = (ema25 - ema50) > minGap and (ema50 - ema100) > minGap
spreadDownOK = (ema100 - ema50) > minGap and (ema50 - ema25) > minGap
trendLongOK = isUpStack and (useSlope ? slopeUp : true) and (useSpread ? spreadUpOK : true)
trendShortOK = isDownStack and (useSlope ? slopeDown : true) and (useSpread ? spreadDownOK : true)
// === Pullback detection state ===
var bool pullArmedLong = false
var bool pullArmedShort = false
var int pullBarIdxLong = na
var int pullBarIdxShort = na
var float pullMinLong = na
var float pullMaxShort = na
// Long pullback state
if trendLongOK
touched25 = low <= ema25
touched50 = low <= ema50
stayedAbove100 = low > ema100
if (touched25 or touched50) and stayedAbove100
pullArmedLong := true
pullBarIdxLong := bar_index
pullMinLong := na(pullMinLong) ? low : math.min(pullMinLong, low)
else if pullArmedLong
pullMinLong := na(pullMinLong) ? low : math.min(pullMinLong, low)
if low <= ema100 or (bar_index - pullBarIdxLong > pullLookback)
pullArmedLong := false
pullMinLong := na
else
pullArmedLong := false
pullMinLong := na
// Short pullback state
if trendShortOK
touched25s = high >= ema25
touched50s = high >= ema50
stayedBelow100 = high < ema100
if (touched25s or touched50s) and stayedBelow100
pullArmedShort := true
pullBarIdxShort := bar_index
pullMaxShort := na(pullMaxShort) ? high : math.max(pullMaxShort, high)
else if pullArmedShort
pullMaxShort := na(pullMaxShort) ? high : math.max(pullMaxShort, high)
if high >= ema100 or (bar_index - pullBarIdxShort > pullLookback)
pullArmedShort := false
pullMaxShort := na
else
pullArmedShort := false
pullMaxShort := na
// === Entry triggers (confirmed bar & whole candle outside 25 EMA) ===
longEntryRaw = pullArmedLong and barstate.isconfirmed and (open > ema25 and low > ema25 and close > ema25) and (na(pullMinLong) or pullMinLong > ema100)
shortEntryRaw = pullArmedShort and barstate.isconfirmed and (open < ema25 and high < ema25 and close < ema25) and (na(pullMaxShort) or pullMaxShort < ema100)
longEntry = longsEnabled and longEntryRaw
shortEntry = shortsEnabled and shortEntryRaw
// Disarm after trigger
if longEntry
pullArmedLong := false
pullMinLong := na
if shortEntry
pullArmedShort := false
pullMaxShort := na
// === Orders (entries only; no TP/SL) ===
if longEntry and strategy.position_size <= 0
strategy.entry("Long", strategy.long)
if shortEntry and strategy.position_size >= 0
strategy.entry("Short", strategy.short)
// === Plots & visuals ===
plot(ema25, "EMA 25", color=color.new(color.teal, 0))
plot(ema50, "EMA 50", color=color.new(color.orange, 0))
plot(ema100, "EMA 100", color=color.new(color.purple, 0))
bgcolor(trendLongOK ? color.new(color.green, 92) : na)
bgcolor(trendShortOK ? color.new(color.red, 92) : na)
if showSignals and longEntry
label.new(bar_index, low, "▲ BUY\nFull candle above 25 EMA", style=label.style_label_up, textcolor=color.white, color=color.new(color.green, 0))
if showSignals and shortEntry
label.new(bar_index, high, "▼ SELL\nFull candle below 25 EMA", style=label.style_label_down, textcolor=color.white, color=color.new(color.red, 0))