OptiNod Academy
Lookahead và repainting — Vì sao tín hiệu hoàn hảo trên biểu đồ lại không tồn tại khi giao dịch thật
Giải thích vì sao nhiều tín hiệu trông hoàn hảo trên biểu đồ thực ra chưa hề tồn tại tại thời điểm đó, thông qua cơ chế xác nhận nến và cách thiết lập request.security.
Repainting là hiện tượng chỉ báo vẽ lại tín hiệu trong quá khứ sau khi nến đã đóng. Look-ahead bias là lỗi trong đó backtest tính tín hiệu bằng cách nhìn trước dữ liệu tương lai. Nguyên nhân của hai vấn đề này khác nhau, nhưng kết quả giống nhau: trên biểu đồ đã đi qua, tín hiệu xuất hiện đúng những vị trí hoàn hảo; nhưng nếu bạn quan sát biểu đồ đó theo thời gian thực, tại thời điểm ấy tín hiệu có thể chưa từng xuất hiện, hoặc nằm ở một vị trí khác.
Cách hiểu phổ biến thường rất đơn giản: chỉ báo cho tín hiệu ở nến nào thì vào lệnh ở nến đó. Mũi tên nằm đúng đáy, tỷ lệ thắng backtest vượt 70%, nên nhiều người tin ngay. Niềm tin này sụp đổ khi giao dịch live. Khi mở cùng một chỉ báo trên biểu đồ thời gian thực và chờ đợi, mũi tên xuất hiện trước khi nến đóng có thể biến mất ngay lúc nến kết thúc, hoặc dịch sang một nến bên cạnh. Tín hiệu từng thấy trong backtest thực ra đã được vẽ lại sau đó.
Điểm cốt lõi cần nắm là: rất nhiều chiến lược có kết quả backtest đẹp trên TradingView trông tốt chỉ vì mã đã nhìn trước tương lai, còn bản thân chiến lược thì không có lợi thế. Chỉ một dòng thiết lập lookahead trong request.security(), một tùy chọn tính toán trên nến chưa hoàn tất, hoặc một công cụ có bản chất tự vẽ lại cũng đủ tạo ra khác biệt. Khi hiểu cơ chế, bạn sẽ biết backtest nào cần loại bỏ, và tín hiệu live nào phải chờ thêm một nến mới được xem là hợp lệ.

Thời điểm tín hiệu được xác nhận khác với thời điểm có thể vào lệnh
Điểm khởi đầu để hiểu repainting và lookahead là hai trạng thái của nến. Nến quá khứ đã đóng có giá mở cửa, cao nhất, thấp nhất và đóng cửa cố định, không thay đổi nữa. Nến realtime đang chạy thì giá cao nhất, thấp nhất và đóng cửa còn thay đổi liên tục cho đến khi nến đóng. Mã trên TradingView hành xử khác nhau trong hai trạng thái này, nhưng phần lớn người dùng không để ý đến sự khác biệt đó.
Giả sử chỉ báo hiển thị mũi tên "mua ở nến này". Nếu nến đó vẫn đang chạy, mũi tên chỉ là giá trị tạm thời được tính theo giá đóng cửa hiện tại. Nếu giá đảo chiều trước khi nến đóng, điều kiện sẽ không còn đúng và mũi tên biến mất. Vì vậy, nếu bạn vào lệnh theo tín hiệu trên nến đang chạy, đến lúc nến đóng bạn có thể đã vào một vị thế mà tín hiệu cuối cùng chưa từng tồn tại.
Tín hiệu có thể dùng trong thực chiến chỉ là tín hiệu đã được xác nhận sau khi nến đóng và giá đóng cửa cố định. Tín hiệu được xác nhận vào đúng khoảnh khắc nến kết thúc, còn thời điểm sớm nhất bạn có thể vào lệnh theo tín hiệu đó là khi nến tiếp theo mở cửa. Nếu backtest giả định lệnh được khớp tại giá đóng cửa của nến tín hiệu, bản thân giả định đó đã chứa một mức lookahead nhẹ, vì trong thực tế gần như không thể thấy nến vừa đóng rồi khớp đúng giá đóng cửa ngay khoảnh khắc đó. Nếu bỏ qua khoảng cách một nến giữa xác nhận tín hiệu và khả năng vào lệnh, backtest sẽ đẹp hơn thực chiến.
lookahead_on trong request.security kéo giá trị chốt sau cùng về hiện tại
Hàm dùng để lấy dữ liệu khung thời gian cao hơn, HTF, là request.security(). Bạn dùng nó khi muốn tham chiếu giá đóng cửa tuần trên biểu đồ ngày, hoặc kéo xu hướng 1 giờ xuống biểu đồ 5 phút. Look-ahead bias thường len vào nhiều nhất qua tham số lookahead của hàm này.
Nếu gọi request.security(syminfo.tickerid, "1W", high, lookahead=barmerge.lookahead_on) trên biểu đồ ngày, hàm sẽ kéo trước mức cao cuối cùng của cây nến tuần đang chạy về ngay từ ngày đầu tiên của tuần đó. Tài liệu chính thức cũng cảnh báo rằng cách gọi này tạo ra kết quả sai lệch nguy hiểm trên nến quá khứ vì nó lấy dữ liệu tương lai. Trên biểu đồ, mọi thứ có vẻ hoạt động hoàn hảo, nhưng sự hoàn hảo đó đến từ việc đã nhìn trước tương lai.
Lấy ví dụ nến tuần BTC mở vào tuần bắt đầu ngày 11 tháng 3 năm 2024. Trong tuần đó, BTC đạt đỉnh 73.777 USD vào ngày 14 tháng 3 và đóng tuần ở 68.393 USD. Nếu tham chiếu đỉnh tuần này trên biểu đồ ngày bằng lookahead_on, giá trị 73,777 đã xuất hiện trên biểu đồ từ thứ Hai, ngày 11 tháng 3. Tức là ba ngày trước khi mức 73,777 thực sự được tạo ra. Nếu dùng giá trị này để backtest logic "short gần đỉnh tuần", mô hình đã biết trước mức 73,777 từ ngày 11 tháng 3, nên tỷ lệ thắng sẽ cao một cách phi thực tế. Trong thực chiến, không có cách nào biết được con số đó vào ngày 11 tháng 3. Vùng short hoàn hảo trên biểu đồ chỉ được tạo ra từ thông tin chưa tồn tại tại thời điểm ấy.

