Isolated Scroll Container

1. Mô Tả Chỉ Báo Liquidity Delta Profiler (LuxAlgo)

Chỉ báo vùng thanh khoản chia mỗi vùng Buy-Side/Sell-Side Liquidity thành 4 góc phần tư theo dõi volume delta, theo dõi "sức khoẻ" còn lại của từng vùng, phát hiện 4 mẫu hình đảo chiều riêng biệt trong lúc sweep, và kiểm chứng tỷ lệ thắng thực tế của từng loại tín hiệu bằng một hệ thống mô phỏng giao dịch tích hợp sẵn

1.1 Khái Niệm Chỉ Báo

Chỉ báo này được xây dựng quanh một ý tưởng Smart Money Concepts: phần bóng nến phía trên một đỉnh swing (hoặc phía dưới một đáy swing) đánh dấu một vùng lệnh chờ (stop order) đang nghỉ - "Buy-Side Liquidity" (BSL) phía trên các đỉnh, "Sell-Side Liquidity" (SSL) phía dưới các đáy. Thay vì coi vùng đó là một mức giá phẳng đơn lẻ, chỉ báo cắt nó thành 4 góc phần tư nằm ngang và theo dõi, theo từng nến, lượng volume mua vs. bán thực sự giao dịch bên trong mỗi lát khi giá tương tác với vùng. Khi giá cuối cùng sweep qua vùng, mẫu hình volume trên 4 góc phần tư đó được phân tích để phân loại lần sweep thành một trong 4 loại tín hiệu đảo chiều riêng biệt - và, khác với một chỉ báo thông thường, kết quả thực tế của mỗi tín hiệu sau đó được theo dõi tiếp về sau và tổng hợp vào một bảng tỷ lệ thắng trực tiếp.

  • Các vùng có "sức chứa" volume bị suy giảm dần: mỗi vùng bắt đầu ở mức sức khoẻ 100% và mất dần sức khoẻ khi volume giao dịch qua nó, dựa trên một ngân sách sức chứa được tính theo volume trung bình - một vùng đã hấp thụ nhiều volume mà chưa bị phá vỡ được xem là "yếu" hơn một vùng còn mới, ngay cả trước khi nó thực sự bị sweep.
  • Chỉ vùng chồng lấn quan trọng nhất được giữ lại: nếu một điểm swing mới sẽ tạo ra một vùng chồng lấn với vùng đang có, chỉ vùng cực trị hơn trong hai vùng (đỉnh cao hơn với BSL, đáy thấp hơn với SSL) được giữ lại.
  • 4 mẫu hình đảo chiều, được kiểm tra theo thứ tự ưu tiên: Absorption (volume ngược hướng bị mắc kẹt ở rìa sweep), Exhaustion (một lần sweep trên volume đáng ngờ là quá mỏng), Divergence (volume cùng hướng rất lớn nhưng vẫn thất bại trong việc phá vỡ), và Rejection (một nến ngược hướng mạnh, xảy ra ngay lập tức sau lần sweep) - mỗi vùng chỉ có thể phát ra một trong các tín hiệu này, đúng một lần duy nhất.
  • Mỗi tín hiệu trở thành một giao dịch mô phỏng được theo dõi: chỉ báo không chỉ vẽ một nhãn rồi thôi - nó mở một vị thế giả định tại giá của tín hiệu và theo dõi tiếp về sau xem giá có thực sự giữ đúng hướng kỳ vọng đủ lâu để tính là thắng hay không, đưa dữ liệu vào một bảng tỷ lệ thắng trực tiếp, theo từng loại mẫu hình.

1.2 Tính Năng Chỉ Báo

  • Box Vùng BSL/SSL: một vùng viền đỏ phía trên mỗi đỉnh swing đủ điều kiện (Buy-Side Liquidity) và một vùng viền xanh lá phía dưới mỗi đáy swing đủ điều kiện (Sell-Side Liquidity), mỗi vùng được chia thành 4 box góc phần tư xếp chồng.
  • Volume Delta Theo Từng Góc Phần Tư: mỗi box góc phần tư hiển thị delta mua/bán tích luỹ của riêng nó (tô màu và làm đậm nhạt theo mức độ áp đảo và độ lớn), cập nhật trực tiếp khi giá giao dịch qua các phần khác nhau của vùng.
  • Sức Khoẻ / Suy Giảm Vùng (tuỳ chọn): một nhãn phần trăm tại điểm giữa mỗi vùng đang hoạt động, giảm dần từ 100% về 0% khi volume tích luỹ giao dịch qua nó.
  • Bộ Nhớ Vùng Đã Bị Sweep (tuỳ chọn): một khi vùng bị sweep, nó có thể vẫn được hiển thị với viền nét đứt và các góc phần tư mờ nhạt thay vì biến mất hoàn toàn.
  • Nhãn Tín Hiệu Đảo Chiều (tuỳ chọn): nhãn "ABS", "EXH", "DIV", hoặc "REJ" xuất hiện tại điểm sweep khi một trong 4 mẫu hình được phát hiện, kích thước/độ đậm nhạt được co giãn theo điểm significance đã tính, kèm tooltip chi tiết khi rê chuột vào.
  • Bảng Hiệu Suất Đảo Chiều (tuỳ chọn): một bảng trực tiếp cho biết, theo từng loại tín hiệu, có bao nhiêu tín hiệu đã xuất hiện, bao nhiêu trong số đó là thắng khi theo dõi tiếp về sau, và tỷ lệ thắng phần trăm tương ứng.
  • Tô Màu Nến (tuỳ chọn): nến vàng trong khi một tín hiệu vẫn đang được đánh giá, nến ngọc lam (aqua) trong khi tín hiệu đó đang đang có lời.
  • Cảnh Báo: 7 điều kiện cảnh báo dạng động (đang test vùng, vùng bị sweep - riêng biệt cho BSL/SSL - và các tín hiệu đảo chiều). Tất cả cảnh báo đều là lệnh gọi alert() động; kịch bản không định nghĩa bất kỳ mục alertcondition() tĩnh nào, nên không có cảnh báo nào trong số này xuất hiện riêng lẻ trong danh sách xổ xuống tạo cảnh báo của TradingView - bạn cần chọn "Any alert() function call" để bắt được chúng.

