Isolated Scroll Container

📊 Chấm Điểm Chất Lượng FVG | Flux Charts

Phân Tích Chỉ Báo · Phiên Bản Tiếng Việt · PineScript v6

1.1 Khái Niệm Cốt Lõi

FVG Quality Scorer đánh giá mỗi vùng Fair Value Gap (FVG / vùng mất cân bằng) qua 4 trục chấm điểm độc lập, mỗi trục tối đa 25 điểm, tổng hợp thành điểm tổng 0–100 và xếp hạng A/B/C/D. Thay vì coi mọi FVG ngang bằng, chỉ báo lọc ra các vùng FVG chất lượng cao nhất — chỉ hiển thị các setup xứng đáng được giao dịch.

FVG tăng hình thành khi low > high[2] (khoảng trống giữa đáy nến hiện tại và đỉnh nến cách 2 nến). FVG giảm khi high < low[2]. Đây là cấu trúc mất cân bằng 3 nến tiêu chuẩn theo phương pháp ICT/SMC.

💡 Nguyên lý thiết kế: Trọng số 4 trục do người dùng tự chỉnh và được chuẩn hóa tự động về tổng 100. Mỗi cấp xếp hạng có thể yêu cầu điểm tối thiểu riêng cho từng trục — đảm bảo điểm A không thể đạt được chỉ nhờ mạnh ở một chiều duy nhất.

1.2 Tính Năng Chính

  • 📐 4 trục chấm điểm — Độ Dịch Chuyển · Delta Khối Lượng · Bối Cảnh · Cấu Trúc, mỗi trục 0–25 điểm
  • 🏆 Xếp hạng A/B/C/D — điểm tối thiểu và trục bắt buộc có thể cấu hình riêng từng cấp
  • ⚖️ Tổng hợp có trọng số — trọng số do người dùng định nghĩa, tự động chuẩn hóa về 100
  • 📊 Delta khối lượng nội nến (LTF) — tỷ lệ khối lượng tăng/giảm trong nến via request.security_lower_tf
  • 🕰️ Lồng ghép FVG đa khung — kiểm tra FVG hiện tại có nằm trong FVG khung thời gian cao hơn không
  • 🎯 Nhận diện Killzone — cộng điểm cho FVG hình thành trong phiên Á/London/NY
  • 📈 Theo dõi BOS/CHoCH — thưởng điểm cho FVG cùng hướng với phá vỡ cấu trúc gần nhất
  • 🔄 Máy trạng thái — Fresh → Tested / PartiallyFilled → Mitigated, hiển thị lịch sử tùy chọn
  • 🖼️ Vẽ trì hoãn — toàn bộ box/nhãn chỉ vẽ tại barstate.islast để tối ưu hiệu năng
  • 📋 Bảng thông tin — số FVG hoạt động/tổng/thời gian lấp trung bình per cấp + khoảng cách FVG-A gần nhất
  • 🚨 Cảnh báo — phát hiện FVG mới cấp A và cấp B

1.3 Hướng Dẫn Sử Dụng

⚙️ Các Tham Số Đầu Vào
Tham SốMặc ĐịnhÝ Nghĩa
swingLen5Số nến mỗi bên để xác nhận swing cao/thấp cho cấu trúc thị trường
atrLen14Chu kỳ ATR cho chấm điểm độ dịch chuyển
ltfTf"1"Khung thời gian thấp hơn để lấy delta khối lượng nội nến
htfTf"60"Khung thời gian cao hơn để kiểm tra lồng ghép FVG
rangeLb50Số nến lookback để tính vùng premium/discount
sweepWin5Cửa sổ (nến) nhìn lại để tìm sự kiện quét thanh khoản
bosLb20Số nến tối đa kể từ BOS/CHoCH gần nhất để cộng điểm cấu trúc
emaLen50Chu kỳ EMA cho kiểm tra căn chỉnh xu hướng
dispW/volW/ctxW/strW25 mỗi trụcTrọng số tương đối cho từng trục (tự chuẩn hóa về 100)
minGrade"C"Ẩn FVG thấp hơn cấp này khỏi biểu đồ
mitMethod"Wick"Wick: vô hiệu khi bóng nến vượt vùng; Close: khi giá đóng cửa vượt
showHistfalseHiện FVG đã bị vô hiệu hóa dưới dạng box xám
📖 Cách Đọc Biểu Đồ
  • Box xanh lá (Grade A) — FVG chất lượng cao nhất, cả 4 trục mạnh, ưu tiên giao dịch
  • Box xanh ngọc (Grade B) — setup tốt, độ dịch chuyển + khối lượng mạnh, cấu trúc/bối cảnh hơi yếu
  • Box vàng (Grade C) — chất lượng trung bình, cần thêm xác nhận
  • Box xám (Grade D) — chất lượng thấp, nên bỏ qua (bộ lọc minGrade sẽ ẩn)
  • Nhãn điểm số — điểm tổng 0–100 hiển thị bên phải mỗi box; hover để xem chi tiết từng trục
  • Box mờ hơn — trạng thái PartiallyFilled (giá đã vào vùng nhưng chưa thoát)
  • Box xám nhạt — trạng thái Mitigated (chỉ hiện khi showHist = true)
🎯 Gợi Ý Ứng Dụng
  • Đặt minGrade = "B" để chỉ hiện setup độ tin cậy cao — giảm nhiễu đáng kể
  • Bật cảnh báo "New A-Grade FVG" để không bỏ lỡ các FVG xuất sắc theo thời gian thực
  • Dùng cột "Avg Fill Time" trong dashboard để ước tính thời gian hold theo từng cấp xếp hạng
  • FVG cấp A lồng ghép trong FVG đa khung (HTF) được cộng đủ 8 điểm bối cảnh — đây là setup có độ hội tụ cao nhất
  • Trên khung thời gian thấp (M5), đặt ltfTf = "1" để có dữ liệu LTF phù hợp

1.4 Nguyên Lý Hoạt Động (Từ Đầu Ra Đến Đầu Vào)