lookahead_off là mặc định an toàn, còn tham chiếu dữ liệu tương lai là rò rỉ ngay trong công thức
Mẫu đúng là dùng barmerge.lookahead_off. Với thiết lập này, dữ liệu của nến khung thời gian cao hơn chỉ được đưa xuống khung thấp hơn sau khi nến đó thật sự đóng. Nến tuần phải đóng vào Chủ nhật thì giá cao nhất và giá đóng cửa của tuần đó mới được phản ánh trên biểu đồ ngày, nên nến quá khứ không nhìn thấy tương lai. Nếu không khai báo, mặc định là lookahead_off, nhưng nhiều đoạn mã sao chép trên internet lại ghi rõ lookahead_on, vì vậy bạn phải kiểm tra trực tiếp.
Nếu muốn dùng an toàn giá trị HTF đã xác nhận nhưng vẫn nhận tín hiệu sớm nhất có thể, tài liệu chính thức khuyến nghị một dạng cụ thể: request.security(syminfo.tickerid, "1W", expression[1], lookahead=barmerge.lookahead_on). Nghĩa là thêm offset [1] vào biểu thức để tham chiếu giá trị của cây nến HTF vừa đóng trước đó. Cách này hoạt động nhất quán giữa dữ liệu quá khứ và realtime. Cái giá phải trả là tín hiệu chậm một nến, nhưng đó cũng chính là độ trễ mà thực chiến vốn buộc phải chấp nhận.
Một dạng lookahead khác là tham chiếu dữ liệu tương lai và offset âm. Nếu dùng chỉ số âm như close[-1] để tham chiếu giá đóng cửa của nến tiếp theo, đó là rò rỉ tương lai rõ ràng. Nó tạo ra kết quả tương tự việc dùng lookahead_on trong security() mà không có offset, chỉ là trực tiếp hơn. Khi kết quả backtest tốt bất thường, thứ đầu tiên cần nghi ngờ trong mã là chỉ số âm và lookahead_on.
Chỉ báo tính trước khi nến xác nhận sẽ tạo tín hiệu xuất hiện rồi biến mất
Nếu chỉ báo hoặc chiến lược đặt calc_on_every_tick=true, phép tính sẽ chạy lại theo từng tick trong lúc nến đang hình thành. Trên biểu đồ live, chính thiết lập này khiến tín hiệu liên tục xuất hiện rồi biến mất. Khi giá đóng cửa tạm thời thỏa điều kiện, mũi tên xuất hiện; khi giá tiếp tục di chuyển và giả định giá đóng cửa thay đổi, mũi tên biến mất.
Vấn đề nằm ở sự không khớp giữa backtest và realtime. Như tài liệu chính thức chỉ ra, chiến lược chạy trên nến quá khứ theo giá đóng cửa nến, nhưng nếu calc_on_every_tick=true, nó chạy theo từng tick trong realtime. Hai cách chạy khác nhau, nên kết quả quá khứ và hành vi live sẽ lệch nhau. Trên phần quá khứ của biểu đồ, mọi nến đều đã đóng nên tín hiệu trông rất gọn gàng, nhưng vẻ gọn gàng đó chỉ là kết quả nhìn lại sau khi nến đã kết thúc.
Công cụ tiêu chuẩn để tránh vấn đề này là barstate.isconfirmed. Khi thêm and barstate.isconfirmed vào điều kiện vào lệnh, tín hiệu chỉ phát sinh ở khoảnh khắc cuối cùng của nến, khi giá đóng cửa đã được xác nhận. Nến quá khứ luôn được xem là đã xác nhận, nên điều kiện này được tái hiện giống nhau giữa quá khứ và realtime. Nếu muốn loại bỏ tín hiệu live xuất hiện rồi biến mất, đây là cách đơn giản nhất. Trước khi vội vào lệnh theo mũi tên trên nến đang chạy, hãy kiểm tra xem tín hiệu đó đã được lọc bằng barstate.isconfirmed hay chưa.
Công cụ có bản chất vẽ lại sẽ dời swing cuối cùng một cách hồi tố
Có những công cụ mà thay đổi thiết lập cũng không giải quyết được, vì bản thân công thức đã là repainting. ZigZag là ví dụ tiêu biểu. ZigZag chỉ xác nhận đỉnh hoặc đáy swing sau khi có một mức đảo chiều đủ lớn theo phần trăm đã định. Trước khi đảo chiều được xác nhận, swing cuối cùng tiếp tục di chuyển theo giá. Nếu giá lên cao hơn, đỉnh cuối cùng được dời lên; chỉ sau khi đảo chiều được xác nhận, nó mới cố định tại vị trí đó.
Quay lại ví dụ BTC đạt 73.777 USD vào ngày 14 tháng 3. ZigZag không xác nhận 73,777 là đỉnh swing ngay trong ngày 14 tháng 3. Nó chỉ xác nhận sau đó, khoảng ngày 16 hoặc 19 tháng 3, khi giá đã giảm đủ sâu. Ngày 19 tháng 3, BTC đã rơi xuống đáy 61.555 USD. Chỉ khi ngưỡng đảo chiều được đáp ứng, ZigZag mới cố định đỉnh tại 73,777; trước đó, đoạn cuối cùng vẫn dao động theo giá. Trên biểu đồ quá khứ, đỉnh 73,777 hiện ra gọn gàng, như thể ai cũng có thể short ngay tại đó. Nhưng tại thời điểm đỉnh ấy được xác nhận, giá đã giảm hơn 12.000 USD.
Một số biến thể Supertrend và công cụ Fibonacci tự động cũng có cùng bẫy. Công cụ tự vẽ các mức hồi dựa trên swing cuối cùng sẽ dịch chuyển toàn bộ các mức nếu swing đó chưa được xác nhận. Đường hồi 0.618 được vẽ tự động có thể trông như một hỗ trợ hoàn hảo trên biểu đồ, nhưng thời điểm đường đó xuất hiện tại đúng vị trí ấy có thể là sau khi hỗ trợ đã được xác nhận. Không nên tin diện mạo quá khứ của công cụ tự động làm căn cứ vào lệnh.

