Isolated Scroll Container

1. Mô tả chỉ báo – Structural Leg Profiler [LuxAlgo]

Phát hiện các leg cấu trúc (sóng tăng/giảm), sau đó xây dựng Volume Profile riêng cho từng leg dựa trên dữ liệu 1 phút để tiết lộ vùng giá được giao dịch nhiều nhất trong mỗi đợt sóng

1.1 Concept của chỉ báo

Hầu hết các chỉ báo khối lượng truyền thống (như Volume Profile cổ điển) chỉ cho thấy khối lượng giao dịch ở đâu trên toàn bộ biểu đồ hoặc phiên. Structural Leg Profiler tiếp cận khác hơn: nó chia biểu đồ thành từng leg cấu trúc riêng biệt — mỗi leg là một đợt di chuyển giá từ một đỉnh/đáy swing đến đỉnh/đáy swing tiếp theo — và xây dựng một Volume Profile riêng cho từng leg.

Ý tưởng cốt lõi: mỗi leg đại diện cho một "chương" của hoạt động thị trường. Bằng cách nhìn vào phân phối khối lượng bên trong từng chương đó, trader có thể trả lời câu hỏi: "Phần lớn khối lượng của đợt tăng này được giao dịch ở đáy, hay ở đỉnh?" Mức giá có khối lượng cao nhất trong một leg được gọi là Point of Control (POC) — điểm thị trường thể hiện sự quan tâm nhiều nhất trong đợt di chuyển đó.

Để có dữ liệu khối lượng chính xác nhất, chỉ báo lấy dữ liệu nến 1 phút từ nguồn dữ liệu khung thời gian thấp hơn của TradingView (bất kể bạn đang dùng khung thời gian nào). Khối lượng mỗi nến 1 phút được chia thành khối lượng muakhối lượng bán ước tính dựa trên vị trí giá đóng cửa trong phạm vi cao-thấp của nến. Điều này cho cái nhìn rõ hơn nhiều so với chỉ dùng khối lượng thô.

Ngoài các profile, chỉ báo còn đánh dấu các nến khối lượng bất thường (đột biến khối lượng quá cao) bằng bong bóng tròn màu, giúp nhận biết ngay lập tức các nến có thể là dấu hiệu hoạt động của smart money.

1.2 Chức năng của chỉ báo

  • Các ô Volume Profile theo từng leg cấu trúc: Với mỗi leg (tăng hoặc giảm) được xác nhận, một cột các ô ngang được vẽ bao phủ toàn bộ vùng giá của leg. Mỗi ô hiển thị biểu đồ ASCII thu nhỏ (ký tự ô đặc █ và ký tự trống ░) thể hiện khối lượng tương đối tại mức giá đó, kèm số liệu khối lượng thực nếu bật tùy chọn.
  • Đường POC (Point of Control): Một đường ngang đứt gạch đánh dấu mức giá có khối lượng cao nhất trong leg. Đây là mức giá quan trọng nhất của đợt di chuyển đó.
  • Kéo dài POC chưa được kiểm tra: Các đường POC có thể tùy chọn kéo dài sang phải cho đến khi giá thực sự quay lại và chạm vào mức đó. Tối đa 2 bull POC chưa tested và 2 bear POC chưa tested được duy trì cùng lúc để tránh rối.
  • Nhãn tóm tắt mỗi leg: Tại điểm giữa của mỗi leg, một nhãn nhỏ hiển thị: tổng khối lượng leg, delta (khối lượng mua trừ khối lượng bán, có dấu + hoặc −), và giá POC.
  • Đường cấu trúc chấm chấm: Đường chấm xám nối đỉnh/đáy đầu với đỉnh/đáy cuối của mỗi leg, làm rõ cấu trúc ZigZag.
  • Hai chế độ tô màu ô: "Volume Gradient" tô màu theo gradient 3 cấp dựa trên vị trí tương đối so với POC. "Delta (Buy vs Sell)" tô màu xanh lá nếu mua chiếm ưu thế, đỏ nếu bán chiếm ưu thế tại mức giá đó.
  • Bong bóng khối lượng bất thường (Volume Bubbles): Trên nến có khối lượng vượt ngưỡng bội số so với SMA-20, một vòng tròn màu được vẽ tại điểm giữa (hl2) của nến. Vòng tròn to hơn khi khối lượng đột biến mạnh hơn.