🖥️ Đầu Ra — Những Gì Hiển Thị Trên Biểu Đồ
  • Box màu (A=xanh lá / B=xanh ngọc / C=vàng / D=xám) trải dài vùng giá FVG
  • Chữ cái xếp hạng canh giữa trong box
  • Nhãn điểm số (0–100) ở cạnh phải box kèm tooltip chi tiết từng trục khi hover
  • Bảng thông tin: số FVG hoạt động/tổng/thời gian lấp trung bình theo cấp + khoảng cách FVG-A gần nhất
🖼️ Tầng 6 — Vẽ Trì Hoãn (barstate.islast)

Toàn bộ đối tượng đồ họa được vẽ lại từ đầu mỗi tick barstate.islast: xóa hết box/nhãn cũ, sau đó duyệt toàn bộ fvgArr. FVG thấp hơn minGrade bị bỏ qua; FVG đã mitigated bị bỏ qua trừ khi showHist = true. Box mờ đi khi PartiallyFilled, chuyển xám hoàn toàn khi Mitigated.

🔄 Tầng 5 — Máy Trạng Thái (Vòng Đời FVG)

Mỗi nến, tất cả FVG chưa bị vô hiệu trong fvgArr được kiểm tra:

Chuyển Trạng TháiĐiều Kiện
→ MitigatedPhương pháp Wick: low < fvg.bottom (tăng) / high > fvg.top (giảm); Close: giá đóng vượt vùng
→ PartiallyFilledGiá vào vùng nhưng chưa vượt qua hoàn toàn: close < top và close > bottom
→ TestedGiá chạm biên vùng từ bên ngoài: low ≤ top và low[1] ≥ top (FVG tăng)
PartiallyFilled → TestedGiá thoát vùng mà không vô hiệu hoàn toàn
🗃️ Tầng 4 — Lưu Trữ & Quản Lý Bộ Nhớ

FVG mới được push vào fvgArr (mảng các đối tượng FVG UDT). Khi mảng vượt 200 phần tử, phần tử cũ nhất bị shift ra để tránh tràn bộ nhớ. Các bộ đếm dashboard (aTotal, aBarsSum...) tích lũy thống kê suốt vòng đời và không bao giờ reset.

🏆 Tầng 3 — Xếp Hạng & Tổng Điểm Có Trọng Số

Công thức tổng điểm:
weighted = (dsp/25)×dW + (vol/25)×vW + (ctx/25)×cW + (str/25)×sW
total = min(round(weighted × 100/wSum), 100)

Xếp hạng — kiểm tra theo thứ tự A → B → C → D; cấp đầu tiên đạt được sẽ được gán. Mỗi cấp kiểm tra cả điểm tối thiểu từng trục VÀ cờ "bắt buộc" (required):

CấpĐiểm Tối Thiểu Mặc ĐịnhTrục Bắt Buộc
A12/25 mỗi trụcCả 4 trục bắt buộc
B12/25 mỗi trụcDịch Chuyển + Khối Lượng bắt buộc
C8/25 mỗi trụcDịch Chuyển + Khối Lượng bắt buộc
D(dự phòng)Không có trục bắt buộc
📊 Tầng 2 — Bốn Trục Chấm Điểm ① Trục Độ Dịch Chuyển (tối đa 25 điểm)
Thành PhầnĐiểm MaxLogic
Tỷ lệ thân nến/biên độ10≥85% → 10 · ≥70% → 7 · ≥55% → 4 · còn lại → 0
Bội số ATR10Kích thước thân ÷ ATR: ≥2× → 10 · ≥1.5× → 7 · ≥1× → 4 · còn lại → 0
Liên tiếp cùng chiều5Nến tại bar[2] và bar[0] đều cùng hướng FVG: 5 · một nến: 2 · không có: 0
② Trục Delta Khối Lượng (tối đa 25 điểm)
Thành PhầnĐiểm MaxLogic
Tỷ lệ tăng/giảm LTF12Tỷ lệ khối lượng hướng tính trên nến dịch chuyển: >75% → 12 · ≥60% → 8 · ≥50% → 4
Khối lượng tương đối8Khối lượng nến ÷ SMA 20: >2× → 8 · ≥1.5× → 5 · ≥1× → 2
Khối lượng tăng dần5vol[1] > vol[2] > vol[3] → 5 điểm
③ Trục Bối Cảnh (tối đa 25 điểm)
Thành PhầnĐiểm MaxLogic
Vùng Premium / Discount8FVG tăng trong vùng discount (dưới 50% biên độ rangeLb) = 8 điểm; trên midpoint = gradient 4→0
Lồng ghép FVG đa khung8FVG hiện tại nằm hoàn toàn trong FVG HTF cùng hướng = 8 điểm
Gần sự kiện quét thanh khoản5Quét thanh khoản (wick vượt swing rồi đóng cửa lại) trong sweepWin nến = 5 điểm
Thời điểm Killzone4FVG hình thành trong phiên Á / London / NY sáng / NY chiều = 4 điểm
④ Trục Cấu Trúc (tối đa 25 điểm)
Thành PhầnĐiểm MaxLogic
Xu hướng cấu trúc thị trường10HH+HL (cấu trúc tăng) + FVG tăng → 10 · cấu trúc ngược chiều → 0 · trung tính → 2
Căn chỉnh EMA8FVG tăng và close > EMA50 → 8 · FVG giảm và close < EMA50 → 8 · ngược → 0
BOS/CHoCH gần đây7BOS trong vòng bosLb nến, đúng hướng → 7 · sai hướng → 0 · không có BOS gần → 2
🔍 Tầng 1 — Dữ Liệu & Tính Toán Toàn Cục
  • atrVal = ta.atr(atrLen) — ATR cho chấm điểm độ dịch chuyển
  • ema50 = ta.ema(close, emaLen) — đường chuẩn xu hướng
  • volSma20 = ta.sma(volume, 20) — baseline khối lượng tương đối
  • pivHi / pivLo — swing cao/thấp để theo dõi cấu trúc và BOS
  • request.security_lower_tf — thu thập mảng khối lượng tăng/giảm mỗi nến LTF, cộng lại theo nến hiện tại
  • request.security(htfTf, [high, low, high[2], low[2]]) — OHLC khung cao để kiểm tra lồng ghép
  • inKillzone() — nhận diện phiên theo giờ New York (Á ≥20:00, London 02:00–05:00, NY AM 09:30–11:00, NY PM 13:30–16:00)
  • rangeHigh / rangeLow / rangeMid — cao nhất/thấp nhất rolling trong rangeLb nến cho vùng premium/discount