1.3 Cách Sử Dụng Chỉ Báo

  • Xem các vùng BSL/SSL như những mục tiêu săn stop: giá có xu hướng thống kê bị kéo về những vùng bóng nến này để quét các lệnh chờ đang nằm ở đó - hãy quan sát giá phản ứng thế nào khi nó đến nơi.
  • Đọc màu sắc góc phần tư trước khi lần sweep xảy ra - volume ngược màu tích tụ mạnh ở góc phần tư ngoài cùng (gần rìa vùng nhất) có thể gợi ý về khả năng absorption trước khi lần sweep thậm chí còn chưa được xác nhận.
  • Dùng nhãn đảo chiều làm tín hiệu chính, và dựa vào số liệu tỷ lệ thắng trên bảng để đánh giá loại mẫu hình nào trong 4 loại thực sự đang hoạt động tốt trên chính biểu đồ/khung thời gian này trước khi tin tưởng một tín hiệu mới.
  • Theo dõi sức khoẻ vùng như một chỉ báo phụ - một vùng đã hấp thụ phần lớn sức chứa của nó mà chưa bị sweep có thể có khả năng giữ vững như một mức giá cao hơn về sau, so với một vùng còn mới, "sức khoẻ đầy đủ".
  • Cho bảng thời gian để tích luỹ đủ mẫu số có ý nghĩa thống kê - tỷ lệ thắng chỉ trên 1-2 tín hiệu lịch sử của một loại nào đó chưa đủ độ tin cậy thống kê; hãy để nhiều tín hiệu hơn tích luỹ trước khi tin vào con số phần trăm đó.

1.4 Cách Chỉ Báo Hoạt Động

Các Input & Vai Trò

  • length ("Pivot Length" - Độ dài Pivot, mặc định: 15, tối thiểu 2): số nến cần có ở mỗi bên của một đỉnh/đáy swing trước khi nó được xác nhận là điểm neo tạo vùng (xác nhận trễ length nến, độ trễ tiêu chuẩn khi phát hiện pivot).
  • maxZones ("Max Zones per Type" - Số Vùng Tối Đa Mỗi Loại, mặc định: 10, khoảng 1-40): có bao nhiêu vùng BSL và bao nhiêu vùng SSL (mỗi loại giới hạn riêng) được giữ lại; vùng cũ nhất bị loại bỏ trước khi đạt giới hạn.
  • showSwept ("Show Swept Zones" - Hiện Vùng Đã Bị Sweep, mặc định: true): giữ viền nét đứt và các góc phần tư mờ nhạt của một vùng hiển thị sau khi nó bị sweep, thay vì ẩn chúng.
  • filterOverlaps ("Filter Overlapping Zones" - Lọc Vùng Chồng Lấn, mặc định: true): khi một pivot mới sẽ tạo ra một vùng chồng lấn với một vùng đang hoạt động, chỉ vùng cực trị hơn (xa giá hiện tại hơn) tồn tại - vùng còn lại bị bỏ qua (nếu vùng mới kém cực trị hơn) hoặc bị thay thế (nếu vùng mới cực trị hơn).
  • showDecay ("Show Zone Decay" - Hiện Suy Giảm Vùng, mặc định: true) / zoneCapacity ("Zone Volume Capacity" - Sức Chứa Volume Vùng, mặc định: 5.0, tối thiểu 1.0): có hiển thị phần trăm sức khoẻ của từng vùng hay không, và ngân sách volume của vùng đó lớn bao nhiêu - được đặt bằng bội số này của volume trung bình (trên cửa sổ độ dài pivot) tại thời điểm vùng được tạo.
  • enableReversals ("Enable Reversal Detection" - Bật Phát Hiện Đảo Chiều, mặc định: true): công tắc tổng cho toàn bộ hệ thống tín hiệu 4 mẫu hình (và, gián tiếp, cả mô phỏng giao dịch lẫn bảng hiệu suất, vì chúng phụ thuộc vào việc tín hiệu có được phát ra hay không).
  • dashboardInput ("Show Dashboard" - Hiện Dashboard, mặc định: true): hiện bảng hiệu suất, hoặc xoá hoàn toàn bảng khi tắt (việc theo dõi thắng/thua bên dưới vẫn chạy bình thường - chỉ bảng hiển thị bị ảnh hưởng).
  • dashboardPositionInput / dashboardSizeInput: góc màn hình và cỡ chữ của dashboard.
  • dashboardWindowInput ("Eval Window (Bars)" - Cửa Sổ Đánh Giá (Nến), mặc định: 10): số nến tối đa mà một giao dịch mô phỏng được cho phép để đạt yêu cầu về thời gian giữ trước khi nó bị đánh dấu là hết giờ (tính là thua, vì nó không bao giờ đạt tới trạng thái "thắng").
  • dashboardHoldInput ("Hold Time (Bars)" - Thời Gian Giữ (Nến), mặc định: 3): số nến liên tiếp mà giá phải duy trì theo đúng hướng kỳ vọng có lời để một giao dịch mô phỏng được tính là thắng. Chỉ cần một nến đi ngược lại giao dịch cũng sẽ đặt lại chuỗi liên tiếp này về 0.
  • dashboardHighlightInput ("Highlight Eval Bars" - Tô Sáng Nến Đánh Giá, mặc định: false): tô nến màu vàng trong khi một giao dịch đang hoạt động chưa có lời, màu ngọc lam (aqua) trong khi giao dịch đó đang có lời.
  • bslColor / sslColor: màu viền cho các vùng BSL (mặc định đỏ) và SSL (mặc định xanh lá).
  • buyDeltaColor / sellDeltaColor: màu tô cho một góc phần tư khi volume mua hoặc bán chiếm ưu thế trong đó.

Các Khối Logic Chính

🎯 Flow 1: Những Gì Bạn Nhìn Thấy

  • Các vùng BSL (đỏ) phía trên đỉnh swing và các vùng SSL (xanh lá) phía dưới đáy swing, mỗi vùng được chia thành 4 box góc phần tư hiển thị delta mua/bán trực tiếp; một nhãn phần trăm sức khoẻ tuỳ chọn cho mỗi vùng; các nhãn ABS/EXH/DIV/REJ tại các điểm đảo chiều đã xác nhận; tô sáng nến vàng/ngọc lam tuỳ chọn cho việc đánh giá giao dịch; và một bảng hiệu suất tỷ lệ thắng tuỳ chọn.