1.3 Cách dùng chỉ báo

  • Xác định vùng giá khối lượng cao trong từng leg: Những ô có nhiều ký tự đặc nhất (█) trong profile thể hiện nơi thị trường dành nhiều thời gian và giao dịch nhiều nhất. Các mức này thường hoạt động như hỗ trợ/kháng cự khi giá quay lại.
  • Giao dịch tại retest POC: Khi giá quay lại mức POC của một leg (đặc biệt là POC chưa tested đang được kéo dài), đây là vùng có xác suất phản ứng cao. POC của bull leg thường là hỗ trợ khi giá pullback về.
  • Đọc nhãn Delta để đánh giá sức mạnh: Delta dương lớn trên bull leg (ví dụ "+2.5M") có nghĩa người mua chiếm ưu thế rõ ràng. Delta gần 0 hoặc âm trên bull leg báo hiệu đợt tăng yếu hoặc có phân phối.
  • Dùng màu Delta để phát hiện hấp thụ/phân phối: Trong bull leg, nếu các ô gần đỉnh đột biến màu đỏ (bán chiếm ưu thế), đó là dấu hiệu người bán đang hấp thụ lực mua tại đó — cảnh báo đảo chiều tiềm năng.
  • Bong bóng khối lượng đánh dấu điểm bùng phát: Bong bóng lớn thường trùng với đỉnh swing, điểm bứt phá, hoặc đảo chiều. Bong bóng tăng trong xu hướng giảm hoặc bong bóng giảm trong xu hướng tăng đặc biệt đáng chú ý.
  • Điều chỉnh Swing Multiplier phù hợp với phong cách giao dịch: Giá trị cao hơn tạo ít leg lớn hơn (phù hợp swing trader). Giá trị thấp hơn bắt micro-swing (phù hợp scalper/intraday). Tránh đặt quá thấp vì sẽ tạo quá nhiều leg nhỏ chồng chéo.

1.4 Cách hoạt động của chỉ báo

Đầu vào & vai trò
  • atrPeriod (Số nguyên, mặc định 5): Độ dài ATR để đo biến động gần đây. Giá trị nhỏ hơn (ví dụ 3) phản ứng nhanh hơn với swing gần nhất; lớn hơn (ví dụ 14) tạo ngưỡng đảo chiều mượt mà hơn.
  • swingMult (Số thập phân, mặc định 2.5): Điều khiển chính về độ nhạy. Chỉ báo kích hoạt pivot mới khi giá đảo chiều ATR(atrPeriod) × swingMult. Giá trị cao (ví dụ 4.0) tạo leg lớn hơn, ít hơn. Giá trị thấp (ví dụ 1.0) bắt micro-swing nhỏ. Quá thấp sẽ tạo rất nhiều leg nhỏ chồng chéo.
  • maxBoxes (Số nguyên, mặc định 50, từ 10–150): Số ô ngang tối đa mỗi profile. Nếu phạm vi giá leg cần nhiều ô hơn giới hạn này, chiều cao ô được tự động tăng lên để vừa. Nhiều ô hơn = phân giải cao hơn nhưng dày đặc hơn.
  • alignMode (Left / Right / Center): Căn chỉnh văn bản ASCII bên trong mỗi ô về bên trái, phải, hoặc giữa.
  • showBoxVol (Tick, mặc định bật): Khi bật, hiển thị số liệu khối lượng thực sau thanh ASCII trong mỗi ô. Tắt để profile gọn hơn.
  • extendPoc (Tick, mặc định bật): Khi bật, đường POC của mỗi leg được kéo dài sang phải cho đến khi giá chạm vào mức đó. Giới hạn tối đa 2 bull POC và 2 bear POC chưa tested cùng lúc.
  • showBubbles (Tick, mặc định bật): Bật/tắt bong bóng khối lượng bất thường.
  • bubbleMult (Số thập phân, mặc định 2.0): Ngưỡng đột biến khối lượng. Khối lượng nến phải ít nhất bằng bội số này so với SMA-20 mới hiển thị bong bóng. Đặt 3.0 chỉ hiện đột biến cực mạnh; đặt 1.5 hiện thường xuyên hơn.
  • colorMode (Volume Gradient / Delta): Chế độ tô màu cho các ô profile.
  • upStartC / upMidC / upEndC: Ba điểm màu gradient cho leg tăng — từ khối lượng thấp đến cao. Mặc định xanh đậm → xanh sáng → xanh ngọc (cyan).
  • dnStartC / dnMidC / dnEndC: Tương tự cho leg giảm. Mặc định tím đậm → đỏ hồng → đỏ.
Các khối logic chính

🔷 Flow 1 – Thu thập dữ liệu khung thời gian thấp hơn (nến 1 phút)