Zoom/Pan Layered Image
Background Overlay
+
				
					// This Pine Script™ code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © fluxchart

//@version=6
indicator("FVG Quality Scorer | Flux Charts", overlay = true, max_boxes_count = 500, max_labels_count = 500, max_bars_back = 5000)

//#region CONSTANTS

GP_SCORE      = "⚙️ Cài Đặt"
GP_WEIGHT     = "⚖️ Trọng Số Trục"
GP_DISP       = "🎨 Hiển Thị"
GP_DASH       = "📋 Bảng Thông Tin"

TT_LTF        = "Khung thời gian thấp hơn để tính delta khối lượng nội nến. Dùng '1' cho 1 phút."
TT_HTF        = "Khung thời gian cao hơn để kiểm tra FVG lồng ghép."
TT_WEIGHT     = "Trọng số tương đối cho trục chấm điểm này. Tất cả trọng số được chuẩn hóa về 100."
TT_GRADE      = "Ẩn các FVG có cấp thấp hơn ngưỡng này."
TT_MIT        = "Wick: vô hiệu khi bóng nến vượt vùng. Close: vô hiệu khi giá đóng cửa vượt vùng."
TT_HIST       = "Khi bật, các FVG đã vô hiệu vẫn hiện dưới dạng box xám."

STATE_FRESH   = "Fresh"
STATE_TESTED  = "Tested"
STATE_PARTIAL = "PartiallyFilled"
STATE_MIT     = "Mitigated"

EXTEND_BARS   = 20

GREEN         = #00C853
TEAL          = #00BFA5
AMBER         = #FFC107
GRAY          = #9E9E9E
DASH_DARK     = #1a1a2e
DASH_MID      = #16213e
DASH_ROW      = #0f3460

//#endregion CONSTANTS

//#region INPUTS

//#region Settings
swingLen   = input.int(5,              "Độ Dài Swing",                  minval = 2, maxval = 50,        group = GP_SCORE,  display = display.none)
atrLen     = input.int(14,             "Chu Kỳ ATR",                    minval = 5, maxval = 50,        group = GP_SCORE,  display = display.none)
ltfTf      = input.timeframe("1",      "LTF cho Delta Khối Lượng",                                      group = GP_SCORE,  display = display.none, tooltip = TT_LTF)
htfTf      = input.timeframe("60",     "HTF cho Lồng Ghép FVG",                                         group = GP_SCORE,  display = display.none, tooltip = TT_HTF)
rangeLb    = input.int(50,             "Lookback Vùng Giá",             minval = 10, maxval = 200,      group = GP_SCORE,  display = display.none)
sweepWin   = input.int(5,              "Cửa Sổ Quét Thanh Khoản",      minval = 1, maxval = 20,        group = GP_SCORE,  display = display.none)
bosLb      = input.int(20,             "Lookback BOS/CHoCH",            minval = 5, maxval = 50,        group = GP_SCORE,  display = display.none)
emaLen     = input.int(50,             "Chu Kỳ EMA",                    minval = 10, maxval = 200,      group = GP_SCORE,  display = display.none)
//#endregion Settings

//#region Axis Weights
dispW      = input.int(25,             "Trọng Số Độ Dịch Chuyển",      minval = 0, maxval = 100,       group = GP_WEIGHT, display = display.none, tooltip = TT_WEIGHT)
volW       = input.int(25,             "Trọng Số Delta Khối Lượng",    minval = 0, maxval = 100,       group = GP_WEIGHT, display = display.none, tooltip = TT_WEIGHT)
ctxW       = input.int(25,             "Trọng Số Bối Cảnh",            minval = 0, maxval = 100,       group = GP_WEIGHT, display = display.none, tooltip = TT_WEIGHT)
strW       = input.int(25,             "Trọng Số Cấu Trúc",            minval = 0, maxval = 100,       group = GP_WEIGHT, display = display.none, tooltip = TT_WEIGHT)
//#endregion Axis Weights

//#region Display
minGrade   = input.string("C",         "Cấp Tối Thiểu Hiển Thị",       options = ["A", "B", "C", "D"], group = GP_DISP,   display = display.none, tooltip = TT_GRADE)
showHist   = input.bool(false,         "Hiện FVG Đã Vô Hiệu",                                          group = GP_DISP,   display = display.none, tooltip = TT_HIST)
showLabels = input.bool(true,          "Hiện Nhãn Điểm Số",                                            group = GP_DISP,   display = display.none)
colA       = input.color(GREEN,        "Màu Cấp (A → D)",               inline = "gradeColors",         group = GP_DISP,   display = display.none)
colB       = input.color(TEAL,         "",                               inline = "gradeColors",         group = GP_DISP,   display = display.none)
colC       = input.color(AMBER,        "",                               inline = "gradeColors",         group = GP_DISP,   display = display.none)
colD       = input.color(GRAY,         "",                               inline = "gradeColors",         group = GP_DISP,   display = display.none)
//#endregion Display

mitMethod  = input.string("Wick",      "Phương Pháp Vô Hiệu",          options = ["Close", "Wick"],    group = GP_SCORE,  display = display.none, tooltip = TT_MIT)

//#region Dashboard
showDash   = input.bool(true,          "Hiện Bảng Thông Tin",                                          group = GP_DASH)
dashPos    = input.string("Top Right", "Vị Trí Bảng",                  options = ["Top Right", "Top Center", "Top Left", "Middle Right", "Middle Center", "Middle Left", "Bottom Right", "Bottom Center", "Bottom Left"], group = GP_DASH,   display = display.none)
dashSize   = input.string("Small",     "Cỡ Chữ Bảng",                  options = ["Tiny", "Small", "Normal", "Large", "Huge"], group = GP_DASH,   display = display.none)
//#endregion Dashboard