🌱 Flow 2: Tạo Một Vùng Mới

  • Tại một đỉnh pivot đã xác nhận, vùng trải dài từ đỉnh thân nến đó (giá trị cao hơn giữa open/close) lên đến đỉnh bóng nến thực sự - tức là chỉ phần bóng nến phía trên thân nến, được đệm thêm một chiều cao tối thiểu nếu bóng nến quá mỏng so với ATR. Một đáy pivot tạo ra vùng SSL đối xứng, dùng đáy thân nến xuống tới đáy bóng nến.
  • Mỗi vùng mới được chia thành 4 box góc phần tư có chiều cao bằng nhau, có khoảng hở nhỏ giữa các box, một box viền ngoài, và một nhãn sức khoẻ "100%" - sau đó được thêm vào đầu danh sách vùng của loại tương ứng (mới nhất đứng đầu), vùng cũ nhất bị loại bỏ nếu danh sách vượt quá maxZones.

🔍 Flow 3: Chỉ Giữ Lại Vùng Chồng Lấn Quan Trọng Nhất

  • Nếu filterOverlaps được bật và khoảng giá của vùng ứng viên mới chồng lấn với một vùng đang hoạt động (chưa bị sweep) cùng loại: vùng cực trị hơn sẽ thắng. Nếu pivot mới cực trị hơn (đỉnh cao hơn với BSL, đáy thấp hơn với SSL), vùng cũ bị xoá và vùng mới được tạo ra; ngược lại, ứng viên mới bị loại bỏ hoàn toàn và vùng hiện có được giữ nguyên không đổi.

📊 Flow 4: Phân Bổ Volume Delta Vào Các Góc Phần Tư

  • Delta tổng thể của mỗi nến trước tiên được ước lượng theo cùng cách với một chỉ báo proxy vị trí đóng cửa: volume × (close-open)/(high-low) - một giá trị có dấu thể hiện volume của nến đó có tính định hướng đến mức nào.
  • Với mỗi vùng đang hoạt động, khoảng cao-thấp của nến hiện tại được kiểm tra riêng với từng góc phần tư trong số 4 góc; bất cứ nơi nào có sự chồng lấn giá thực sự, góc phần tư đó nhận được một tỷ lệ tương ứng của delta và volume tổng của nến, theo tỷ trọng là bao nhiêu phần khoảng giá tổng của nến rơi vào đúng góc phần tư đó - vì vậy một nến duy nhất chạm vào 2 góc phần tư sẽ chia delta của nó giữa hai góc thay vì gán hết cho một góc.

🌡️ Flow 5: Sức Khoẻ / Suy Giảm Vùng

  • Độc lập với việc chia delta theo từng góc phần tư, mọi lượng volume chồng lấn với vùng theo bất kỳ hướng nào (bất kể chiều) cũng được cộng vào tổng volumeTraded tích luỹ của vùng. Sức khoẻ = 100% trừ đi phần trăm của tổng đó so với ngân sách sức chứa của vùng, giới hạn dưới ở 0% - một vùng đã hấp thụ nhiều volume hai chiều mà chưa bị phá vỡ sẽ hiển thị chỉ số sức khoẻ thấp ngay cả khi delta theo từng góc phần tư của nó tương đối cân bằng.

🔄 Flow 6: Phát Hiện Sweep & 4 Mẫu Hình Đảo Chiều

  • Một vùng bị "sweep" ngay tại thời điểm giá cao của nến vượt qua đỉnh vùng (BSL) hoặc giá thấp vượt qua đáy vùng (SSL). Sau khi bị sweep, vùng ngừng tích luỹ cập nhật delta/sức khoẻ và (nếu được bật) chuyển sang hình thức "bộ nhớ" mờ nhạt, viền nét đứt.
  • Trong khi vùng đang bị chạm hoặc ngay tại thời điểm nó sweep, và chỉ khi nó chưa từng phát tín hiệu, bốn mẫu hình được kiểm tra theo thứ tự (mẫu hình đầu tiên khớp sẽ thắng):
  • 1. ABS (Absorption - Hấp Thụ): giá đang sweep qua rìa ngoài của vùng, nhưng delta volume tại chính góc phần tư ngoài đó lại ngược hướng với hướng sweep, và chiếm hơn 20% tổng hoạt động của vùng - được hiểu là các lệnh mạnh mẽ phía đối nghịch đang hấp thụ nỗ lực phá vỡ.
  • 2. EXH (Exhaustion - Cạn Kiệt): giá đang sweep qua, nhưng hoạt động tại góc phần tư ngoài dưới 10% tổng hoạt động của vùng - lần phá vỡ xảy ra trên volume bất thường mỏng, gợi ý rất ít sự quyết tâm thực sự đằng sau nó.
  • 3. DIV (Divergence - Phân Kỳ): giá đã quẹt qua khỏi vùng nhưng đóng cửa trở lại bên trong (breakout thất bại), trong khi góc phần tư ngoài cho thấy hơn 60% hoạt động của vùng theo đúng hướng breakout - volume lớn, có vẻ như cam kết mạnh mẽ nhưng vẫn thất bại trong việc giữ vững breakout.
  • 4. REJ (Snapback Rejection - Từ Chối Bật Lại): giá đã sweep qua và đóng cửa trở lại ở nửa "trong" của vùng, và delta riêng của chính nến này vừa ngược hướng với hướng sweep vừa chiếm hơn 20% volume của chính nến đó - một nến từ chối tức thời, mạnh mẽ ngay tại thời điểm sweep.
  • Bất kỳ mẫu hình nào khớp (nếu có) sẽ khoá vùng khỏi việc phát tín hiệu thêm lần nữa, gửi một cảnh báo, và vẽ một nhãn được co giãn kích thước/độ mờ (theo một điểm "significance" từ 0-1 với công thức riêng cho từng mẫu hình) kèm tooltip chi tiết giải thích lý do.

🎨 Flow 7: Phản Hồi Hình Ảnh Theo Từng Góc Phần Tư

  • Màu tô và độ đậm nhạt của mỗi góc phần tư được xác định bởi delta của chính nó so với góc phần tư nào trong vùng đó có delta tuyệt đối lớn nhất tính đến hiện tại - góc phần tư mà volume nghiêng về mua hoặc bán càng mạnh, và mức nghiêng đó càng lớn so với góc phần tư hoạt động mạnh nhất của vùng, thì màu của nó càng đậm. Các góc phần tư chưa có delta đáng kể sẽ quay về một gradient vị trí trung tính. Một khi vùng bị sweep, tất cả điều này chuyển sang hình thức phẳng, mờ nhạt (hoặc biến mất hoàn toàn nếu showSwept bị tắt).