Mỗi nến biểu đồ, chỉ báo lấy tất cả nến 1 phút con bằng request.security_lower_tf. Với mỗi nến 1 phút, ước tính khối lượng mua/bán:

buyVol = tổng khối lượng × (giá đóng − giá thấp) / (giá cao − giá thấp)
sellVol = tổng khối lượng × (giá cao − giá đóng) / (giá cao − giá thấp)
Nến đóng gần đỉnh → phần lớn khối lượng quy cho người mua. Nến đóng gần đáy → phần lớn quy cho người bán. Đây là ước tính đơn giản hóa, không cần dữ liệu order flow thực.

Toàn bộ nến 1 phút được lưu trong danh sách ltfHistory (giới hạn 90,000 mục). Khi khung thời gian quá cao để lấy dữ liệu 1m, chỉ báo tự dùng dữ liệu nến biểu đồ thay thế.

🔷 Flow 2 – Phát hiện Swing (ZigZag dựa trên ATR)

Chỉ báo theo dõi hướng qua biến zzDir (1 = đang tìm đỉnh, -1 = đang tìm đáy). Ngưỡng đảo chiều:

revAmount = ATR(atrPeriod) × swingMult
Hướng hiện tạiĐiều kiện kích hoạtHành động
Upswing (zzDir = 1) Giá tiếp tục lập đỉnh mới → cập nhật zzHigh. Khi giá thấp nhất giảm quá revAmount so với zzHigh → xác nhận swing high. Đặt isSwingHigh = true. Ghi nhận zzHigh là pivot. Đổi zzDir = -1.
Downswing (zzDir = -1) Giá tiếp tục lập đáy mới → cập nhật zzLow. Khi giá cao nhất tăng quá revAmount so với zzLow → xác nhận swing low. Đặt isSwingLow = true. Ghi nhận zzLow là pivot. Đổi zzDir = 1.

Ví dụ: ATR = 10, swingMult = 2.5 → revAmount = 25. Giá tăng từ 1000 lên 1080 (zzHigh = 1080). Sau đó giá giảm xuống 1054 (= 1080 − 26, vượt ngưỡng 25) → swing high xác nhận tại 1080.

🔷 Flow 3 – Xử lý Leg (Kích hoạt xây dựng Profile)

Khi pivot được xác nhận, chỉ báo gọi f_processLeg() cho leg vừa hoàn thành:

  • Vẽ đường chấm xám cấu trúc từ lastPivotBar đến pivot mới.
  • Lọc ltfHistory, lấy các nến 1 phút có chartBar nằm trong khoảng thời gian của leg.
  • Tính tổng buy volume, sell volume, high thực, low thực từ tất cả nến 1m trong leg.
  • Tính chiều cao ô mục tiêu: ATR(14) / 10. Nếu phạm vi leg cần nhiều ô hơn maxBoxes, chiều cao ô được tăng lên để vừa.
  • Gọi f_createProfile() để xây dựng phân phối, sau đó profile.draw() để vẽ.
  • Cắt bỏ ltfHistory, chỉ giữ lại nến từ pivot mới trở đi (dữ liệu cũ bị loại bỏ để tiết kiệm bộ nhớ).

🔷 Flow 4 – Xây dựng Volume Profile

Trong f_createProfile(), phạm vi giá của leg được chia thành N ô ngang. Với mỗi nến 1 phút và mỗi ô, tính độ chồng lấp giữa phạm vi nến 1m và phạm vi giá của ô:

overlapRatio = chiều cao chồng lấp / phạm vi nến 1m
volContrib = khối lượng nến 1m × overlapRatio
deltaContrib = (buyVol − sellVol) × overlapRatio

Ví dụ: nến 1m có phạm vi 10 điểm, khối lượng 500. Một ô giá chồng lấp 4 điểm → overlapRatio = 0.4 → ô đó nhận 200 đơn vị khối lượng từ nến này. Tất cả nến 1m đóng góp vào tất cả ô chồng lấp.

Sau khi xử lý xong, ô có tổng khối lượng cao nhất là POC, giá trung tâm ô đó là pocPrice.

🔷 Flow 5 – Vẽ Profile (Ô màu, Đường POC, Nhãn tóm tắt)