//#region A-Grade Requirements
aMinDisp   = input.int(12,             "A: Diem Dich Chuyen Toi Thieu", minval = 0, maxval = 25,        group = "🏆 Yêu Cầu Cấp A", display = display.none)
aMinVol    = input.int(12,             "A: Diem Khoi Luong Toi Thieu",  minval = 0, maxval = 25,        group = "🏆 Yêu Cầu Cấp A", display = display.none)
aMinCtx    = input.int(12,             "A: Diem Boi Canh Toi Thieu",    minval = 0, maxval = 25,        group = "🏆 Yêu Cầu Cấp A", display = display.none)
aMinStr    = input.int(12,             "A: Diem Cau Truc Toi Thieu",    minval = 0, maxval = 25,        group = "🏆 Yêu Cầu Cấp A", display = display.none)
aReqDisp   = input.bool(true,          "A: Bat Buoc Dich Chuyen",                                       group = "🏆 Yêu Cầu Cấp A", display = display.none)
aReqVol    = input.bool(true,          "A: Bat Buoc Khoi Luong",                                        group = "🏆 Yêu Cầu Cấp A", display = display.none)
aReqCtx    = input.bool(true,          "A: Bat Buoc Boi Canh",                                          group = "🏆 Yêu Cầu Cấp A", display = display.none)
aReqStr    = input.bool(true,          "A: Bat Buoc Cau Truc",                                          group = "🏆 Yêu Cầu Cấp A", display = display.none)
//#endregion A-Grade Requirements

//#region B-Grade Requirements
bMinDisp   = input.int(12,             "B: Diem Dich Chuyen Toi Thieu", minval = 0, maxval = 25,        group = "🥈 Yêu Cầu Cấp B", display = display.none)
bMinVol    = input.int(12,             "B: Diem Khoi Luong Toi Thieu",  minval = 0, maxval = 25,        group = "🥈 Yêu Cầu Cấp B", display = display.none)
bMinCtx    = input.int(12,             "B: Diem Boi Canh Toi Thieu",    minval = 0, maxval = 25,        group = "🥈 Yêu Cầu Cấp B", display = display.none)
bMinStr    = input.int(12,             "B: Diem Cau Truc Toi Thieu",    minval = 0, maxval = 25,        group = "🥈 Yêu Cầu Cấp B", display = display.none)
bReqDisp   = input.bool(true,          "B: Bat Buoc Dich Chuyen",                                       group = "🥈 Yêu Cầu Cấp B", display = display.none)
bReqVol    = input.bool(true,          "B: Bat Buoc Khoi Luong",                                        group = "🥈 Yêu Cầu Cấp B", display = display.none)
bReqCtx    = input.bool(false,         "B: Bat Buoc Boi Canh",                                          group = "🥈 Yêu Cầu Cấp B", display = display.none)
bReqStr    = input.bool(false,         "B: Bat Buoc Cau Truc",                                          group = "🥈 Yêu Cầu Cấp B", display = display.none)
//#endregion B-Grade Requirements

//#region C-Grade Requirements
cMinDisp   = input.int(8,              "C: Diem Dich Chuyen Toi Thieu", minval = 0, maxval = 25,        group = "🥉 Yêu Cầu Cấp C", display = display.none)
cMinVol    = input.int(8,              "C: Diem Khoi Luong Toi Thieu",  minval = 0, maxval = 25,        group = "🥉 Yêu Cầu Cấp C", display = display.none)
cMinCtx    = input.int(8,              "C: Diem Boi Canh Toi Thieu",    minval = 0, maxval = 25,        group = "🥉 Yêu Cầu Cấp C", display = display.none)
cMinStr    = input.int(8,              "C: Diem Cau Truc Toi Thieu",    minval = 0, maxval = 25,        group = "🥉 Yêu Cầu Cấp C", display = display.none)
cReqDisp   = input.bool(true,          "C: Bat Buoc Dich Chuyen",                                       group = "🥉 Yêu Cầu Cấp C", display = display.none)
cReqVol    = input.bool(true,          "C: Bat Buoc Khoi Luong",                                        group = "🥉 Yêu Cầu Cấp C", display = display.none)
cReqCtx    = input.bool(false,         "C: Bat Buoc Boi Canh",                                          group = "🥉 Yêu Cầu Cấp C", display = display.none)
cReqStr    = input.bool(false,         "C: Bat Buoc Cau Truc",                                          group = "🥉 Yêu Cầu Cấp C", display = display.none)
//#endregion C-Grade Requirements

//#endregion INPUTS

//#region DECLARATIONS

//#region USER-DEFINED TYPES

type FVG
    int     formedAt
    float   top
    float   bottom
    bool    isBullish
    string  state
    int     dispScore
    int     volScore
    int     ctxScore
    int     strScore
    int     totalScore
    string  grade
    int     mitigatedAt = na

//#endregion USER-DEFINED TYPES

//#endregion DECLARATIONS

//#region CALCULATIONS

//#region GLOBAL CALCULATIONS

atrVal   = ta.atr(atrLen)
ema50    = ta.ema(close, emaLen)
volSma20 = ta.sma(volume, 20)

//#region Swing Highs / Lows for Market Structure

pivHi = ta.pivothigh(high, swingLen, swingLen)
pivLo = ta.pivotlow(low, swingLen, swingLen)

var float lastSwingHigh    = na
var float prevSwingHigh    = na
var int   lastSwingHighBar = na
var float lastSwingLow     = na
var float prevSwingLow     = na
var int   lastSwingLowBar  = na

if not na(pivHi)
    prevSwingHigh    := lastSwingHigh
    lastSwingHigh    := pivHi
    lastSwingHighBar := bar_index - swingLen

if not na(pivLo)
    prevSwingLow    := lastSwingLow
    lastSwingLow    := pivLo
    lastSwingLowBar := bar_index - swingLen

//#endregion Swing Highs / Lows for Market Structure

//#region BOS Tracking

var int    lastBosBar      = na
var bool   lastBosBull     = false
var float  brokenSwingHigh = na
var float  brokenSwingLow  = na

if not na(lastSwingHigh) and close > lastSwingHigh and close[1] <= lastSwingHigh and lastSwingHigh != brokenSwingHigh
    lastBosBar      := bar_index
    lastBosBull     := true
    brokenSwingHigh := lastSwingHigh

if not na(lastSwingLow) and close < lastSwingLow and close[1] >= lastSwingLow and lastSwingLow != brokenSwingLow
    lastBosBar     := bar_index
    lastBosBull    := false
    brokenSwingLow := lastSwingLow

//#endregion BOS Tracking

//#region LTF Volume Delta via request.security_lower_tf

