26 tháng Tám, 2026 · dữ liệu thị trường

Giải phẫu một thanh nến 1 phút: mọi thứ OHLCV không nói với backtest của bạn

Giải phẫu một thanh nến 1 phút: mọi thứ OHLCV không nói với backtest của bạn

Đây là một thanh nến. Hợp đồng vĩnh cửu Binance USDⓈ-M, BTCUSDT, phút bắt đầu từ 13:46:00 UTC. Nó xuất ra từ endpoint klines dưới dạng một mảng trần gồm mười hai giá trị, và đây là đơn vị đầu vào phổ biến nhất trong nghiên cứu quant dành cho nhà đầu tư cá nhân và những nhóm gần với đó. Hầu như mọi chiến lược mà chúng tôi tạo ra đều chạm vào thứ có hình dạng như thế này.

Vậy hãy tách nó ra, theo từng chỉ số (index), và xem backtest hiểu sai bao nhiêu phần trong đó.

Chỉ sốGiá trịTên
01773495960000thời gian mở (ms)
1"84120.50"giá mở
2"84177.90"giá cao nhất
3"84098.10"giá thấp nhất
4"84163.40"giá đóng
5"38.417"khối lượng cơ sở (BTC)
61773496019999thời gian đóng (ms)
7"3232108.94"khối lượng quote (USDT)
81204số lượng giao dịch
9"21.883"khối lượng mua taker (cơ sở)
10"1841203.55"khối lượng mua taker (quote)
11"0"bỏ qua

[0] và [6]: đây là phút nào, và theo đồng hồ của ai

Thời gian mở là 1773495960000, thời gian đóng là 1773496019999. Chú ý con số thứ hai: nó kết thúc bằng 19999, thiếu đúng một milli giây so với thời điểm mở của thanh nến kế tiếp. Khung thời gian này là nửa-mở (half-open), và sàn giao dịch đang nói rõ điều đó với bạn. Một nửa các lỗi dữ liệu mà tôi từng truy tìm bắt đầu từ việc ai đó coi cả hai đầu mút là bao gồm (inclusive) và đếm trùng một giao dịch ngay tại ranh giới, hoặc căn chỉnh một phép resample sao cho mỗi thanh nến 5 phút mượn một milli giây từ thanh nến bên cạnh.

Các mốc thời gian này là thời gian của matching engine trên sàn. Không phải đồng hồ của bạn, không phải đồng hồ ingest của nhà cung cấp dữ liệu, và cũng không phải đồng hồ mà snapshot funding sử dụng. Khi bạn join một chuỗi kline với một chuỗi funding rate hay một chuỗi open interest lấy từ một endpoint khác, bạn đang join giữa các nguồn chỉ khớp nhau trong khoảng một giây, hầu hết thời gian. Với một chiến lược khung 1 phút, đó là sai số thời gian 1,7% trên mỗi dòng được join. Với bất cứ thứ gì dưới một phút, đó là lỗi chết người.

Và thanh nến này được đóng dấu thời gian tại thời điểm mở của nó. Thông tin bên trong nó chỉ được biết đến sau 13:46:59.999. Mọi câu hỏi về quy ước đánh chỉ số — tôi có nên lệch đi một đơn vị không, tôi nên dùng mốc thời gian đóng hay mở thanh nến làm thời gian sự kiện — thực chất đều là câu hỏi lookahead giống nhau, chỉ khoác lớp áo khác.

[1] "84120.50": giá mở mà bạn không thể giao dịch

Giá mở là giá của giao dịch đầu tiên khớp trong khung thời gian đó. Đó là một giao dịch đã hoàn tất giữa hai người khác. Đến khi thanh nến tồn tại dưới dạng một dòng trong dataframe của bạn, giá đó đã cũ sáu mươi giây.

Thanh nến trước đóng ở mức 84109.80, vậy là có 10,70 USD biến động giá nằm trong khoảng trống giữa hai thanh nến 1 phút liền kề. Đó là 1,3 điểm cơ bản, vô hình trên biểu đồ, và tương đương khoảng một phần tư phí taker. Không sao. Nhưng hãy nhìn cùng một chuỗi dữ liệu trên một perp altcoin trong lúc công bố CPI của Mỹ, và những khoảng trống giữa các thanh nến đó lên tới 15 đến 40 điểm cơ bản. Một backtest phát tín hiệu tại giá đóng thanh nến và khớp lệnh tại giá mở thanh nến kế tiếp đang âm thầm giả định khoảng trống đó bằng không — và nó chỉ bằng không đúng vào những lúc không quan trọng.

