
VS, ATR, MA200, HTF
Dữ liệu phản hồi cho thấy: Khi thị trường xuất hiện tín hiệu Volume Spike ((VS), kết hợp với nhiều bộ lọc MA, tỷ lệ chiến thắng rõ ràng tốt hơn chiến lược phá vỡ truyền thống.
Chiến lược truyền thống nhìn vào giá, hệ thống này nhìn vào biến động khối lượng giao dịch. Đánh giá biến động trung bình được tính sau khi loại bỏ hai cực trong chu kỳ 21, và kích hoạt tín hiệu khi dòng K hiện tại biến động hơn 2,3 lần so với giá trị trung bình và chiếm hơn 0,7% giá trị khống chế. Quan trọng hơn, giá khống chế phải nằm ở vị trí 65% trên dòng K gốc, đảm bảo rằng đây là mức phát hành chủ đạo của nhiều đầu.
Dữ liệu nói lênCác nhà nghiên cứu đã phát hiện ra một hệ thống phát hiện VS có thể lọc hơn 90% các vụ đột phá giả mạo và chỉ bắt được những trường hợp thực sự có sự tham gia của các ngân hàng lớn.
Không phải tất cả các đợt phát hành đều đáng để theo đuổi, xu hướng thị trường quyết định mọi thứ. Chiến lược đặt ra bốn đường phòng thủ MA200:
Điều này có nghĩa là gì?Bạn sẽ không bao giờ bị mắc kẹt trong một xu hướng giảm rõ ràng, bởi vì hệ thống sẽ không có bất kỳ tín hiệu nào.
Mỗi giao dịch rủi ro được cố định ở mức 100 đô la (có thể điều chỉnh), kích thước vị trí được tính toán động thông qua ATR. 14 chu kỳ ATR nhân 2,7 lần là mức dừng ban đầu, tham số này được tối ưu hóa sau khi được đo lại rất nhiều, để tránh dừng biến động bình thường và có thể thoát khỏi thời gian khi thực sự đảo ngược.
Sự đổi mới quan trọng: Mỗi khi có tín hiệu VS mới xuất hiện, giá dừng sẽ tự động di chuyển đến mức thấp mới nhất, khóa các lỗ hổng đã có lợi trong khi vẫn còn chỗ cho xu hướng.
Các tín hiệu VS đầu tiên mở vị trí, tín hiệu VS thứ hai tăng vị trí, tín hiệu VS thứ ba sau khi dừng lỗ chuyển sang giá chi phí. Đây không phải là tăng vị trí mù quáng, nhưng dựa trên logic phán đoán của thị trường biến động liên tục và dòng tiền lớn liên tiếp thường có nghĩa là thị trường lớn hơn.
Dữ liệu hỗ trợLịch sử cho thấy các trường hợp có hơn 3 tín hiệu VS liên tiếp, mức tăng trung bình là 2.8 lần tín hiệu VS đơn lẻ.
Khi tín hiệu VS lần thứ 4 được kích hoạt, tự động dừng 33% vị trí; khi tín hiệu VS lần thứ 5, dừng lại 50% vị trí còn lại. Lý thuyết thiết kế như vậy là: tín hiệu VS trước xác nhận xu hướng, tín hiệu VS sau thường gần khu vực trên cùng.
Hiệu quả thực tếTrong khi đó, một số vị trí vẫn được giữ lại để nắm bắt những siêu hình có thể xảy ra.
Đây là cốt lõi của quản lý rủi ro. Khi mức nổi đạt 2%, giá dừng tự động tăng lên 0,15% trên giá chi phí. Dường như bảo thủ, thực tế là để lại đủ chỗ cho xu hướng lớn với giả định đảm bảo sự ổn định lâu dài của chiến lược.
Tại sao lại là 2%?Dữ liệu đánh giá lại cho thấy rằng các giao dịch có thể đạt được 2% thả lỏng và có khả năng lợi nhuận cuối cùng là hơn 78%.
Chiến lược này được thiết kế để tối ưu hóa biểu đồ 1 giờ của BTC, hoạt động nổi bật trong tình huống có xu hướng. Cần lưu ý rằng tín hiệu VS thường xuyên nhưng có mức độ hạn chế trong thị trường rung động, có thể xảy ra các trường hợp dừng nhỏ liên tục.
Dấu hiệu nguy cơ: Lịch sử không phản ánh lợi nhuận trong tương lai, có nguy cơ mất mát liên tục của chiến lược. Ưu tiên kiểm soát chặt chẽ rủi ro đơn lẻ, không quá 1-2% tài khoản.
Nếu bạn mong đợi tín hiệu mỗi ngày, chiến lược này không phù hợp với bạn. Nếu bạn muốn nắm bắt xu hướng thực sự và sẵn sàng chờ đợi cơ hội nhập vào chất lượng cao, thì bộ theo dõi cá mập này đáng để nghiên cứu sâu hơn.
/*backtest
start: 2025-01-13 00:00:00
end: 2026-01-11 00:00:00
period: 1h
basePeriod: 1h
exchanges: [{"eid":"Futures_OKX","currency":"ETH_USDT","balance":500000}]
*/
//@version=5
strategy("BULL Whale Finder + BTC 1h",
overlay=true,
pyramiding=4,
calc_on_every_tick=true,
process_orders_on_close=false)
// =====================================================
// INPUTS (SOLO 1)
// =====================================================
float MLPT_USD = input.float(100, "MLPT USD (riesgo por trade)", minval=1, step=1)
// =====================================================
// HARD CODED (NO TOCAR)
// =====================================================
// Execution
string POINTER = ""
bool allowBacktestNoPointer = true
// SL (ATR)
int atrLen = 14
float atrMult = 2.7
// Pay-Self
bool usePaySelf = true
float payTriggerPct = 2.0 / 100.0
float payLockPct = 0.15 / 100.0
// MA200 Filter
bool useMA200Filter = true
bool useMA200Slope = true
int ma200Len = 200
int ma200SlopeLen = 20
// MA200 HTF
bool useMA200HTF = true
string ma200HTF_tf = "240" // 4H
// VS Params
int vsLen = 21
int vsOut = 2
float vsMult = 2.3
float vsMinPct = 0.7 / 100.0
float vsClosePct = 35.0 / 100.0
// Exchange / rounding
float SL_BUFFER = 0.01
float qtyFixed = 0.001
float stepQty = 0.001
float MIN_NOTIONAL_USD = 20.0
// TP
bool tpFromVS3 = false
float tp1Pct = 33.0
float tp2Pct = 50.0
// Visual
bool showSL = true
bool showShade = true
bool showEntryDot = true
color cSL = color.new(color.green, 0)
color cShade = color.new(color.green, 85)
color cVSentry = color.lime
color cVStp = color.orange
// Proximidad MA1/MA2 (tal cual tus valores)
bool useMA1Filter = true // exigir close > MA20
bool useEntryNearMA2 = true // VS#1 cerca MA200 desde LOW
float entryNearMA2Pct = 6.0 / 100.0 // 6%
bool useEntryNearMA1 = false // desactivado (tu screenshot)
float entryNearMA1Pct = 6.0 / 100.0 // queda fijo aunque no se use
bool useMA1MA2Near = true // MA20 y MA200 cerca
float ma1ma2NearPct = 6.0 / 100.0 // 6%
// =====================================================
// JSON (ALERTS) — hardcode pointer vacío
// =====================================================
f_json(_event, _reduce) =>
"{" + "\"ticker\":\"{{ticker}}\"," + "\"action\":\"{{strategy.order.action}}\"," + "\"quantity\":\"{{strategy.order.contracts}}\"," + "\"pointer\":\"" + POINTER + "\"," + "\"reduce_only\":" + (_reduce ? "true" : "false") + "," + "\"event\":\"" + _event + "\"}"
// =====================================================
// HELPERS
// =====================================================
f_round_step_floor(_x, _step) => _step > 0 ? math.floor(_x / _step) * _step : _x
f_round_step_ceil(_x, _step) => _step > 0 ? math.ceil(_x / _step) * _step : _x
f_qty_min_notional(_qty, _px) =>
need = (MIN_NOTIONAL_USD > 0) ? (MIN_NOTIONAL_USD / _px) : 0.0
qRaw = math.max(_qty, need)
f_round_step_ceil(qRaw, stepQty)
f_qty_mlpt_long(_entry, _sl) =>
risk = _entry - _sl
qRaw = (risk > 0) ? (MLPT_USD / risk) : 0.0
f_round_step_floor(qRaw, stepQty)
// =====================================================
// MA200 / MA20
// =====================================================
ma200 = ta.sma(close, ma200Len)
plot(ma200, "MA200", color=color.red, linewidth=2)
ma1 = ta.sma(close, 20)
plot(ma1, "MA20", color=color.blue, linewidth=2)
ma200Slope = ma200 - ma200[ma200SlopeLen]
ma200SlopeOK = (not useMA200Slope) or (not na(ma200Slope) and ma200Slope > 0)
ma200FilterOK = (not useMA200Filter) or (close > ma200 and ma200SlopeOK)
// HTF MA200
ma200HTF = request.security(syminfo.tickerid, ma200HTF_tf, ta.sma(close, ma200Len))
ma200HTFFilterOK = (not useMA200HTF) or (not na(ma200HTF) and close > ma200HTF)
// Proximidad (medido desde LOW)
ma1FilterOK = (not useMA1Filter) or (close > ma1)
distLowMA2 = (not na(ma200) and low > 0) ? math.abs(low - ma200) / low : na
entryNearMA2OK = (not useEntryNearMA2) or (not na(distLowMA2) and distLowMA2 <= entryNearMA2Pct)
distLowMA1 = (not na(ma1) and low > 0) ? math.abs(low - ma1) / low : na
entryNearMA1OK = (not useEntryNearMA1) or (not na(distLowMA1) and distLowMA1 <= entryNearMA1Pct)
distMA1MA2 = (not na(ma1) and not na(ma200) and ma1 != 0) ? math.abs(ma1 - ma200) / ma1 : na
ma1ma2NearOK = (not useMA1MA2Near) or (not na(distMA1MA2) and distMA1MA2 <= ma1ma2NearPct)
// =====================================================
// VS DETECTION — LONG
// =====================================================
rng = high - low
f_avg_no_out(_len, _k) =>
float result = na
if bar_index >= _len
arr = array.new_float(0)
for i = 0 to _len - 1
array.push(arr, high[i] - low[i])
array.sort(arr, order.ascending)
n = array.size(arr)
kk = math.min(_k, math.floor((n - 1) / 2))
start = kk
stop = n - kk - 1
sum = 0.0
count = 0
if stop >= start
for j = start to stop
sum += array.get(arr, j)
count += 1
result := count > 0 ? sum / count : na
result
avgRng = f_avg_no_out(vsLen, vsOut)
okRange = not na(avgRng) and rng >= avgRng * vsMult
okMinPct = rng >= close * vsMinPct
strongBull = rng > 0 and (high - close) / rng <= vsClosePct
isVS = okRange and okMinPct and strongBull
// =====================================================
// EXEC FLAGS (hardcoded)
// =====================================================
hasPointer = str.length(POINTER) > 0
canTrade = allowBacktestNoPointer or hasPointer
// =====================================================
// VARS
// =====================================================
var float slPrice = na
var float entryPx = na
var float initQty = na
var float mfePct = 0.0
var bool payArmed = false
var int vsCount = 0
var float vs2Low = na
var bool tp1 = false
var bool tp2 = false
// RESET
if strategy.position_size == 0
slPrice := na
entryPx := na
initQty := na
mfePct := 0.0
payArmed := false
vsCount := 0
vs2Low := na
tp1 := false
tp2 := false
// =====================================================
// ENTRY (VS #1) + SL inicial ATR
// =====================================================
enterCond = barstate.isconfirmed and isVS and ma200FilterOK and ma200HTFFilterOK and ma1FilterOK and entryNearMA2OK and entryNearMA1OK and ma1ma2NearOK and strategy.position_size == 0 and canTrade
if enterCond
atr = ta.atr(atrLen)
slInit = close - atr * atrMult
qtyRisk = f_qty_mlpt_long(close, slInit)
qtyFinal = f_qty_min_notional(qtyRisk, close)
qtyFinal := f_round_step_floor(qtyFinal, stepQty)
if qtyFinal > 0
strategy.entry("L", strategy.long, qty=qtyFinal, alert_message=(hasPointer ? f_json("ENTRY_INIT", false) : ""))
entryPx := close
initQty := qtyFinal
slPrice := slInit
vsCount := 1
plotshape(showEntryDot and enterCond, title="Entry Dot", style=shape.circle, size=size.tiny, location=location.belowbar, color=color.new(color.green, 0))
// =====================================================
// PAY-SELF (MFE % -> SL piso a profit fijo, sin cerrar size)
// =====================================================
if usePaySelf and strategy.position_size > 0 and not na(entryPx) and entryPx > 0
curMfePct = math.max(0.0, (high - entryPx) / entryPx)
mfePct := math.max(mfePct, curMfePct)
if not payArmed and mfePct >= payTriggerPct
payArmed := true
if payArmed and payLockPct > 0 and not na(initQty) and initQty > 0
paySL = entryPx * (1.0 + payLockPct)
slPrice := na(slPrice) ? paySL : math.max(slPrice, paySL)
// =====================================================
// VS SEQUENCE
// =====================================================
if barstate.isconfirmed and strategy.position_size > 0 and isVS
vsCount += 1
slTrail = low - SL_BUFFER
slPrice := na(slPrice) ? slTrail : math.max(slPrice, slTrail)
if vsCount == 2
vs2Low := low - SL_BUFFER
addQty = f_qty_mlpt_long(close, slPrice)
addQty := f_qty_min_notional(addQty, close)
addQty := f_round_step_floor(addQty, stepQty)
if addQty > 0
strategy.entry("L", strategy.long, qty=addQty, alert_message=(hasPointer ? f_json("ADD_VS2", false) : ""))
if vsCount == 3
slPrice := math.max(slPrice, entryPx)
if not na(vs2Low)
slPrice := math.max(slPrice, vs2Low)
int tp1VS = tpFromVS3 ? 3 : 4
int tp2VS = tpFromVS3 ? 4 : 5
if vsCount == tp1VS and not tp1
strategy.close("L", qty_percent=tp1Pct, alert_message=(hasPointer ? f_json("TP1_VS" + str.tostring(tp1VS), true) : ""))
tp1 := true
if vsCount == tp2VS and not tp2
strategy.close("L", qty_percent=tp2Pct, alert_message=(hasPointer ? f_json("TP2_VS" + str.tostring(tp2VS), true) : ""))
tp2 := true
// =====================================================
// EXIT (SL EVENT)
// =====================================================
if strategy.position_size > 0 and not na(slPrice)
strategy.exit("XL", from_entry="L", stop=slPrice, alert_message=(hasPointer ? f_json("SL_EVENT", true) : ""))
// =====================================================
// BAR COLORS (VS entrada vs VS de TP)
// =====================================================
int tp1VS_now = tpFromVS3 ? 3 : 4
int tp2VS_now = tpFromVS3 ? 4 : 5
isTPvs = strategy.position_size > 0 and isVS and (vsCount == tp1VS_now or vsCount == tp2VS_now)
barcolor(isTPvs ? cVStp : (isVS ? cVSentry : na))
// =====================================================
// SL PLOT + SHADE
// =====================================================
pSL = plot(showSL ? slPrice : na, "SL", color=cSL, linewidth=2, style=plot.style_linebr)
pPx = plot(showShade and strategy.position_size > 0 ? close : na, "PX (fill)", color=color.new(color.white, 100), display=display.none)
fill(pSL, pPx, color=(showShade and strategy.position_size > 0 ? cShade : na))