[ltfBullVol, ltfBearVol] = request.security_lower_tf(syminfo.tickerid, ltfTf, [close > open ? volume : 0.0, close <= open ? volume : 0.0])

sumBullVol = 0.0
sumBearVol = 0.0

if not na(ltfBullVol) and ltfBullVol.size() > 0
    for i = 0 to ltfBullVol.size() - 1
        sumBullVol += ltfBullVol.get(i)
        sumBearVol += ltfBearVol.get(i)

//#endregion LTF Volume Delta via request.security_lower_tf

//#region HTF FVG Nesting Data

[htfHigh0, htfLow0, htfHigh2, htfLow2] = request.security(syminfo.tickerid, htfTf, [high, low, high[2], low[2]], lookahead = barmerge.lookahead_off)

//#endregion HTF FVG Nesting Data

//#region Killzone Detection (New York Time)

nyHour   = hour(time, "America/New_York")
nyMinute = minute(time, "America/New_York")
nyTime   = nyHour * 100 + nyMinute

inKillzone() =>
    inAsian  = nyTime >= 2000
    inLondon = nyTime >= 200 and nyTime < 500
    inNYAM   = nyTime >= 930 and nyTime < 1100
    inNYPM   = nyTime >= 1330 and nyTime < 1600
    inAsian or inLondon or inNYAM or inNYPM

isKillzone = inKillzone()

//#endregion Killzone Detection (New York Time)

//#region Range for Premium / Discount

rangeHigh = ta.highest(high, rangeLb)
rangeLow  = ta.lowest(low, rangeLb)
rangeMid  = (rangeHigh + rangeLow) / 2.0

//#endregion Range for Premium / Discount
//#endregion GLOBAL CALCULATIONS

//#region FVG STORAGE

var fvgArr = array.new<FVG>()

// Dashboard stats
var aTotal     = 0
var aBarsSum   = 0
var aBarsCount = 0
var bTotal     = 0
var bBarsSum   = 0
var bBarsCount = 0
var cTotal     = 0
var cBarsSum   = 0
var cBarsCount = 0
var dTotal     = 0
var dBarsSum   = 0
var dBarsCount = 0

//#endregion FVG STORAGE

//#endregion CALCULATIONS

//#region FUNCTIONS

//#region HELPER FUNCTIONS

gradeColor(string g) =>
    switch g
        "A" => colA
        "B" => colB
        "C" => colC
        => colD

gradeMinScore(string g) =>
    switch g
        "A" => 75
        "B" => 50
        "C" => 25
        => 0

passesFilter(string g) =>
    minS               = gradeMinScore(minGrade)
    gScore             = gradeMinScore(g)
    gScore >= minS

dashPosition() =>
    switch dashPos
        "Top Right"      => position.top_right
        "Top Center"     => position.top_center
        "Top Left"       => position.top_left
        "Middle Right"   => position.middle_right
        "Middle Center"  => position.middle_center
        "Middle Left"    => position.middle_left
        "Bottom Right"   => position.bottom_right
        "Bottom Center"  => position.bottom_center
        => position.bottom_left

dashTextSize() =>
    switch dashSize
        "Tiny"   => size.tiny
        "Small"  => size.small
        "Normal" => size.normal
        "Large"  => size.large
        => size.huge

avgBars(int sumBars, int count) =>
    count > 0 ? str.tostring(math.round(sumBars / count)) + " Nến" : "—"

//#endregion HELPER FUNCTIONS

//#region Axis 1: Displacement Strength

scoreDisplacement(bool isBull) =>
    // Tỷ lệ thân nến/biên độ (0-10)
    bodyRange = high[1] - low[1]
    ratio     = bodyRange > 0 ? math.abs(close[1] - open[1]) / bodyRange : 0.0
    ptsBody   = ratio >= 0.85 ? 10 : ratio >= 0.70 ? 7 : ratio >= 0.55 ? 4 : 0

    // Bội số ATR (0-10)
    mult   = atrVal > 0 ? bodyRange / atrVal : 0.0
    ptsATR = mult >= 2.0 ? 10 : mult >= 1.5 ? 7 : mult >= 1.0 ? 4 : 0

    // Liên tiếp cùng chiều (0-5)
    bar3Aligned = isBull ? close[2] > open[2] : close[2] < open[2]
    bar1Aligned = isBull ? close > open : close < open
    alignCount  = (bar3Aligned ? 1 : 0) + (bar1Aligned ? 1 : 0)
    ptsConsec   = alignCount == 2 ? 5 : alignCount == 1 ? 2 : 0

    ptsBody + ptsATR + ptsConsec

//#endregion Axis 1: Displacement Strength

//#region Axis 2: Volume Delta

scoreVolumeDelta(bool isBull) =>
    // Tỷ lệ tăng/giảm LTF (0-12) — nến dịch chuyển (bar N-1)
    totalVol  = sumBullVol[1] + sumBearVol[1]
    dominance = totalVol > 0 ? (isBull ? sumBullVol[1] / totalVol : sumBearVol[1] / totalVol) : 0.0
    ptsDom    = dominance > 0.75 ? 12 : dominance >= 0.60 ? 8 : dominance >= 0.50 ? 4 : 0

    // Khối lượng tương đối (0-8)
    relVol = volSma20 > 0 ? volume[1] / volSma20 : 0.0
    ptsRel = relVol > 2.0 ? 8 : relVol >= 1.5 ? 5 : relVol >= 1.0 ? 2 : 0

    // Khối lượng tăng dần vào FVG (0-5)
    rising  = volume[1] > volume[2] and volume[2] > volume[3]
    ptsRise = rising ? 5 : 0

    ptsDom + ptsRel + ptsRise

//#endregion Axis 2: Volume Delta

//#region Axis 3: Contextual Location