Trong profile.draw(), ba loại đối tượng biểu đồ được tạo:

  • Ô giá: Một box.new() cho mỗi mức giá. Văn bản bên trong là thanh ASCII như ████████░░░░░ 1.23K. Số ký tự đặc tỉ lệ với khối lượng ô so với ô POC. Màu sắc theo chế độ được chọn:
    • Volume Gradient: Ô bên dưới POC dùng gradient startC → midC; ô bên trên POC dùng midC → endC. Ô POC dùng midC.
    • Delta: Xanh lá nếu delta dương (mua > bán), đỏ nếu delta âm (bán > mua), xám nếu bằng nhau.
  • Đường POC đứt gạch: Đường ngang tại pocPrice, vẽ từ đầu đến cuối leg (và có thể được kéo dài tiếp bởi hệ thống POC Extension).
  • Nhãn tóm tắt: Nhãn nhỏ tại điểm giữa leg, hiển thị ở phía trên (bull leg) hoặc phía dưới (bear leg) profile. Nội dung: tổng khối lượng leg, delta có dấu, giá POC.

🔷 Flow 6 – Kéo dài POC chưa được kiểm tra

Khi extendPoc bật, mỗi POC mới được thêm vào danh sách untestedPocs. Mỗi nến:

  • Kiểm tra xem giá có cắt qua POC không (high >= poc.price AND low <= poc.price). Nếu có → POC đã "tested" → dừng đường tại nến hiện tại, xóa khỏi danh sách.
  • Nếu chưa tested → kéo dài đường POC đến nến hiện tại.
  • Giới hạn tối đa 2 bull POC và 2 bear POC chưa tested. Nếu vượt quá → cắt bỏ POC cũ nhất tại điểm kết thúc gốc của nó.

🔷 Flow 7 – Bong bóng khối lượng bất thường

  • Tính volSma = SMA(volume, 20)volRatio = volume / volSma.
  • Nếu volRatio >= bubbleMult → vẽ vòng tròn tại hl2 của nến.
  • Màu bong bóng: xanh ngọc (cyan) cho nến tăng, đỏ cho nến giảm.
  • Kích thước bong bóng: giới hạn(volRatio × 4, 6, 20). Đột biến 3× → kích thước 12; 5× → kích thước tối đa 20.
  • Nếu volRatio >= bubbleMult × 1.5 → hiển thị số khối lượng thực bên trong vòng tròn.
Đầu ra & vai trò trong sử dụng
  • Cột ô Volume Profile: "Biểu đồ cột nằm ngang" cho mỗi leg. Ô nhiều ký tự đặc nhất = khối lượng cao nhất tại mức giá đó. Xác định vùng giá trị và POC trong mỗi đợt di chuyển cấu trúc.
  • Đường POC đứt gạch: Mức giá quan trọng nhất trong mỗi leg. Hoạt động như nam châm — giá có xu hướng quay lại phản ứng tại POC. POC chưa tested được kéo dài trở thành vùng hỗ trợ/kháng cự hướng về tương lai.
  • Nhãn tóm tắt (Leg Vol / Delta / POC): Ảnh chụp nhanh sức mạnh của leg. Delta dương lớn xác nhận người mua chiếm ưu thế. Delta âm hoặc gần 0 trên bull leg báo hiệu yếu kém hoặc phân phối.
  • Đường cấu trúc chấm chấm: Hiển thị điểm bắt đầu và kết thúc của mỗi leg, làm rõ cấu trúc ZigZag.
  • Bong bóng khối lượng bất thường: Dấu hiệu tròn trên nến khối lượng cực cao. Thường trùng với điểm quyết định quan trọng — đảo chiều, bứt phá, hoặc hấp thụ. Bong bóng càng to → đột biến càng mạnh.
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/
// © LuxAlgo
// Việt hóa bởi: AI Code Bot — chỉ dịch label/tooltip/group, giữ nguyên toàn bộ logic gốc

//@version=6
indicator("Structural Leg Profiler [LuxAlgo]", "LuxAlgo - Leg Profiler", overlay=true, max_boxes_count=500, max_lines_count=500, max_labels_count=500)

//---------------------------------------------------------------------------------------------------------------------}
// Inputs
//---------------------------------------------------------------------------------------------------------------------{
string GRP_SWING   = "📊 Phát Hiện Swing"
int    atrPeriod   = input.int(5, "Chu Kỳ ATR",
     group=GRP_SWING,
     tooltip="Số nến để tính ATR (Average True Range - biên độ biến động trung bình). Giá trị nhỏ hơn = phản ứng nhanh hơn với biến động gần đây. Khuyến nghị: 3–7.")

float  swingMult   = input.float(2.5, "Hệ Số Swing (ATR)",
     step=0.1,
     group=GRP_SWING,
     tooltip="Điều chỉnh kích thước tối thiểu của mỗi leg cấu trúc. Chỉ báo xác nhận swing mới khi giá đảo chiều ít nhất ATR x hệ số này. Giá trị cao hơn = leg lớn hơn, ít hơn (phù hợp swing trader). Giá trị thấp hơn = bắt micro-swing nhỏ (phù hợp scalper). Khuyến nghị: 1.5–4.0.")