🧪 Flow 8: Theo Dõi Giao Dịch Mô Phỏng & Bảng Tỷ Lệ Thắng

  • Mỗi tín hiệu phát ra tạo ra một "giao dịch" mô phỏng nhẹ, bắt đầu từ nến sau tín hiệu (entry = giá đóng cửa của nến đó, hướng = ngược lại phía vùng bị sweep). Bắt đầu từ nến kế tiếp, mỗi giao dịch đang hoạt động được kiểm tra ở mỗi nến: nếu giá hiện đang ở phía có lời so với entry, bộ đếm số nến có lời liên tiếp tăng lên; bất kỳ nến nào đi ngược lại sẽ đặt lại bộ đếm này về 0.
  • Một giao dịch được đánh dấu thắng một khi bộ đếm nến có lời liên tiếp đạt tới dashboardHoldInput. Nếu điều đó không xảy ra trong vòng dashboardWindowInput nến kể từ entry, giao dịch hết giờ (tính là không thắng). Trong cả hai trường hợp, kết quả được cộng vào bộ đếm tổng/thắng đang chạy của loại tín hiệu đó, dữ liệu này nuôi các con số tỷ lệ thắng trực tiếp trên bảng.
  • Nếu tính năng tô sáng nến được bật, nến hiện tại được tô màu vàng trong khi có bất kỳ giao dịch nào đang "chưa có lời", và tô màu ngọc lam trong khi có giao dịch nào đó đang hiện đang có lời (màu ngọc lam được ưu tiên hiển thị khi nhiều giao dịch không đồng thuận trong cùng một nến).

🔔 Flow 9: Cảnh Báo

  • Cả 7 điều kiện cảnh báo - test một vùng BSL/SSL, một vùng BSL/SSL bị sweep, và một tín hiệu đảo chiều phát ra - đều chỉ được gửi qua các lệnh gọi alert() động. Không có bất kỳ định nghĩa alertcondition() tĩnh nào trong kịch bản này, nên không có cảnh báo nào trong số này xuất hiện như một tuỳ chọn có thể chọn riêng lẻ trong hộp thoại tạo cảnh báo tiêu chuẩn của TradingView; bạn cần tạo cảnh báo trên chỉ báo này bằng điều kiện "Any alert() function call" để nhận được bất kỳ cảnh báo nào trong số đó.

Đầu Ra & Vai Trò Sử Dụng

  • Vùng BSL/SSL + delta góc phần tư: nơi khả năng cao thanh khoản săn stop đang nằm, và một chỉ số trực tiếp cho biết thực sự ai đang giao dịch qua đó.
  • Sức khoẻ vùng: một chỉ số phụ về độ bền vững, độc lập với việc chia delta.
  • Tín hiệu đảo chiều: đầu ra hành động chính, được phân loại thành 4 loại mẫu hình riêng biệt, loại trừ lẫn nhau.
  • Bảng tỷ lệ thắng: sự kiểm chứng thực sự, được theo dõi tiếp về sau, về việc mỗi loại mẫu hình thực sự đã hoạt động tốt như thế nào trên chính biểu đồ này, chứ không chỉ là một con số đếm đồng xuất hiện trong lịch sử.
  • Cảnh báo: tất cả đều được gửi qua alert() động (xem Flow 9).
Zoom/Pan Layered Image
Background Overlay
− +
				
					// This work is licensed under a Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) https://creativecommons.org/licenses/by-nc-sa/4.0/
// © LuxAlgo
//@version=6
indicator("Liquidity Delta Profiler [LuxAlgo]", "LuxAlgo - Liquidity Delta Profiler", overlay = true, max_boxes_count = 500, max_labels_count = 500)

//---------------------------------------------------------------------------------------------------------------------}
// Constants (Đã Việt hóa)
//---------------------------------------------------------------------------------------------------------------------{
color DATA              = #DBDBDB
color HEADERS           = #808080
color BACKGROUND        = #161616
color BORDERS           = #2E2E2E

string TOP_RIGHT        = 'Trên Phải'
string BOTTOM_RIGHT     = 'Dưới Phải'
string BOTTOM_LEFT      = 'Dưới Trái'

string TINY             = 'Rất Nhỏ'
string SMALL            = 'Nhỏ'
string NORMAL           = 'Vừa'
string LARGE            = 'Lớn'
string HUGE             = 'Rất Lớn'

string DASHBOARD_GROUP  = 'Cài Đặt Dashboard'

//---------------------------------------------------------------------------------------------------------------------}
// Settings (Đã Việt hóa)
//---------------------------------------------------------------------------------------------------------------------{
length = input.int(15, "Độ Dài Pivot", minval = 2, tooltip = "Số nến nhìn lại và nhìn tới để phát hiện các đỉnh/đáy swing chính.")
maxZones = input.int(10, "Số Vùng Tối Đa Mỗi Loại", minval = 1, maxval = 40, tooltip = "Số lượng vùng mua và bán đang hoạt động/lịch sử tối đa được giữ lại trên biểu đồ.")
showSwept = input.bool(true, "Hiện Vùng Đã Bị Sweep", tooltip = "Giữ các vùng hiển thị (với viền nét đứt và độ mờ giảm) sau khi giá sweep qua chúng.")
filterOverlaps = input.bool(true, "Lọc Vùng Chồng Lấn", tooltip = "Khi bật, ngăn việc tạo vùng mới chồng lấn với các vùng đang hoạt động hiện có. Chỉ mức quan trọng nhất (cao nhất với BSL, thấp nhất với SSL) được giữ lại.")

showDecay = input.bool(true, "Hiện Suy Giảm Vùng", tooltip = "Hiển thị 'sức khoẻ' còn lại của các vùng đang hoạt động dựa trên volume đã giao dịch bên trong chúng. Sức khoẻ giảm từ 100% xuống 0%.")
zoneCapacity = input.float(5.0, "Sức Chứa Volume Vùng", minval = 1.0, tooltip = "Hệ số nhân cho volume trung bình để xác định vùng có thể hấp thụ bao nhiêu volume trước khi đạt sức khoẻ 0%.")