scoreContextual(bool isBull, float fvgTop, float fvgBottom) =>
    fvgMid = (fvgTop + fvgBottom) / 2.0

    // Vùng Premium/Discount (0-8)
    rangeSize = rangeHigh - rangeLow
    ptsPD     = 0
    if rangeSize > 0
        pos = (fvgMid - rangeLow) / rangeSize
        if isBull
            ptsPD := pos <= 0.5 ? 8 : math.round((1.0 - pos) * 8.0)
        else
            ptsPD := pos >= 0.5 ? 8 : math.round(pos * 8.0)

    // Lồng ghép FVG đa khung (0-8)
    ptsHTF     = 0
    htfBullFvg = not na(htfLow0) and not na(htfHigh2) and htfLow0 > htfHigh2
    htfBearFvg = not na(htfHigh0) and not na(htfLow2) and htfHigh0 < htfLow2
    if isBull and htfBullFvg
        htfFvgTop = htfLow0
        htfFvgBot = htfHigh2
        if fvgTop <= htfFvgTop and fvgBottom >= htfFvgBot
            ptsHTF := 8
    else if not isBull and htfBearFvg
        htfFvgTop = htfLow2
        htfFvgBot = htfHigh0
        if fvgTop <= htfFvgTop and fvgBottom >= htfFvgBot
            ptsHTF := 8

    // Gần sự kiện quét thanh khoản (0-5)
    ptsSweep = 0
    for i = 1 to sweepWin
        if isBull
            swingLowAtI = lastSwingLow[i]
            if not na(swingLowAtI) and low[i] < swingLowAtI and close[i] > swingLowAtI
                ptsSweep := 5
                break
        if not isBull
            swingHighAtI = lastSwingHigh[i]
            if not na(swingHighAtI) and high[i] > swingHighAtI and close[i] < swingHighAtI
                ptsSweep := 5
                break

    // Thời điểm Killzone (0-4)
    ptsKZ = isKillzone ? 4 : 0

    ptsPD + ptsHTF + ptsSweep + ptsKZ

//#endregion Axis 3: Contextual Location

//#region Axis 4: Structural Alignment

scoreStructural(bool isBull) =>
    // Xu hướng cấu trúc thị trường (0-10)
    bullStruct = not na(prevSwingHigh) and not na(prevSwingLow) and lastSwingHigh > prevSwingHigh and lastSwingLow > prevSwingLow
    bearStruct = not na(prevSwingHigh) and not na(prevSwingLow) and lastSwingHigh < prevSwingHigh and lastSwingLow < prevSwingLow

    ptsMkt = 0
    if isBull
        ptsMkt := bullStruct ? 10 : bearStruct ? 0 : 2
    else
        ptsMkt := bearStruct ? 10 : bullStruct ? 0 : 2

    // Căn chỉnh EMA (0-8)
    ptsEma = 0
    if isBull and close > ema50
        ptsEma := 8
    else if not isBull and close < ema50
        ptsEma := 8

    // BOS/CHoCH gần đây (0-7)
    ptsBos = 2
    if not na(lastBosBar) and (bar_index - lastBosBar) <= bosLb
        if isBull and lastBosBull
            ptsBos := 7
        else if not isBull and not lastBosBull
            ptsBos := 7
        else
            ptsBos := 0

    ptsMkt + ptsEma + ptsBos

//#endregion Axis 4: Structural Alignment

//#region Weighted Total

calcWeightedTotal(int dsp, int vol, int ctx, int str_score) =>
    wSum                                                    = dispW + volW + ctxW + strW
    dW                                                      = wSum > 0 ? dispW : 25.0
    vW                                                      = wSum > 0 ? volW  : 25.0
    cW                                                      = wSum > 0 ? ctxW  : 25.0
    sW                                                      = wSum > 0 ? strW  : 25.0
    ws                                                      = wSum > 0 ? wSum : 100.0
    weighted                                                = (dsp / 25.0) * dW + (vol / 25.0) * vW + (ctx / 25.0) * cW + (str_score / 25.0) * sW
    result                                                  = math.round(weighted * (100.0 / ws))
    math.min(result, 100)

getGrade(int dsp, int vol, int ctx, int str_score) =>
    aPass                                          = (not aReqDisp or dsp >= aMinDisp) and (not aReqVol or vol >= aMinVol) and (not aReqCtx or ctx >= aMinCtx) and (not aReqStr or str_score >= aMinStr)
    bPass                                          = (not bReqDisp or dsp >= bMinDisp) and (not bReqVol or vol >= bMinVol) and (not bReqCtx or ctx >= bMinCtx) and (not bReqStr or str_score >= bMinStr)
    cPass                                          = (not cReqDisp or dsp >= cMinDisp) and (not cReqVol or vol >= cMinVol) and (not cReqCtx or ctx >= cMinCtx) and (not cReqStr or str_score >= cMinStr)
    aPass ? "A" : bPass ? "B" : cPass ? "C" : "D"

//#endregion Weighted Total

//#region FVG DETECTION

bullFVG = low > high[2]
bearFVG = high < low[2]

//#endregion FVG DETECTION

//#region TOOLTIP BUILDER

buildTooltip(int dsp, int vol, int ctx, int str_score, int total, string g, string st) =>
    str.format("Điểm Chất Lượng FVG: {0}/100 ({1})\n──────────────────\nĐộ Dịch Chuyển: {2}/25\nDelta Khối Lượng: {3}/25\nBối Cảnh:        {4}/25\nCấu Trúc:        {5}/25\n──────────────────\nTrạng Thái: {6}", total, g, dsp, vol, ctx, str_score, st)

//#endregion TOOLTIP BUILDER

//#endregion FUNCTIONS

//#region EXECUTION

//#region CREATE NEW FVGs

isBullDetect = bullFVG
fvgTop       = isBullDetect ? low : low[2]
fvgBottom    = isBullDetect ? high[2] : high

// Gọi hàm chấm điểm mỗi nến để đảm bảo tính nhất quán series
dspScore   = scoreDisplacement(isBullDetect)
volScore   = scoreVolumeDelta(isBullDetect)
ctxScore   = scoreContextual(isBullDetect, fvgTop, fvgBottom)
strScore   = scoreStructural(isBullDetect)
totalScore = calcWeightedTotal(dspScore, volScore, ctxScore, strScore)
gradeVal   = getGrade(dspScore, volScore, ctxScore, strScore)

if bullFVG or bearFVG
    // Cập nhật thống kê dashboard
    switch gradeVal
        "A" => aTotal += 1
        "B" => bTotal += 1
        "C" => cTotal += 1
        "D" => dTotal += 1

    newFvg = FVG.new(bar_index, fvgTop, fvgBottom, isBullDetect, STATE_FRESH, dspScore, volScore, ctxScore, strScore, totalScore, gradeVal)
    fvgArr.push(newFvg)