[2] và [3]: giá cao nhất "84177.90", giá thấp nhất "84098.10"

Hai giá trị này là nơi hầu hết engine khớp lệnh gục ngã, vì vậy đây sẽ là phần dài nhất.

Giá cao nhất và thấp nhất là các mức giá cực trị đã bị chạm tới. Chúng không mang theo thông tin về kích thước hay thời lượng. Từ tape giao dịch thô của cùng phút này, tất cả những gì ở mức 84100.00 hoặc thấp hơn chỉ là 0,62 BTC trải qua chín lệnh khớp trong một khung 1,4 giây. Vậy một lệnh dừng (stop) trong backtest tại 84100 "khớp" — và nếu vị thế của bạn là 3 BTC, bạn đã ăn hết toàn bộ sổ lệnh khả kiến trên đường xuống, còn phần còn lại của khối lượng của bạn được khớp ở đâu đó trong đoạn hồi lại 84105–84130. Thanh nến nói rằng giá thấp nhất là 84098.10. Thanh nến không nói rằng chỉ có 52.000 USD giá trị danh nghĩa được giao dịch ở đó.

1204giao dịch trong phút đó
0.032 BTCkích thước lệnh khớp trung bình (~2,7 nghìn USD)
0.62 BTCtổng khối lượng giao dịch tại hoặc dưới mức giá thấp nhất
57%khối lượng do bên mua khởi tạo

Hướng ngược lại còn tệ hơn, vì nó tô vẽ có lợi cho bạn. Giả sử bạn có một lệnh limit sell đang chờ ở mức 84175. Giá cao nhất của thanh nến là 84177.90, vậy một engine ngây thơ sẽ khớp lệnh cho bạn ở mức 84175 và ghi nhận rebate maker thay vì chi phí taker. Việc lệnh đó có thực sự được khớp hay không phụ thuộc vào vị trí hàng chờ (queue position) tại mức giá đó, điều mà thanh nến không thể biết và bạn có lẽ chưa từng ghi lại. Bị chạm tới không đồng nghĩa với được khớp.

Quy tắc mà chúng tôi chốt trong engine khớp lệnh của mình: một lệnh limit đang chờ chỉ được khớp nếu thanh nến giao dịch xuyên qua mức giá đó, không chỉ đơn thuần chạm tới nó. Các lệnh khớp tại đúng mức giá cực trị đòi hỏi bằng chứng về khối lượng-tại-mức-giá từ tape giao dịch, nếu không sẽ bị từ chối. Điều này loại bỏ khoảng 6% số giao dịch từ một sổ lệnh mean-reversion điển hình và kéo Sharpe backtest của một ứng viên chiến lược từ 1,9 xuống 1,1. Ứng viên đó chưa từng thực sự tồn tại; quy tắc khớp lệnh chỉ là điều đầu tiên đủ thẳng thắn để nói ra điều đó.

Một cái bẫy liên quan: đường đi trong thanh nến (intrabar path). Nếu biên độ của một thanh nến trải qua cả mức stop và mức take-profit của bạn, OHLCV không thể cho bạn biết mức nào xảy ra trước. Mọi engine đều phải chọn một quy ước. Engine của chúng tôi luôn giả định stop xảy ra trước, điều này mang tính bi quan và đôi khi sai, nhưng không bao giờ tạo ra một khoản thắng giả. Nếu engine của bạn giả định take-profit xảy ra trước, những thanh nến biên độ rộng sẽ tự tạo ra lợi nhuận từ sự mơ hồ đó — và những thanh nến biên độ rộng chính là những thanh nến chi phối phân phối PnL của bạn.

[4] "84163.40": con số kém vững chắc nhất trong thanh nến

Giá đóng là lệnh khớp cuối cùng trong khung thời gian đó. Vậy thôi. Nó có thể là một lô lẻ 0,002 BTC từ một bot đang tròn hóa vị thế lúc 13:46:59,8. Chính cái tick đơn lẻ, mang tính ngẫu nhiên về cấu trúc này lại là thứ mà hầu hết pipeline nghiên cứu dùng để tính mọi tín hiệu, mark mọi vị thế, và đánh giá mọi lệnh thoát.