string GRP_PROFILE = "⚙️ Cài Đặt Profile"
int    maxBoxes    = input.int(50, "Số Ô Profile Tối Đa",
     minval=10, maxval=150,
     group=GRP_PROFILE,
     tooltip="Số ô ngang tối đa được vẽ trong mỗi Volume Profile. Nếu phạm vi giá của leg quá lớn, chiều cao ô sẽ tự động tăng để vừa trong giới hạn này. Nhiều ô hơn = chi tiết hơn nhưng nặng hơn. Khuyến nghị: 30–80.")

string alignMode   = input.string("Left", "Căn Chỉnh Profile",
     options=["Left", "Right", "Center"],
     group=GRP_PROFILE,
     tooltip="Căn chỉnh văn bản ASCII bên trong mỗi ô profile. Left: căn trái. Right: căn phải. Center: căn giữa.")

bool   showBoxVol  = input.bool(true, "Hiện Số Khối Lượng Trong Ô",
     group=GRP_PROFILE,
     tooltip="Bật để hiển thị số liệu khối lượng thực (ví dụ: 1.23K) sau thanh ASCII trong mỗi ô. Tắt nếu muốn profile gọn hơn về mặt thị giác.")

bool   extendPoc   = input.bool(true, "Kéo Dài POC Chưa Được Kiểm Tra",
     group=GRP_PROFILE,
     tooltip="Bật để kéo dài đường POC (Point of Control - mức giá khối lượng cao nhất) sang phải cho đến khi giá thực sự chạm vào mức đó. Tối đa 2 POC tăng và 2 POC giảm được duy trì cùng lúc. Hữu ích để xác định vùng hỗ trợ/kháng cự tiềm năng trong tương lai.")

string GRP_BUBBLES = "🫧 Bất Thường Khối Lượng"
bool   showBubbles = input.bool(true, "Hiện Bong Bóng Khối Lượng",
     group=GRP_BUBBLES,
     tooltip="Bật để hiển thị vòng tròn màu trên các nến có khối lượng đột biến bất thường. Các nến này thường đánh dấu điểm quyết định quan trọng như đảo chiều, bứt phá, hoặc hấp thụ của smart money.")

float  bubbleMult  = input.float(2.0, "Ngưỡng Đột Biến (x SMA)",
     step=0.1,
     group=GRP_BUBBLES,
     tooltip="Bội số ngưỡng để kích hoạt bong bóng. Khối lượng nến phải ít nhất bằng bội số này so với SMA-20 của khối lượng. VD: 2.0 = chỉ hiện khi KL gấp đôi trung bình. 3.0 = chỉ hiện đột biến cực mạnh. 1.5 = hiện thường xuyên hơn. Khuyến nghị: 1.8–2.5.")

string GRP_COLORS  = "🎨 Kiểu Dáng & Màu Sắc"
string colorMode   = input.string("Volume Gradient", "Chế Độ Màu Ô",
     options=["Volume Gradient", "Delta (Buy vs Sell)"],
     group=GRP_COLORS,
     tooltip="Chọn cách tô màu các ô profile. Volume Gradient: tô màu theo gradient 3 cấp dựa trên vị trí so với POC (xanh đậm → xanh sáng → cyan cho leg tăng). Delta Buy vs Sell: tô màu xanh lá nếu mua chiếm ưu thế, đỏ nếu bán chiếm ưu thế tại mức giá đó.")