enableReversals = input.bool(true, "Bật Phát Hiện Đảo Chiều", group = "Cài Đặt Đảo Chiều", tooltip = "Phát hiện các mẫu hình volume delta bất thường trong lúc sweep thanh khoản để báo hiệu khả năng đảo chiều. Vẽ một nhãn với loại tín hiệu (ABS, EXH, DIV, REJ) và tooltip khi rê chuột.")

dashboardInput          = input.bool(   true,       'Hiện Dashboard',    group = DASHBOARD_GROUP, tooltip = 'Bật hoặc tắt dashboard.')
dashboardPositionInput  = input.string( TOP_RIGHT,  'Vị Trí',          group = DASHBOARD_GROUP, tooltip = 'Chọn vị trí đặt dashboard.', options = [TOP_RIGHT, BOTTOM_RIGHT, BOTTOM_LEFT])
dashboardSizeInput      = input.string( TINY,       'Cỡ Chữ',              group = DASHBOARD_GROUP, tooltip = 'Chọn cỡ chữ của dashboard.', options = [TINY, SMALL, NORMAL, LARGE, HUGE])
dashboardWindowInput    = input.int(    10,         'Cửa Sổ Đánh Giá (Nến)', group = DASHBOARD_GROUP, minval = 1, tooltip = 'Số nến tối đa chờ đợi để lần đảo chiều xảy ra.')
dashboardHoldInput      = input.int(    3,          'Thời Gian Giữ (Nến)',   group = DASHBOARD_GROUP, minval = 1, tooltip = 'Số nến liên tiếp giá phải duy trì có lời (ngược hướng sweep) để được tính là thắng.')
dashboardHighlightInput = input.bool(   false,      'Tô Sáng Nến Đánh Giá',group = DASHBOARD_GROUP, tooltip = 'Tô sáng các nến đang được đánh giá. Vàng = Đang đánh giá, Ngọc lam = Đang có lời.')