Trên các perp BTC thì điều này hầu như không quan trọng; nhưng trên một perp altcoin thanh khoản mỏng vào lúc 04:00 UTC, nó lại quan trọng vô cùng, và chênh lệch giữa giá đóng của cùng một phút ở hai sàn có thể vượt quá toàn bộ edge trên mỗi giao dịch của bạn. Khi PnL của một chiến lược phụ thuộc cụ thể vào giá đóng, chúng tôi chạy lại nó bằng cách mark theo mark price của sàn — vốn được suy ra từ chỉ số (index) và khó bị tác động hơn nhiều. Nếu kết quả khác biệt, chiến lược đó thực chất đang giao dịch trên một hiện tượng giả (artifact).

[5] và [7]: "38.417" và "3232108.94", khối lượng tính theo đơn vị gì

Khối lượng cơ sở là BTC; khối lượng quote là USDT. Cả hai đều có ở đây, đây là một sự ưu ái không phải sàn nào cũng dành cho bạn. Lý do cần quan tâm là việc tổng hợp giữa nhiều sàn. Các hợp đồng ký quỹ bằng coin được yết giá theo hợp đồng với giá trị danh nghĩa 100 USD. Một số nguồn dữ liệu cổ phiếu báo cáo theo lô tròn (round lot). Các sàn dự đoán (prediction market) báo cáo theo số lượng share, trong đó mỗi share là một claim nhị phân định giá bằng đô la. Cộng "khối lượng" trên một tập hợp hỗn tạp mà không chuẩn hóa về một đơn vị danh nghĩa duy nhất sẽ tạo ra một bảng xếp hạng thanh khoản hoàn toàn vô nghĩa — và nó sẽ vô nghĩa theo cách trông có vẻ ổn định, đủ để qua được review.

Hãy chuẩn hóa mọi thứ về giá trị danh nghĩa quote ngay tại giai đoạn ingest. Vẫn lưu trường dữ liệu thô, nhưng đừng bao giờ để một chiến lược nhìn thấy nó.

[8] 1204: trường dữ liệu nên định hình mô hình impact của bạn

Lấy khối lượng cơ sở chia cho số lượng giao dịch cho ra một lệnh khớp trung bình 0,032 BTC, khoảng 2.700 USD. Nếu chiến lược ứng viên của bạn muốn vào lệnh 250.000 USD trong một lần, nó đang yêu cầu có kích thước gấp khoảng 92 lần giao dịch điển hình trong phút đó. Chính con số này — không phải một hằng số trượt giá (slippage) chung 5 điểm cơ bản nào đó — mới là thứ nên chi phối số hạng impact căn bậc hai của bạn. Chúng tôi tính tỷ lệ tham gia (participation rate) trên mỗi thanh nến như một cột dữ liệu hạng nhất, và loại bỏ những chiến lược mà lệnh vào trung vị vượt quá vài phần trăm giá trị danh nghĩa của thanh nến, vì mọi thứ phía sau điểm đó chỉ là viễn tưởng.

Số lượng giao dịch cũng là cách rẻ tiền để phát hiện những phút bất thường. Khối lượng bình thường, nhưng số lượng giao dịch tụt xuống chỉ còn 11? Ai đó đã thực hiện một block trade. Khối lượng bình thường, nhưng số lượng giao dịch lên tới 9.000? Đó là một cơn liquidation cascade đang bị nghiền nát thành từng mảnh nhỏ.

[9] và [10]: "21.883", trường dữ liệu mà ai cũng vứt bỏ

Khối lượng mua taker (cơ sở). 21,883 trên tổng 38,417 BTC trong thanh nến này là do bên mua khởi tạo, vậy delta khối lượng có dấu là +5,349 BTC và tỷ lệ phe chủ động (aggressor split) là 57/43 nghiêng về bên mua. Sàn giao dịch đang trao cho bạn thông tin order flow imbalance, hoàn toàn miễn phí, trong một trường dữ liệu mà hầu hết mọi người chưa từng đọc chỉ vì pandas không đặt tên cột đó hộ họ.