color  upStartC    = input.color(color.new(#01579B, 0), "Leg Tăng (KL Thấp)",  group=GRP_COLORS, inline="up")
color  upMidC      = input.color(color.new(#2962FF, 0), "Leg Tăng (KL Trung)", group=GRP_COLORS, inline="up")
color  upEndC      = input.color(color.new(#00E5FF, 0), "Leg Tăng (KL Cao)",   group=GRP_COLORS, inline="up")

color  dnStartC    = input.color(color.new(#311B92, 0), "Leg Giảm (KL Thấp)",  group=GRP_COLORS, inline="dn")
color  dnMidC      = input.color(color.new(#C51162, 0), "Leg Giảm (KL Trung)", group=GRP_COLORS, inline="dn")
color  dnEndC      = input.color(color.new(#FF5252, 0), "Leg Giảm (KL Cao)",   group=GRP_COLORS, inline="dn")

//---------------------------------------------------------------------------------------------------------------------}
// Types
//---------------------------------------------------------------------------------------------------------------------{
type LTFBar
    int chartBar
    float h
    float l
    float v
    float buyV
    float sellV

type CandleProfile
    float profileHigh
    float profileLow
    int   numBoxes
    float boxHeight
    array<float> volumeDistribution
    array<float> deltaDistribution
    float maxVolumeInProfile
    int   maxVolumeIndex
    float pocPrice

type UntestedPOC
    line  pocLine
    float price
    bool  isBull
    int   endBar

//---------------------------------------------------------------------------------------------------------------------}
// State Variables
//---------------------------------------------------------------------------------------------------------------------{
var array<LTFBar>      ltfHistory   = array.new<LTFBar>()
var array<UntestedPOC> untestedPocs = array.new<UntestedPOC>()

var int   zzDir          = 1
var float zzHigh         = high
var int   zzHighBar      = bar_index
var float zzLow          = low
var int   zzLowBar       = bar_index
var int   lastPivotBar   = bar_index
var float lastPivotPrice = close

//---------------------------------------------------------------------------------------------------------------------}
// Methods / Functions
//---------------------------------------------------------------------------------------------------------------------{
f_buildVolumeBar(int filledBlocks, int totalBlocks) =>
    string result = ""
    for i = 1 to totalBlocks
        result += i <= filledBlocks ? "█" : "░"
    result

f_createProfile(float legHigh, float legLow, float targetBoxHeight, array<LTFBar> legBars) =>
    float barRange = legHigh - legLow
    int numBoxes = math.max(1, int(math.round(barRange / targetBoxHeight)))
    float adjustedBoxHeight = barRange / numBoxes
    array<float> volDist = array.new_float(numBoxes, 0.0)
    array<float> deltaDist = array.new_float(numBoxes, 0.0)

    for i = 0 to legBars.size() - 1
        LTFBar b = legBars.get(i)
        float ltfBarRange = math.max(b.h - b.l, syminfo.mintick)

        for boxIndex = 0 to numBoxes - 1
            float boxBottom = legLow + (boxIndex * adjustedBoxHeight)
            float boxTop = boxBottom + adjustedBoxHeight
            float overlapHeight = math.max(0, math.min(boxTop, b.h) - math.max(boxBottom, b.l))

            if overlapHeight > 0
                float overlapRatio = overlapHeight / ltfBarRange
                float volContrib = b.v * overlapRatio
                float buyContrib = b.buyV * overlapRatio
                float sellContrib = b.sellV * overlapRatio

                volDist.set(boxIndex, volDist.get(boxIndex) + volContrib)
                deltaDist.set(boxIndex, deltaDist.get(boxIndex) + (buyContrib - sellContrib))

    float maxVolume = volDist.max()
    int maxVolIdx = volDist.indexof(maxVolume)
    float pocPrice = legLow + (maxVolIdx * adjustedBoxHeight) + (adjustedBoxHeight / 2)

    CandleProfile.new(legHigh, legLow, numBoxes, adjustedBoxHeight, volDist, deltaDist, maxVolume, maxVolIdx, pocPrice)

method draw(CandleProfile this, int startBar, int endBar, bool isUpLeg, float totalBuyVol, float totalSellVol) =>
    int bLeft = startBar
    int bRight = math.max(startBar + 1, endBar)
    int totalBlocks = math.max(5, math.min(30, int(15 * ((bRight - bLeft) / 50.0))))

    color startC = isUpLeg ? upStartC : dnStartC
    color midC   = isUpLeg ? upMidC : dnMidC
    color endC   = isUpLeg ? upEndC : dnEndC

    string parsedAlign = alignMode == "Left" ? text.align_left : alignMode == "Right" ? text.align_right : text.align_center

    for i = 0 to this.numBoxes - 1
        float boxBottom = this.profileLow + (i * this.boxHeight)
        float boxTop = boxBottom + this.boxHeight
        float currentBoxVolume = this.volumeDistribution.get(i)
        float currentBoxDelta = this.deltaDistribution.get(i)

        if currentBoxVolume > 0
            int filledBlocks = this.maxVolumeInProfile > 0 ? int(math.round((currentBoxVolume / this.maxVolumeInProfile) * totalBlocks)) : 0
            string boxText = f_buildVolumeBar(filledBlocks, totalBlocks)
            if showBoxVol
                boxText += " " + str.tostring(currentBoxVolume, format.volume)

            color textColor = na

            if colorMode == "Delta (Buy vs Sell)"
                if currentBoxDelta > 0
                    textColor := color.from_gradient(currentBoxDelta, 0, this.maxVolumeInProfile, color.new(color.green, 60), color.new(color.green, 0))
                else if currentBoxDelta < 0
                    textColor := color.from_gradient(math.abs(currentBoxDelta), 0, this.maxVolumeInProfile, color.new(color.red, 60), color.new(color.red, 0))
                else
                    textColor := color.gray
            else
                if i < this.maxVolumeIndex
                    textColor := color.from_gradient(i, 0, this.maxVolumeIndex, startC, midC)
                else if i > this.maxVolumeIndex
                    textColor := color.from_gradient(i, this.maxVolumeIndex, this.numBoxes - 1, midC, endC)
                else
                    textColor := midC

            color bgC = color.new(textColor, 90)
            box.new(left=bLeft, top=boxTop, right=bRight, bottom=boxBottom, border_color=color(na), bgcolor=bgC, text=boxText, text_size=size.small, text_color=textColor, text_halign=parsedAlign, text_valign=text.align_center, text_wrap=text.wrap_none, text_font_family=font.family_monospace)

    line pocL = line.new(startBar, this.pocPrice, endBar, this.pocPrice, color=color.new(endC, 0), style=line.style_dashed, width=2)
    if extendPoc
        untestedPocs.push(UntestedPOC.new(pocL, this.pocPrice, isUpLeg, endBar))

    float delta = totalBuyVol - totalSellVol
    color deltaColor = delta > 0 ? color.green : color.red
    string deltaSign = delta > 0 ? "+" : ""

    // Nhãn tóm tắt: Tổng KL leg / Delta (mua - bán) / Giá POC
    string summaryText = "KL Leg: " + str.tostring(totalBuyVol + totalSellVol, format.volume) + "\n" +
                         "Delta: " + deltaSign + str.tostring(delta, format.volume) + "\n" +
                         "POC: " + str.tostring(this.pocPrice, format.mintick)

    int midBar = int(math.round((startBar + endBar) / 2))
    float summaryY = isUpLeg ? this.profileHigh : this.profileLow
    label.new(midBar, summaryY, text=summaryText, color=color.new(chart.bg_color, 20), style=isUpLeg ? label.style_label_down : label.style_label_up, textcolor=deltaColor, size=size.tiny)

f_processLeg(int startBar, int endBar, float startPrice, float endPrice, bool isUpLeg, array<LTFBar> history, float baseBoxHeight) =>
    line.new(startBar, startPrice, endBar, endPrice, color=color.new(color.gray, 50), width=2, style=line.style_dotted)

    array<LTFBar> legBars = array.new<LTFBar>()
    float totalBuy = 0.0
    float totalSell = 0.0
    float maxH = -1.0
    float minL = 99999999.0

    for i = 0 to history.size() - 1
        LTFBar b = history.get(i)
        if b.chartBar >= startBar and b.chartBar <= endBar
            legBars.push(b)
            totalBuy += b.buyV
            totalSell += b.sellV
            maxH := math.max(maxH, b.h)
            minL := math.min(minL, b.l)

    if legBars.size() > 0
        float groupRange = maxH - minL
        float targetBoxHeight = (groupRange / baseBoxHeight) > maxBoxes ? groupRange / maxBoxes : baseBoxHeight

        CandleProfile profile = f_createProfile(maxH, minL, targetBoxHeight, legBars)
        profile.draw(startBar, endBar, isUpLeg, totalBuy, totalSell)

    array<LTFBar> newHistory = array.new<LTFBar>()
    for i = 0 to history.size() - 1
        if history.get(i).chartBar >= endBar
            newHistory.push(history.get(i))
    newHistory

//---------------------------------------------------------------------------------------------------------------------}
// Data Retrieval & Accumulation
//---------------------------------------------------------------------------------------------------------------------{
[ltfOpens, ltfHighs, ltfLows, ltfCloses, ltfVolumes] = request.security_lower_tf(syminfo.tickerid, "1", [open, high, low, close, volume])

if ltfHighs.size() > 0
    for i = 0 to ltfHighs.size() - 1
        float h = ltfHighs.get(i)
        float l = ltfLows.get(i)
        float c = ltfCloses.get(i)
        float v = ltfVolumes.get(i)

        float rng = math.max(h - l, syminfo.mintick)
        float buyVol = v * (c - l) / rng
        float sellVol = v * (h - c) / rng

        ltfHistory.push(LTFBar.new(bar_index, h, l, v, buyVol, sellVol))
else
    float rng = math.max(high - low, syminfo.mintick)
    float buyVol = volume * (close - low) / rng
    float sellVol = volume * (high - close) / rng
    ltfHistory.push(LTFBar.new(bar_index, high, low, volume, buyVol, sellVol))

if ltfHistory.size() > 90000
    ltfHistory.shift()

//---------------------------------------------------------------------------------------------------------------------}
// Swing Logic & Profiling
//---------------------------------------------------------------------------------------------------------------------{
float globalAtr = ta.atr(atrPeriod)
float globalAtr14 = ta.atr(14)
float revAmount = globalAtr * swingMult

bool isSwingHigh = false
bool isSwingLow = false

if zzDir == 1
    if high > zzHigh
        zzHigh := high
        zzHighBar := bar_index
    if low < zzHigh - revAmount
        zzDir := -1
        zzLow := low
        zzLowBar := bar_index
        isSwingHigh := true
else
    if low < zzLow
        zzLow := low
        zzLowBar := bar_index
    if high > zzLow + revAmount
        zzDir := 1
        zzHigh := high
        zzHighBar := bar_index
        isSwingLow := true

if isSwingHigh
    float boxHeightLimit = globalAtr14 / 10
    ltfHistory := f_processLeg(lastPivotBar, zzHighBar, lastPivotPrice, zzHigh, true, ltfHistory, boxHeightLimit)
    lastPivotBar := zzHighBar
    lastPivotPrice := zzHigh

if isSwingLow
    float boxHeightLimit = globalAtr14 / 10
    ltfHistory := f_processLeg(lastPivotBar, zzLowBar, lastPivotPrice, zzLow, false, ltfHistory, boxHeightLimit)
    lastPivotBar := zzLowBar
    lastPivotPrice := zzLow

//---------------------------------------------------------------------------------------------------------------------}
// Untested POC Extensions
//---------------------------------------------------------------------------------------------------------------------{
if untestedPocs.size() > 0
    for i = untestedPocs.size() - 1 to 0
        UntestedPOC p = untestedPocs.get(i)
        bool isTested = high >= p.price and low <= p.price

        if isTested
            p.pocLine.set_x2(bar_index)
            untestedPocs.remove(i)
        else
            p.pocLine.set_x2(bar_index)

    if untestedPocs.size() > 0
        int bullCount = 0
        int bearCount = 0
        for i = untestedPocs.size() - 1 to 0
            UntestedPOC p = untestedPocs.get(i)
            if p.isBull
                bullCount += 1
                if bullCount > 2
                    p.pocLine.set_x2(p.endBar)
                    untestedPocs.remove(i)
            else
                bearCount += 1
                if bearCount > 2
                    p.pocLine.set_x2(p.endBar)
                    untestedPocs.remove(i)

//---------------------------------------------------------------------------------------------------------------------}
// Volume Anomalies
//---------------------------------------------------------------------------------------------------------------------{
if showBubbles
    float volSma = ta.sma(volume, 20)
    if volSma > 0
        float volRatio = volume / volSma
        if volRatio >= bubbleMult
            bool isBull = close >= open
            color bubColor = isBull ? color.new(upEndC, 40) : color.new(dnEndC, 40)
            int dynSize = int(math.max(6, math.min(volRatio * 4, 20)))
            string bubText = volRatio >= (bubbleMult * 1.5) ? str.tostring(volume, format.volume) : ""
            label.new(bar_index, hl2, text=bubText, color=bubColor, style=label.style_circle, textcolor=color.white, size=dynSize, tooltip="Khối lượng: " + str.tostring(volume, format.volume) + "\nĐột biến: " + str.tostring(volRatio, "#.##") + "x Trung bình")

//---------------------------------------------------------------------------------------------------------------------}

				
			

Indicator Insider

04 Structural Leg Profiler [LuxAlgo]

Isolated Scroll Container 1. Mô tả chỉ báo – Structural Leg Profiler [LuxAlgo] Phát hiện các leg cấu trúc (sóng tăng/giảm), sau đó xây dựng Volume Profile riêng cho từng leg dựa trên dữ liệu 1 phút để tiết lộ vùng giá được giao dịch nhiều nhất trong mỗi đợt

03 Ranked FVG

Isolated Scroll Container 1. Mô tả chỉ báo – Ranked FVG Imbalance Zones (Zeiierman) Phát hiện, xếp hạng và hiển thị các vùng mất cân bằng giá (FVG) mạnh nhất trên biểu đồ theo điểm chất lượng động 1.1 Concept của chỉ báo Fair Value Gap (FVG) – hay còn