bslColor = input.color(color.new(#f23645, 0), "Màu Viền BSL", group = "Cài Đặt Màu Sắc", tooltip = "Màu cho các vùng Buy-Side Liquidity (phía trên giá).")
sslColor = input.color(color.new(#089981, 0), "Màu Viền SSL", group = "Cài Đặt Màu Sắc", tooltip = "Màu cho các vùng Sell-Side Liquidity (phía dưới giá).")

buyDeltaColor = input.color(color.new(#089981, 0), "Màu Tô Delta Mua", group = "Cài Đặt Màu Sắc", tooltip = "Màu tô khi volume mua chiếm ưu thế trong một góc phần tư của vùng.")
sellDeltaColor = input.color(color.new(#f23645, 0), "Màu Tô Delta Bán", group = "Cài Đặt Màu Sắc", tooltip = "Màu tô khi volume bán chiếm ưu thế trong một góc phần tư của vùng.")

//---------------------------------------------------------------------------------------------------------------------}
// Types
//---------------------------------------------------------------------------------------------------------------------{
type Zone
    float top
    float bottom
    int left
    int right
    bool swept
    bool signaled
    box[] quads
    float[] deltas
    box outline
    float volumeTraded
    float capacity
    bool wasHit
    label decayLabel

type Trade
    string type
    int dir
    float entry
    int entryBar
    bool active
    bool won
    int consecBars

//---------------------------------------------------------------------------------------------------------------------}
// Variables
//---------------------------------------------------------------------------------------------------------------------{
var Zone[] bslZones = array.new<Zone>()
var Zone[] sslZones = array.new<Zone>()

var activeTrades = array.new<Trade>()

var int absTotal = 0
var int absWins = 0
var int exhTotal = 0
var int exhWins = 0
var int divTotal = 0
var int divWins = 0
var int rejTotal = 0
var int rejWins = 0

var string parsedDashboardPosition = switch dashboardPositionInput
    TOP_RIGHT       => position.top_right
    BOTTOM_RIGHT    => position.bottom_right
    BOTTOM_LEFT     => position.bottom_left
    => position.top_right

var string parsedDashboardSize     = switch dashboardSizeInput
    TINY            => size.tiny
    SMALL           => size.small
    NORMAL          => size.normal
    LARGE           => size.large
    HUGE            => size.huge
    => size.normal

var table t_able = table.new(parsedDashboardPosition, 4, 11, bgcolor = dashboardInput ? BACKGROUND : na, border_width = 0, frame_color = dashboardInput ? BORDERS : na, frame_width = 1, force_overlay = true)

//---------------------------------------------------------------------------------------------------------------------}
// Methods / Functions (Đã Việt hóa)
//---------------------------------------------------------------------------------------------------------------------{
cell(table t_able, int col, int r, string data, color txtColor = color.white, string align = text.align_right, color background = na, float h = 0) =>
    t_able.cell(col, r, data, text_color = txtColor, text_size = parsedDashboardSize, text_halign = align, bgcolor = background, height = h)

divider(table t_able, int r, int lastColumn) =>
    string rowDivider = '━━━━━━━━━━━━━━━━'
    t_able.merge_cells(0, r, lastColumn, r)
    cell(t_able, 0, r, rowDivider, txtColor = BORDERS, align = text.align_center, h = 0.5)

method evaluateReversals(Zone z, bool isBsl, float barH, float barL, float barC, float barO, float barDelta, float barVol) =>
    if enableReversals and not z.signaled
        float totalD = 0.0
        float absTotalD = 0.0
        for d in z.deltas
            totalD += d
            absTotalD += math.abs(d)

        if absTotalD > 0
            int outerIdx = isBsl ? 3 : 0
            float outerD = z.deltas.get(outerIdx)

            bool isSweeping = isBsl ? barH > z.top : barL < z.bottom
            bool closesInside = isBsl ? (barC <= z.top and barC >= z.bottom) : (barC >= z.bottom and barC <= z.top)
            float midPoint = (z.top + z.bottom) / 2
            bool closesRejecting = isBsl ? barC < midPoint : barC > midPoint

            string signalType = ""
            string tooltipTxt = ""
            float significance = 0.0
            color sigColor = isBsl ? color.red : color.green // Reversing from Resistance is Bearish (Red), from Support is Bullish (Green)

            // 1. Absorption at the Extreme (Trap)
            if isSweeping and ((isBsl and outerD < 0) or (not isBsl and outerD > 0))
                float ratio = math.abs(outerD) / (absTotalD + 0.0001)
                if ratio > 0.2
                    signalType := "ABS"
                    tooltipTxt := "Hấp Thụ Tại Điểm Cực Trị\n" + (isBsl ? "Bên bán" : "Bên mua") + " đã hấp thụ mạnh lần sweep.\nDelta Góc Phần Tư Ngoài: " + str.tostring(outerD, format.volume)
                    significance := ratio * 2

            // 2. Exhaustion (Dry Sweep)
            if signalType == "" and isSweeping
                float ratio = math.abs(outerD) / (absTotalD + 0.0001)
                if ratio < 0.1
                    signalType := "EXH"
                    tooltipTxt := "Cạn Kiệt (Sweep Khô)\nVolume tối thiểu tại rìa cực trị.\nDelta Góc Phần Tư Ngoài: " + str.tostring(outerD, format.volume)
                    significance := 1.0 - (ratio * 5)

            // 3. Delta Divergence (FOMO)
            if signalType == "" and closesInside
                float ratio = math.abs(outerD) / (absTotalD + 0.0001)
                if ratio > 0.6 and ((isBsl and outerD > 0) or (not isBsl and outerD < 0))
                    signalType := "DIV"
                    tooltipTxt := "Phân Kỳ Delta (FOMO)\nVolume lớn bị mắc kẹt tại điểm cực trị, nhưng giá đã thất bại trong việc phá vỡ.\nDelta Góc Phần Tư Ngoài: " + str.tostring(outerD, format.volume)
                    significance := ratio

            // 4. Snapback (Climax + Rejection)
            if signalType == "" and isSweeping and closesRejecting
                float barRatio = math.abs(barDelta) / (barVol + 0.0001)
                if ((isBsl and barDelta < 0) or (not isBsl and barDelta > 0)) and barRatio > 0.2
                    signalType := "REJ"
                    tooltipTxt := "Từ Chối Bật Lại\nSweep theo sau bởi một sự từ chối mạnh mẽ ngay lập tức.\nDelta Nến: " + str.tostring(barDelta, format.volume)
                    significance := barRatio * 2

            // Plot Signal
            if signalType != ""
                z.signaled := true
                alert("Tín Hiệu Đảo Chiều (" + signalType + ") phát hiện tại " + str.tostring(barC), alert.freq_once_per_bar)

                significance := math.min(math.max(significance, 0.0), 1.0)
                string sSize = size.tiny
                int trans = 50

                if significance > 0.7
                    sSize := size.normal
                    trans := 10
                else if significance > 0.4
                    sSize := size.small
                    trans := 30

                color finalColor = color.new(sigColor, trans)
                float yLoc = isBsl ? barH : barL
                string labelStyle = isBsl ? label.style_label_down : label.style_label_up

                label.new(bar_index, yLoc, text=signalType, color=finalColor, style=labelStyle, textcolor=color.white, size=sSize, tooltip=tooltipTxt)

                // Track Trade Performance
                float entry = barC
                activeTrades.push(Trade.new(signalType, isBsl ? -1 : 1, entry, bar_index, true, false, 0))

method updateVisuals(Zone z, bool isBsl, bool showS, color buyC, color sellC, color bslC, color sslC) =>
    float maxD = 0.0
    for d in z.deltas
        maxD := math.max(maxD, math.abs(d))

    for i = 0 to 3
        float d = z.deltas.get(i)
        color c = na
        string txt = ""
        color tColor = na
        if maxD > 0 and math.abs(d) > 0.001
            color baseC = d > 0 ? buyC : sellC
            int trans = z.swept ? 90 : 100 - int((math.abs(d) / maxD) * 60)
            c := color.rgb(color.r(baseC), color.g(baseC), color.b(baseC), trans)
            txt := (d > 0 ? "+" : d < 0 ? "-" : "") + str.tostring(math.abs(d), format.volume)
            tColor := color.new(baseC, z.swept ? 50 : 0)
        else
            color baseC = isBsl ? bslC : sslC
            int defaultTrans = 60 + (isBsl ? (3 - i) : i) * 10
            c := color.rgb(color.r(baseC), color.g(baseC), color.b(baseC), z.swept ? 90 : defaultTrans)
            txt := "0"
            tColor := color.new(baseC, z.swept ? 80 : 50)

        box q = z.quads.get(i)
        q.set_bgcolor(not showS and z.swept ? na : c)
        q.set_text(not showS and z.swept ? "" : txt)
        q.set_text_color(not showS and z.swept ? na : tColor)

        if z.swept
            q.set_border_color(not showS ? na : color.new(isBsl ? bslC : sslC, 80))
            q.set_border_style(line.style_dashed)
        else
            q.set_border_color(color.new(isBsl ? bslC : sslC, 60))
            q.set_border_style(line.style_solid)

    if z.swept
        if showS
            z.outline.set_border_style(line.style_dashed)
            z.outline.set_border_color(color.new(isBsl ? bslC : sslC, 80))
        else
            z.outline.set_border_color(na)

barOverlap(float barH, float barL, float qTop, float qBot) =>
    float overlapTop = math.min(barH, qTop)
    float overlapBot = math.max(barL, qBot)
    overlapTop > overlapBot ? (overlapTop - overlapBot) : 0.0

//---------------------------------------------------------------------------------------------------------------------}
// Logic (Đã Việt hóa)
//---------------------------------------------------------------------------------------------------------------------{
float atr = ta.atr(14)
float ph = ta.pivothigh(high, length, length)
float pl = ta.pivotlow(low, length, length)
float avgVol = nz(ta.sma(volume, length))
if avgVol == 0
    avgVol := 1
float currentZoneCap = avgVol * zoneCapacity

if not na(ph) and bar_index >= length
    int pivotIdx = bar_index - length
    float pHigh = high[length]
    float pBot = math.max(close[length], open[length])
    if pHigh - pBot < atr[length] * 0.1
        pBot := pHigh - atr[length] * 0.1

    bool skip = false
    if filterOverlaps and bslZones.size() > 0
        for i = bslZones.size() - 1 to 0
            if i < bslZones.size()
                Zone ex = bslZones.get(i)
                if not ex.swept
                    bool overlaps = math.max(pBot, ex.bottom) <= math.min(pHigh, ex.top)
                    if overlaps
                        if pHigh > ex.top
                            for b in ex.quads
                                b.delete()
                            ex.outline.delete()
                            ex.decayLabel.delete()
                            bslZones.remove(i)
                        else
                            skip := true
                            break

    if not skip
        box[] quads = array.new<box>(4)
        float[] deltas = array.new<float>(4, 0.0)
        float step = (pHigh - pBot) / 4
        for i = 0 to 3
            float qBot = pBot + i * step
            float qTop = pBot + (i + 1) * step
            color baseC = bslColor
            int trans = 60 + (3 - i) * 10
            color c = color.rgb(color.r(baseC), color.g(baseC), color.b(baseC), trans)

            float gap = step * 0.05
            float bTop = qTop - gap
            float bBot = qBot + gap
            if i == 0
                bBot := qBot
            if i == 3
                bTop := qTop

            quads.set(i, box.new(pivotIdx, bTop, pivotIdx, bBot, border_color = color.new(baseC, 60), bgcolor = c, text = "0", text_color = color.new(baseC, 50), text_halign = text.align_right, text_size = size.auto))

        box outline = box.new(pivotIdx, pHigh, pivotIdx, pBot, border_color = color.new(bslColor, 80), bgcolor = na)
        label dLabel = label.new(bar_index, pBot + (pHigh - pBot)/2, text = "100%", style = label.style_label_left, color = color.new(bslColor, 80), textcolor = color.white, size = size.small)
        if not showDecay
            dLabel.set_x(na)

        Zone z = Zone.new(pHigh, pBot, pivotIdx, pivotIdx, false, false, quads, deltas, outline, 0.0, currentZoneCap, false, dLabel)
        bslZones.unshift(z)
        if bslZones.size() > maxZones
            Zone removed = bslZones.pop()
            for b in removed.quads
                b.delete()
            removed.outline.delete()
            removed.decayLabel.delete()

if not na(pl) and bar_index >= length
    int pivotIdx = bar_index - length
    float pLow = low[length]
    float pTop = math.min(close[length], open[length])
    if pTop - pLow < atr[length] * 0.1
        pTop := pLow + atr[length] * 0.1

    bool skip = false
    if filterOverlaps and sslZones.size() > 0
        for i = sslZones.size() - 1 to 0
            if i < sslZones.size()
                Zone ex = sslZones.get(i)
                if not ex.swept
                    bool overlaps = math.max(pLow, ex.bottom) <= math.min(pTop, ex.top)
                    if overlaps
                        if pLow < ex.bottom
                            for b in ex.quads
                                b.delete()
                            ex.outline.delete()
                            ex.decayLabel.delete()
                            sslZones.remove(i)
                        else
                            skip := true
                            break

    if not skip
        box[] quads = array.new<box>(4)
        float[] deltas = array.new<float>(4, 0.0)
        float step = (pTop - pLow) / 4
        for i = 0 to 3
            float qBot = pLow + i * step
            float qTop = pLow + (i + 1) * step
            color baseC = sslColor
            int trans = 60 + i * 10
            color c = color.rgb(color.r(baseC), color.g(baseC), color.b(baseC), trans)

            float gap = step * 0.05
            float bTop = qTop - gap
            float bBot = qBot + gap
            if i == 0
                bBot := qBot
            if i == 3
                bTop := qTop

            quads.set(i, box.new(pivotIdx, bTop, pivotIdx, bBot, border_color = color.new(baseC, 60), bgcolor = c, text = "0", text_color = color.new(baseC, 50), text_halign = text.align_right, text_size = size.auto))

        box outline = box.new(pivotIdx, pTop, pivotIdx, pLow, border_color = color.new(sslColor, 80), bgcolor = na)
        label dLabel = label.new(bar_index, pLow + (pTop - pLow)/2, text = "100%", style = label.style_label_left, color = color.new(sslColor, 80), textcolor = color.white, size = size.small)
        if not showDecay
            dLabel.set_x(na)

        Zone z = Zone.new(pTop, pLow, pivotIdx, pivotIdx, false, false, quads, deltas, outline, 0.0, currentZoneCap, false, dLabel)
        sslZones.unshift(z)
        if sslZones.size() > maxZones
            Zone removed = sslZones.pop()
            for b in removed.quads
                b.delete()
            removed.outline.delete()
            removed.decayLabel.delete()

float vol = nz(volume)
float totalRange = high - low
float barDelta = totalRange == 0 ? 0 : vol * (close - open) / totalRange

if bslZones.size() > 0
    for i = bslZones.size() - 1 to 0
        if i < bslZones.size()
            Zone z = bslZones.get(i)
            if not z.swept
                z.right := bar_index
                z.outline.set_right(bar_index)
                for b in z.quads
                    b.set_right(bar_index)

                bool hit = false
                float step = (z.top - z.bottom) / 4
                for j = 0 to 3
                    float qBot = z.bottom + j * step
                    float qTop = z.bottom + (j + 1) * step
                    float overlap = barOverlap(high, low, qTop, qBot)
                    if overlap > 0
                        hit := true
                        float overlapRatio = totalRange == 0 ? 0 : overlap / totalRange
                        float qDelta = barDelta * overlapRatio
                        z.deltas.set(j, z.deltas.get(j) + qDelta)
                        z.volumeTraded += vol * overlapRatio

                if hit and not z.wasHit
                    alert("Giá đang test Vùng Kháng Cự BSL tại " + str.tostring(z.top), alert.freq_once_per_bar)

                if high > z.top
                    z.swept := true
                    alert("Vùng Kháng Cự BSL đã bị Sweep tại " + str.tostring(z.top), alert.freq_once_per_bar)

                if not z.swept
                    if showDecay
                        int health = int(math.max(0, 100 - (z.volumeTraded / z.capacity * 100)))
                        z.decayLabel.set_x(bar_index + 1)
                        z.decayLabel.set_text(str.tostring(health) + "%")
                    else
                        z.decayLabel.set_x(na)
                else
                    z.decayLabel.delete()

                z.wasHit := hit

                if hit or z.swept
                    z.evaluateReversals(true, high, low, close, open, barDelta, vol)
                    z.updateVisuals(true, showSwept, buyDeltaColor, sellDeltaColor, bslColor, sslColor)

if sslZones.size() > 0
    for i = sslZones.size() - 1 to 0
        if i < sslZones.size()
            Zone z = sslZones.get(i)
            if not z.swept
                z.right := bar_index
                z.outline.set_right(bar_index)
                for b in z.quads
                    b.set_right(bar_index)

                bool hit = false
                float step = (z.top - z.bottom) / 4
                for j = 0 to 3
                    float qBot = z.bottom + j * step
                    float qTop = z.bottom + (j + 1) * step
                    float overlap = barOverlap(high, low, qTop, qBot)
                    if overlap > 0
                        hit := true
                        float overlapRatio = totalRange == 0 ? 0 : overlap / totalRange
                        float qDelta = barDelta * overlapRatio
                        z.deltas.set(j, z.deltas.get(j) + qDelta)
                        z.volumeTraded += vol * overlapRatio

                if hit and not z.wasHit
                    alert("Giá đang test Vùng Hỗ Trợ SSL tại " + str.tostring(z.bottom), alert.freq_once_per_bar)

                if low < z.bottom
                    z.swept := true
                    alert("Vùng Hỗ Trợ SSL đã bị Sweep tại " + str.tostring(z.bottom), alert.freq_once_per_bar)

                if not z.swept
                    if showDecay
                        int health = int(math.max(0, 100 - (z.volumeTraded / z.capacity * 100)))
                        z.decayLabel.set_x(bar_index + 1)
                        z.decayLabel.set_text(str.tostring(health) + "%")
                    else
                        z.decayLabel.set_x(na)
                else
                    z.decayLabel.delete()

                z.wasHit := hit

                if hit or z.swept
                    z.evaluateReversals(false, high, low, close, open, barDelta, vol)
                    z.updateVisuals(false, showSwept, buyDeltaColor, sellDeltaColor, bslColor, sslColor)

// Process Active Trades
color barHighlight = na
if activeTrades.size() > 0
    for i = activeTrades.size() - 1 to 0
        if i < activeTrades.size()
            Trade t = activeTrades.get(i)
            if t.active
                if bar_index > t.entryBar // Wait until the next bar to start evaluating
                    bool inProfit = (t.dir == 1 and close > t.entry) or (t.dir == -1 and close < t.entry)

                    if inProfit
                        t.consecBars += 1
                        if na(barHighlight) or barHighlight == color.yellow
                            barHighlight := color.aqua
                    else
                        t.consecBars := 0
                        if na(barHighlight)
                            barHighlight := color.yellow

                    if t.consecBars >= dashboardHoldInput
                        t.active := false
                        t.won := true
                    else if bar_index - t.entryBar >= dashboardWindowInput
                        t.active := false // Time ran out

                    if not t.active
                        if t.type == "ABS"
                            absTotal += 1
                            if t.won
                                absWins += 1
                        else if t.type == "EXH"
                            exhTotal += 1
                            if t.won
                                exhWins += 1
                        else if t.type == "DIV"
                            divTotal += 1
                            if t.won
                                divWins += 1
                        else if t.type == "REJ"
                            rejTotal += 1
                            if t.won
                                rejWins += 1
                        activeTrades.remove(i)

barcolor(dashboardHighlightInput ? barHighlight : na)

if barstate.islast
    if dashboardInput
        t_able.merge_cells(0, 0, 3, 0)
        cell(t_able, 0, 0, 'Hiệu Suất Đảo Chiều (Theo Thời Gian)', txtColor = DATA, align = text.align_center)

        divider(t_able, 1, 3)

        cell(t_able, 0, 2, 'Tín Hiệu', txtColor = HEADERS, align = text.align_left)
        cell(t_able, 1, 2, 'Tổng',  txtColor = HEADERS, align = text.align_right)
        cell(t_able, 2, 2, 'Thắng',   txtColor = HEADERS, align = text.align_right)
        cell(t_able, 3, 2, 'Tỷ Lệ Thắng',  txtColor = HEADERS, align = text.align_right)

        divider(t_able, 3, 3)

        float absRate = absTotal > 0 ? (absWins / absTotal) * 100 : 0
        cell(t_able, 0, 4, 'ABS', txtColor = DATA, align = text.align_left)
        cell(t_able, 1, 4, str.tostring(absTotal), txtColor = DATA, align = text.align_right)
        cell(t_able, 2, 4, str.tostring(absWins), txtColor = DATA, align = text.align_right)
        cell(t_able, 3, 4, str.tostring(absRate, '#.##') + '%', txtColor = absTotal > 0 ? (absRate >= 50 ? color.green : color.red) : DATA, align = text.align_right)

        divider(t_able, 5, 3)

        float exhRate = exhTotal > 0 ? (exhWins / exhTotal) * 100 : 0
        cell(t_able, 0, 6, 'EXH', txtColor = DATA, align = text.align_left)
        cell(t_able, 1, 6, str.tostring(exhTotal), txtColor = DATA, align = text.align_right)
        cell(t_able, 2, 6, str.tostring(exhWins), txtColor = DATA, align = text.align_right)
        cell(t_able, 3, 6, str.tostring(exhRate, '#.##') + '%', txtColor = exhTotal > 0 ? (exhRate >= 50 ? color.green : color.red) : DATA, align = text.align_right)

        divider(t_able, 7, 3)

        float divRate = divTotal > 0 ? (divWins / divTotal) * 100 : 0
        cell(t_able, 0, 8, 'DIV', txtColor = DATA, align = text.align_left)
        cell(t_able, 1, 8, str.tostring(divTotal), txtColor = DATA, align = text.align_right)
        cell(t_able, 2, 8, str.tostring(divWins), txtColor = DATA, align = text.align_right)
        cell(t_able, 3, 8, str.tostring(divRate, '#.##') + '%', txtColor = divTotal > 0 ? (divRate >= 50 ? color.green : color.red) : DATA, align = text.align_right)

        divider(t_able, 9, 3)

        float rejRate = rejTotal > 0 ? (rejWins / rejTotal) * 100 : 0
        cell(t_able, 0, 10, 'REJ', txtColor = DATA, align = text.align_left)
        cell(t_able, 1, 10, str.tostring(rejTotal), txtColor = DATA, align = text.align_right)
        cell(t_able, 2, 10, str.tostring(rejWins), txtColor = DATA, align = text.align_right)
        cell(t_able, 3, 10, str.tostring(rejRate, '#.##') + '%', txtColor = rejTotal > 0 ? (rejRate >= 50 ? color.green : color.red) : DATA, align = text.align_right)
    else
        t_able.clear(0, 0, 3, 10)

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