//#endregion CREATE NEW FVGs

//#region LIFECYCLE STATE MANAGEMENT

if fvgArr.size() > 0
    for i = fvgArr.size() - 1 to 0
        fvg = fvgArr.get(i)

        if fvg.state == STATE_MIT
            continue

        isBull = fvg.isBullish
        top    = fvg.top
        bot    = fvg.bottom

        // Kiểm tra vô hiệu hóa
        mitigated = false
        if mitMethod == "Close"
            mitigated := isBull ? close < bot : close > top
        else
            mitigated := isBull ? low < bot : high > top

        if mitigated
            fvg.state := STATE_MIT
            fvg.mitigatedAt := bar_index
            barsAlive = bar_index - fvg.formedAt
            switch fvg.grade
                "A" =>
                    aBarsSum += barsAlive
                    aBarsCount += 1
                "B" =>
                    bBarsSum += barsAlive
                    bBarsCount += 1
                "C" =>
                    cBarsSum += barsAlive
                    cBarsCount += 1
                "D" =>
                    dBarsSum += barsAlive
                    dBarsCount += 1
            continue

        // Kiểm tra lấp một phần
        partial = false
        if isBull
            partial := close < top and close > bot
        else
            partial := close > bot and close < top

        if partial and fvg.state != STATE_PARTIAL
            fvg.state := STATE_PARTIAL

        // Kiểm tra đã được test (giá chạm biên nhưng chưa vào)
        else if not partial and fvg.state == STATE_FRESH
            tested = false
            if isBull
                tested := low <= top and low[1] >= top
            else
                tested := high >= bot and high[1] <= bot
            if tested
                fvg.state := STATE_TESTED

        // Hoàn nguyên từ partial về tested nếu giá thoát vùng
        else if not partial and fvg.state == STATE_PARTIAL
            fvg.state := STATE_TESTED

//#endregion LIFECYCLE STATE MANAGEMENT

//#region MEMORY MANAGEMENT

while fvgArr.size() > 200
    fvgArr.shift()

//#endregion MEMORY MANAGEMENT

//#endregion EXECUTION

//#region VISUALS

//#region DEFERRED DRAWING (barstate.islast only)

var drawnBoxes  = array.new<box>()
var drawnLabels = array.new<label>()

if barstate.islast
    // Xóa toàn bộ đồ họa cũ
    if drawnBoxes.size() > 0
        for i = 0 to drawnBoxes.size() - 1
            box.delete(drawnBoxes.get(i))
    if drawnLabels.size() > 0
        for i = 0 to drawnLabels.size() - 1
            label.delete(drawnLabels.get(i))
    drawnBoxes.clear()
    drawnLabels.clear()

    // Vẽ lại toàn bộ FVG từ mảng
    if fvgArr.size() > 0
        for i = 0 to fvgArr.size() - 1
            fvg = fvgArr.get(i)

            if not passesFilter(fvg.grade)
                continue

            if fvg.state == STATE_MIT and not showHist
                continue

            gradeCol  = gradeColor(fvg.grade)
            bgCol     = color.new(gradeCol, 80)
            borderCol = color.new(gradeCol, 50)

            // Mờ khi lấp một phần
            if fvg.state == STATE_PARTIAL
                bgCol := color.new(gradeCol, 90)

            // Xám khi đã vô hiệu
            if fvg.state == STATE_MIT
                bgCol     := color.new(colD, 90)
                borderCol := color.new(colD, 70)
                gradeCol  := color.new(colD, 50)

            // Cạnh phải: đóng tại mitigatedAt hoặc kéo dài nếu còn hoạt động
            rightEdge = not na(fvg.mitigatedAt) ? fvg.mitigatedAt : bar_index + EXTEND_BARS

            box bx = box.new(fvg.formedAt - 2, fvg.top, rightEdge, fvg.bottom, border_color = borderCol, bgcolor = bgCol, border_width = 1, text = fvg.grade, text_color = gradeCol, text_size = size.normal, text_halign = text.align_center, text_valign = text.align_center)
            drawnBoxes.push(bx)

            if showLabels
                ttText   = buildTooltip(fvg.dispScore, fvg.volScore, fvg.ctxScore, fvg.strScore, fvg.totalScore, fvg.grade, fvg.state)
                label lb = label.new(rightEdge, (fvg.top + fvg.bottom) / 2.0, str.tostring(fvg.totalScore), style = label.style_label_left, color = color.new(gradeCol, 60), textcolor = gradeCol, size = size.small, tooltip = ttText)
                drawnLabels.push(lb)

//#endregion DEFERRED DRAWING (barstate.islast only)

//#region DASHBOARD

var table dash = na

if showDash and na(dash)
    dash := table.new(dashPosition(), 4, 7, border_width = 1, border_color = color.new(color.gray, 60), frame_width = 2, frame_color = color.new(color.gray, 40))

    txtSize = dashTextSize()

    // Tiêu đề
    table.cell(dash, 0, 0, "Chấm Điểm Chất Lượng FVG", text_color = color.white, text_size = txtSize, bgcolor = color.new(DASH_DARK, 0), text_halign = text.align_center)
    table.merge_cells(dash, 0, 0, 3, 0)

    // Tiêu đề cột
    table.cell(dash, 0, 1, "Cấp",              text_color = color.white, text_size = txtSize, bgcolor = color.new(DASH_MID, 0), text_halign = text.align_center)
    table.cell(dash, 1, 1, "Hoạt Động",        text_color = color.white, text_size = txtSize, bgcolor = color.new(DASH_MID, 0), text_halign = text.align_center)
    table.cell(dash, 2, 1, "Tổng",             text_color = color.white, text_size = txtSize, bgcolor = color.new(DASH_MID, 0), text_halign = text.align_center)
    table.cell(dash, 3, 1, "TB Thời Gian Lấp", text_color = color.white, text_size = txtSize, bgcolor = color.new(DASH_MID, 0), text_halign = text.align_center, tooltip = "Số nến trung bình từ khi FVG hình thành đến khi bị vô hiệu hóa")

    // Chữ cái cấp (tĩnh)
    rowBg                                  = color.new(DASH_ROW, 0)
    table.cell(dash, 0, 2, "A", text_color = colA, text_size = txtSize, bgcolor = rowBg, text_halign = text.align_center)
    table.cell(dash, 0, 3, "B", text_color = colB, text_size = txtSize, bgcolor = rowBg, text_halign = text.align_center)
    table.cell(dash, 0, 4, "C", text_color = colC, text_size = txtSize, bgcolor = rowBg, text_halign = text.align_center)
    table.cell(dash, 0, 5, "D", text_color = colD, text_size = txtSize, bgcolor = rowBg, text_halign = text.align_center)