Theo tín hiệu HTF realtime một cách máy móc sẽ khiến tín hiệu đảo chiều ngay trong nến
Cách dùng sai đầu tiên là tin vào tỷ lệ thắng backtest chỉ vì thấy mũi tên của công cụ repainting. Nếu chiến lược dựa trên ZigZag có tỷ lệ thắng 80%, rất có thể tỷ lệ đó được tính từ các vị trí swing đã được xác nhận sau khi giá đã chạy. Trong realtime, bạn phải quyết định vào lệnh trước khi swing được xác nhận, nên không thể vào đúng vị trí mà backtest giả định. Bản thân con số tỷ lệ thắng đã là phép tính nhìn trước tương lai.
Cách dùng sai thứ hai là theo tín hiệu khung thời gian cao hơn theo realtime mà không chờ xác nhận. Khi xem tín hiệu 4 giờ hoặc ngày trên biểu đồ 5 phút, tín hiệu đó sẽ còn thay đổi cho đến khi nến HTF đóng. Rất thường gặp tình huống nến ngày có vẻ sẽ đóng xanh nên tín hiệu mua xuất hiện, nhưng đến gần lúc nến ngày đóng, nến đảo thành đỏ và tín hiệu biến mất. Tín hiệu HTF chỉ nên được xem là xác nhận sau khi nến của chính khung thời gian đó đóng. Nếu cứ kiểm tra màu xu hướng của nến ngày đang chạy mỗi 5 phút để giao dịch, bạn sẽ liên tục vào rồi ra theo cùng một tín hiệu và tài khoản bị bào mòn dần bởi phí.
Cách dùng sai thứ ba là cảnh báo, Alert. Nếu đặt cảnh báo TradingView theo điều kiện dựa trên calc_on_every_tick, cảnh báo sẽ kích hoạt ở mọi khoảnh khắc điều kiện được chạm trong lúc nến đang chạy. Đến lúc nến đóng, điều kiện có thể đã không còn đúng, nhưng cảnh báo đã được gửi. Nếu không đặt tùy chọn cảnh báo là "Once Per Bar Close", bạn có thể vào lệnh theo một cảnh báo giữa nến tại vị trí mà đến khi nến đóng không còn là tín hiệu nữa.
Quy trình kiểm tra một chỉ báo có repainting hay không
Bạn có thể kiểm tra repainting trực tiếp trên biểu đồ mà không cần đọc mã. Khi nghi ngờ một chỉ báo, hãy đi qua quy trình sau.
- [ ] Chạy Bar Replay: Dùng tính năng Bar Replay của TradingView, chọn một thời điểm bất kỳ trong quá khứ, tiến từng nến một và ghi lại vị trí mũi tên tín hiệu. Nếu mũi tên di chuyển hoặc biến mất khi nến đóng, đó là công cụ repainting.
- [ ] Kiểm tra thiết lập lookahead: Trong mã nguồn chỉ báo, tìm
request.securityvà xem cólookahead=barmerge.lookahead_onđược dùng mà không có offset[1]hay không. Chỉ cần một trường hợplookahead_onkhông offset là đã có rò rỉ tương lai. - [ ] Tìm chỉ số âm: Tìm
[-trong mã nguồn để kiểm tra các offset âm nhưclose[-1]. Chỉ số âm là cách rò rỉ trực tiếp vì nó nhìn trước nến tiếp theo. - [ ] Kiểm tra tùy chọn tính theo tick: Nếu là chiến lược, xem giá trị
calc_on_every_tick; nếu làtrue, kiểm tra điều kiện vào lệnh có kèmbarstate.isconfirmedhay không. Nếu không có cổng xác nhận này, tín hiệu realtime sẽ dao động trong nến. - [ ] Đối chiếu quá khứ và realtime: Mở cùng chỉ báo trên biểu đồ live, quan sát trong vài ngày theo thời gian thực, rồi sau đó xem lại vị trí các tín hiệu đã xuất hiện trên biểu đồ quá khứ. Nếu hai vị trí khác nhau, backtest của chỉ báo đó không đáng tin.

Các kiểm tra nhanh để lọc repainting
Khi không có thời gian đi qua toàn bộ quy trình kiểm tra, vẫn có vài cách lọc nhanh. Thứ nhất, hãy nghi ngờ trước các chiến lược có tỷ lệ thắng backtest cao bất thường. Với một chiến lược trend-following đơn giản, tỷ lệ thắng thực tế thường nằm trong khoảng 40-55%. Nếu tỷ lệ thắng vượt 70%, khả năng cao đó là kiểu martingale có tỷ lệ lời/lỗ cực thấp, hoặc có lookahead nhìn trước tương lai. Khi con số đẹp quá mức, thói quen giảm thiểu thua lỗ là nghi ngờ mã trước tiên.
Thứ hai, đừng tin ngay cả khi mô tả chỉ báo ghi "non-repainting". Người viết có thể vô tình để lại lookahead_on, chỉ xử lý phần đa khung thời gian nhưng vẫn để phép tính ZigZag bị repainting. Kết quả tự kiểm tra bằng Bar Replay quan trọng hơn mô tả.
Thứ ba, hãy đặt mặc định là luôn nhận tín hiệu trễ một nến. Với bất kỳ chỉ báo nào, hãy xây dựng setup theo giả định bỏ qua giá đóng cửa của nến tín hiệu và vào lệnh ở giá mở cửa của nến tiếp theo. Khoảng đệm một nến này là ranh giới giữa tín hiệu có thể đảo chiều trong nến và tín hiệu đã xác nhận, đồng thời là cách giảm mạnh nhất khoảng cách giữa backtest và thực chiến. Tín hiệu càng hoàn hảo trên biểu đồ, bạn càng phải hỏi trước: sự hoàn hảo đó có phải chỉ xuất hiện sau khi nến đã đóng hay không.