Tôi không cho rằng nó tự dự đoán được lợi nhuận; các chiến lược delta ngây thơ là một trong những cách đáng tin cậy nhất để cống nạp cho sổ phí. Nhưng đây thực sự là một phép đo khác hoàn toàn so với giá, nó có sẵn trong cùng một request mà bạn đã đang thực hiện, và nó cho phép bạn phân biệt một đợt tăng giá được mua lên với một đợt tăng giá xảy ra vì bên bán rút lui. Hai trường hợp đó trông giống hệt nhau trên OHLC nhưng hành xử khác nhau mười phút sau đó. Research agent của chúng tôi coi một giả thuyết bỏ qua tỷ lệ taker trên một sàn có công bố dữ liệu này là đang bỏ sót bằng chứng ngay trước mắt.

[11] "0": bỏ qua — và mọi thứ khác không có ở đây

Chỉ số 11 là một trường đã bị deprecate, luôn luôn bằng không. Điều thú vị hơn là danh sách những thứ mà thanh nến này không chứa: không có giá bid, không có giá ask, không có spread, không có độ sâu sổ lệnh, không có funding rate, không có open interest, không có liquidation, không có mark price, không có index price. Và quan trọng nhất, không có cách nào để biết liệu lệnh của bạn sẽ là maker hay taker — chính là sự khác biệt giữa việc trả 0,045% và kiếm được 0,01% trên sàn này.

Vậy nên bất kỳ mô hình phí nào chỉ được xây dựng dựa trên kline đơn thuần chính là một giả định đang khoác lên mình vỏ bọc của một con số. Chúng tôi giải quyết điều này bằng cách buộc mọi chiến lược phải khai báo trước phong cách thực thi (execution style) của nó, và sau đó tính phí taker cho bất cứ thứ gì không thể chứng minh điều ngược lại.

Thanh nến chưa bao giờ xuất hiện

Phần cuối cùng, và cũng là phần cắn đau nhất bên ngoài các cặp coin lớn. Một phút không có giao dịch nào sẽ không tạo ra kline nào cả. Các nhà cung cấp dữ liệu và thư viện thường forward-fill nó: mở = cao nhất = thấp nhất = đóng = giá đóng trước đó, khối lượng bằng 0. Indicator của bạn cứ thế tính toán vui vẻ. Chiến lược của bạn nhìn thấy một dòng dữ liệu hợp lệ và có thể tạo ra tín hiệu trên một phút mà không ai trên thế giới giao dịch công cụ đó.

Trên một perp mid-cap mà chúng tôi ingest, 4,1% các thanh nến 1 phút trong một khung mười hai tháng không có giao dịch nào. Một ứng viên chiến lược mean-reversion trên symbol đó đặt tới 38% số lệnh vào của nó trên các thanh nến tổng hợp (synthetic), vì các mức giá tổng hợp bằng phẳng là mồi ngon cho bất cứ thứ gì đo độ lệch so với đường trung bình động. Nó backtest ra kết quả đẹp tuyệt vời. Nhưng thực chất nó đang giao dịch các lỗ hổng trong dữ liệu.

Đó là lý do vì sao lớp ingest hiện nay mang theo một cờ boolean synthetic cho mỗi thanh nến, được truyền qua mọi phép resample, và bộ kiểm định (verification gauntlet) sẽ loại bất kỳ chiến lược nào có giao dịch tập trung vào đó. Một cột dữ liệu rẻ tiền. Nó đã loại nhiều ứng viên chiến lược hơn bất kỳ indicator nào chúng tôi từng viết.

Mười hai giá trị. Bốn trong số đó thường xuyên bị hiểu sai, hai trong số đó thường xuyên bị vứt bỏ, và cả một hạng mục hoàn toàn vắng mặt trong dòng dữ liệu, được engine tự tưởng tượng ra. Trước lần backtest tiếp theo của bạn, hãy lấy một thanh nến thô từ chính kho dữ liệu của bạn và đọc to từng trường dữ liệu, đối chiếu với những gì logic khớp lệnh của bạn đang giả định về nó. Đây là một bài tập hai mươi phút, và tôi chưa từng thấy ai làm nó mà không phát hiện ra điều gì cả.

ohlcvcấu trúc vi mô thị trườngkỹ thuật dữ liệubacktestinghợp đồng tương lai crypto
← Tất cả bài viết