if showDash and barstate.islast
    txtSize = dashTextSize()
    rowBg   = color.new(DASH_ROW, 0)

    // Đếm hoạt động theo cấp
    aActive            = 0
    bActive            = 0
    cActive            = 0
    dActive            = 0
    float nearestADist = na
    nearestAAbove      = false

    for i = 0 to fvgArr.size() - 1
        fvg = fvgArr.get(i)
        if fvg.state != STATE_MIT
            switch fvg.grade
                "A" => aActive += 1
                "B" => bActive += 1
                "C" => cActive += 1
                "D" => dActive += 1

            // Theo dõi FVG-A gần nhất
            if fvg.grade == "A"
                isInside = close <= fvg.top and close >= fvg.bottom
                dist     = 0.0
                isAbove  = false
                if isInside
                    dist := 0.0
                else
                    isAbove := fvg.bottom > close
                    edge = isAbove ? fvg.bottom : fvg.top
                    dist := math.abs(close - edge)
                if na(nearestADist) or dist < nearestADist
                    nearestADist  := dist
                    nearestAAbove := isAbove

    // Cập nhật ô động
    table.cell(dash, 1, 2, str.tostring(aActive), text_color         = color.white, text_size = txtSize, bgcolor = rowBg, text_halign = text.align_center)
    table.cell(dash, 2, 2, str.tostring(aTotal), text_color          = color.white, text_size = txtSize, bgcolor = rowBg, text_halign = text.align_center)
    table.cell(dash, 3, 2, avgBars(aBarsSum, aBarsCount), text_color = color.white, text_size = txtSize, bgcolor = rowBg, text_halign = text.align_center)

    table.cell(dash, 1, 3, str.tostring(bActive), text_color         = color.white, text_size = txtSize, bgcolor = rowBg, text_halign = text.align_center)
    table.cell(dash, 2, 3, str.tostring(bTotal), text_color          = color.white, text_size = txtSize, bgcolor = rowBg, text_halign = text.align_center)
    table.cell(dash, 3, 3, avgBars(bBarsSum, bBarsCount), text_color = color.white, text_size = txtSize, bgcolor = rowBg, text_halign = text.align_center)

    table.cell(dash, 1, 4, str.tostring(cActive), text_color         = color.white, text_size = txtSize, bgcolor = rowBg, text_halign = text.align_center)
    table.cell(dash, 2, 4, str.tostring(cTotal), text_color          = color.white, text_size = txtSize, bgcolor = rowBg, text_halign = text.align_center)
    table.cell(dash, 3, 4, avgBars(cBarsSum, cBarsCount), text_color = color.white, text_size = txtSize, bgcolor = rowBg, text_halign = text.align_center)

    table.cell(dash, 1, 5, str.tostring(dActive), text_color         = color.white, text_size = txtSize, bgcolor = rowBg, text_halign = text.align_center)
    table.cell(dash, 2, 5, str.tostring(dTotal), text_color          = color.white, text_size = txtSize, bgcolor = rowBg, text_halign = text.align_center)
    table.cell(dash, 3, 5, avgBars(dBarsSum, dBarsCount), text_color = color.white, text_size = txtSize, bgcolor = rowBg, text_halign = text.align_center)

    // Hàng FVG-A gần nhất
    nearestTxt = "FVG-A Gần Nhất: "
    if not na(nearestADist)
        if nearestADist == 0.0
            nearestTxt += "Đang trong vùng"
        else
            arrow = nearestAAbove ? "↑" : "↓"
            nearestTxt += arrow + " " + str.tostring(nearestADist, format.mintick) + " cách xa"
    else
        nearestTxt += "Không có"

    table.cell(dash, 0, 6, nearestTxt, text_color = colA, text_size = txtSize, bgcolor = color.new(DASH_DARK, 0), text_halign = text.align_center)
    table.merge_cells(dash, 0, 6, 3, 6)

//#endregion DASHBOARD

//#endregion VISUALS

//#region ALERTS

newAGrade = false
newBGrade = false

if bullFVG or bearFVG
    if fvgArr.size() > 0
        latest = fvgArr.last()
        if latest.formedAt == bar_index
            newAGrade := latest.grade == "A"
            newBGrade := latest.grade == "B"

alertcondition(newAGrade, "FVG Mới Cấp A", "Phát hiện FVG cấp A mới trên {{ticker}} {{interval}}")
alertcondition(newBGrade, "FVG Mới Cấp B", "Phát hiện FVG cấp B mới trên {{ticker}} {{interval}}")

//#endregion ALERTS

				
			

Indicator Insider

Smart Money Concepts Pro – OB, FVG, Liquidity + Trade Setups

Chỉ báo dựa trên tư duy Smart Money Concepts (SMC): giá di chuyển vì mất cân bằng cung–cầu và hành trình đi tìm thanh khoản. Từ đó, ta tập trung vào 4 “cột mốc” quan trọng:
Order Block (OB)
Fair Value Gap (FVG)
Liquidity Pools (EQH/EQL)

Elliott Wave

Chỉ báo này dựa trên Lý thuyết sóng Elliott (Elliott Wave Theory) – một trong những nguyên lý phân tích kỹ thuật nổi tiếng nhất, cho rằng thị trường di chuyển theo các mô hình sóng có thể dự đoán được.

Volume Profile, Pivot Anchored by DGT

Chỉ báo này dựa trên nguyên lý “Khối lượng giao dịch tập trung tại mức giá quan trọng” kết hợp với “Lý thuyết điểm xoay chiều” (Pivot Points).
Lý thuyết tin tưởng: Khi giá quay lại các vùng có khối lượng giao dịch lớn trong quá khứ (đặc biệt