Trang chủBlogPhân Tích Cổ PhiếuPoint-in-Time Data là gì và vì sao bắt buộc khi backtest báo cáo tài chính?
Point-in-Time Data là gì và vì sao bắt buộc khi backtest báo cáo tài chính?
Phân Tích Cổ Phiếu

Point-in-Time Data là gì và vì sao bắt buộc khi backtest báo cáo tài chính?

Point-in-Time Data

Ngọc Hải LươngNgọc Hải Lương
14/8/2026
9 phút phút đọc
15 lượt xem

Một lỗi có thể khiến backtest factor nhìn tốt hơn thực tế rất nhiều nhưng lại khá khó phát hiện bằng mắt là dùng đúng dữ liệu, nhưng dùng sai thời điểm. Điều này xuất hiện đặc biệt nhiều khi chiến lược sử dụng báo cáo tài chính. Doanh thu quý I thuộc giai đoạn kết thúc ngày 31/3 không có nghĩa nhà đầu tư đã biết doanh thu đó vào ngày 31/3. ROE năm 2025 cũng không thể được strategy sử dụng từ đầu năm 2025 chỉ vì database đang gắn nó với fiscal year 2025. Nếu backtest vô tình làm như vậy, model đang được cung cấp thông tin từ tương lai.

Point-in-Time Data, thường viết tắt là PIT, giải quyết đúng vấn đề này. Với mỗi ngày lịch sử T, dataset phải tái tạo được những gì thị trường thực sự có thể biết tại T, chứ không phải phiên bản dữ liệu hoàn chỉnh mà chúng ta nhìn thấy hôm nay.

77d4c42d-128b-4ff5-8595-59a507dc1165.png

1. Báo cáo tài chính có ít nhất hai mốc thời gian, nhưng backtest thường chỉ lưu một

Giả sử doanh nghiệp A có quý IV kết thúc ngày 31/12/2025 và báo cáo cho thấy lợi nhuận sau thuế đạt 1.200 tỷ đồng. Dataset rất thường được lưu như sau:

ticker      period_end      net_income
AAA         2025-12-31      1,200

Nếu join bảng này với giá cổ phiếu theo period_end, strategy có thể bắt đầu sử dụng lợi nhuận 1.200 tỷ từ ngày 31/12/2025. Nhưng nhà đầu tư tại thời điểm đó chưa chắc đã biết con số này. Doanh nghiệp có thể đến cuối tháng 1 hoặc tháng 2/2026 mới công bố báo cáo.

Dataset đúng cần thêm ít nhất một trường:

ticker   period_end   public_date   net_income
AAA      2025-12-31   2026-01-28    1,200

period_end trả lời câu hỏi “con số này thuộc kỳ kinh doanh nào?”, còn public_date trả lời câu hỏi quan trọng hơn với backtest: “từ thời điểm nào nhà đầu tư có thể biết con số này?”

Sự khác biệt này không chỉ là một quy ước kỹ thuật. Ngay trong hệ thống EDGAR của SEC, CONFORMED PERIOD OF REPORT, FILED AS OF DATE và thời điểm hồ sơ được hệ thống chấp nhận được lưu thành những trường khác nhau. SEC cũng cung cấp dữ liệu financial statements dựa trên các báo cáo thực sự đã được doanh nghiệp nộp.

Ví dụ một factor rất đơn giản:

quality = ROE > 20%

Nếu ROE năm 2025 chỉ được công bố ngày 28/1/2026 thì mọi tín hiệu trước ngày đó đều phải sử dụng thông tin cũ. Backtest sai thường vô tình làm:

df["year"] = df.index.year
df = df.merge(financials, on=["ticker", "year"])

Nhìn code không có gì bất thường. Nhưng toàn bộ năm có thể đã được gắn với một báo cáo chỉ xuất hiện sau khi năm kết thúc. Strategy từ đầu năm được “biết trước” doanh thu, lợi nhuận, EPS và ROE cuối năm.

Đây là look-ahead bias ở tầng dữ liệu.

Nó nguy hiểm hơn lỗi dùng close.shift(-1) vì không dễ nhìn thấy trong code strategy. Signal có thể được viết hoàn toàn đúng nhưng dataset đầu vào đã chứa tương lai. Các nghiên cứu về PIT pipelines cũng chỉ ra những nguồn leakage phổ biến như sử dụng fundamentals đã được restate, bỏ qua khoảng cách giữa kỳ báo cáo và ngày thông tin thực sự xuất hiện, hoặc xây dữ liệu cross-sectional bằng thông tin chưa đồng thời khả dụng cho tất cả doanh nghiệp.

Một nguyên tắc ngắn gọn có thể dùng cho toàn bộ hệ thống:

Tại ngày T, chỉ được sử dụng bản ghi có public_date <= T.

Không phải period_end <= T.

2. Lỗi còn khó hơn khi báo cáo được điều chỉnh sau đó

Giả sử doanh nghiệp A công bố báo cáo lần đầu:

28/01/2026
Net Income = 1,200 tỷ

Hai tháng sau doanh nghiệp công bố báo cáo kiểm toán hoặc điều chỉnh:

30/03/2026
Net Income = 1,050 tỷ

Database hiện tại có thể chỉ giữ con số cuối cùng:

2025 Net Income = 1,050 tỷ

Nếu lấy database đó để backtest tháng 2/2026, strategy sẽ sử dụng 1.050 tỷ, mặc dù nhà đầu tư vào tháng 2 chỉ có thể nhìn thấy 1.200 tỷ.

Đây là lý do một PIT database tốt không chỉ lưu ngày công bố mà còn phải giữ version history.

Ví dụ:

ticker  period_end   effective_date   version   net_income
AAA     2025-12-31   2026-01-28       1         1,200
AAA     2025-12-31   2026-03-30       2         1,050

Nếu backtest tại:

T = 2026-02-15

kết quả phải là:

Net Income = 1,200

Nếu:

T = 2026-04-15

mới được sử dụng:

Net Income = 1,050

Có thể hình dung database PIT như một hàm:

Value(ticker, metric, period, T)

không chỉ là:

Value(ticker, metric, period)

Tham số T chính là thứ khiến dữ liệu trở thành Point-in-Time.

Một cách triển khai khá thực tế bằng Pandas là dùng merge_asof. Giả sử bảng giá:

prices

có:

date        ticker   close
2026-01-27  AAA      25.1
2026-01-28  AAA      25.7
2026-01-29  AAA      26.2
...

và fundamentals:

effective_date   ticker   net_income   equity
2025-10-28       AAA      850          5,200
2026-01-28       AAA      1,200        5,600
2026-03-30       AAA      1,050        5,600

Có thể join phiên bản gần nhất đã tồn tại tại ngày giao dịch:

import pandas as pd

prices["date"] = pd.to_datetime(prices["date"])
fund["effective_date"] = pd.to_datetime(fund["effective_date"])

prices = prices.sort_values(["date", "ticker"])
fund = fund.sort_values(["effective_date", "ticker"])

pit = pd.merge_asof(
    prices,
    fund,
    left_on="date",
    right_on="effective_date",
    by="ticker",
    direction="backward"
)

Ngày 27/1 sẽ nhận báo cáo cũ. Ngày 28/1 trở đi mới nhận báo cáo mới. Sau 30/3, bản điều chỉnh mới bắt đầu có hiệu lực.

Từ dataset đó mới tính:

pit["roe"] = pit["net_income"] / pit["equity"]

Một lỗi thường gặp là làm ngược trình tự: lấy database fundamentals hiện tại, tính ROE hoàn chỉnh cho mọi kỳ rồi forward-fill ngược vào lịch sử. Backtest vẫn chạy, chart vẫn đẹp, thậm chí Sharpe có thể rất cao, nhưng nó đang kiểm tra một strategy mà nhà đầu tư thật không thể thực hiện.

SEC Company Facts API cũng cho thấy vì sao cần cẩn thận với cách lấy fundamentals: API có thể chứa nhiều filing liên quan tới cùng một fact và các API tổng hợp có logic chọn fact được filing gần nhất phù hợp với kỳ yêu cầu. Điều đó rất tiện cho phân tích hiện tại nhưng không đồng nghĩa có thể lấy trực tiếp “latest value” rồi coi đó là giá trị lịch sử PIT.

3. PIT thay đổi hoàn toàn cách backtest Value, Quality và Fundamental Momentum

Ảnh hưởng rõ nhất xuất hiện với những chiến lược sử dụng P/E, P/B, ROE, EPS Growth, Revenue Growth, Operating Margin, Debt/Equity hay Earnings Surprise.

Giả sử muốn kiểm tra một strategy khá thực tế tại Việt Nam:

ROE > 15%
P/B < 2
Revenue Growth > 10%
Momentum 60D > 0

Mỗi tháng chọn 20 cổ phiếu tốt nhất rồi nắm giữ một tháng.

Phần price momentum tương đối đơn giản vì giá ngày T đã biết tại T. Nhưng ba feature đầu tiên đều phụ thuộc vào dữ liệu báo cáo tài chính.

Code naïve có thể trông rất hợp lý:

df["roe"] = df["net_income"] / df["equity"]
df["revenue_growth"] = df["revenue"] / df["revenue_last_year"] - 1
df["pb"] = df["market_cap"] / df["book_value"]

signal = (
    (df["roe"] > 0.15) &
    (df["pb"] < 2) &
    (df["revenue_growth"] > 0.10) &
    (df["momentum_60d"] > 0)
)

Sai hay đúng không nằm ở công thức. Nó nằm ở net_income, equity, revenuebook_value được biết từ ngày nào.

Nếu fundamentals là PIT, logic trên dùng được.

Nếu fundamentals được backfill từ database hiện tại, toàn bộ strategy có thể bị nhiễm tương lai.

Một cách thiết kế data model tốt hơn là giữ rõ:

ticker
fiscal_period
report_date
public_date
effective_date
revision_date
metric
value
source
version

Sau đó tại mỗi rebalance date T, query:

SELECT *
FROM financial_facts
WHERE effective_date <= T

và với mỗi ticker + fiscal_period + metric, chỉ giữ version mới nhất tính đến T.

Pseudo-code:

def get_pit_snapshot(financials, t):
    x = financials[
        financials["effective_date"] <= t
    ].copy()

    x = (
        x.sort_values("effective_date")
         .groupby(
             ["ticker", "period_end", "metric"],
             as_index=False
         )
         .tail(1)
    )

    return x

Backtest chạy ngày:

T = pd.Timestamp("2026-03-15")

thì gọi:

snapshot = get_pit_snapshot(financials, T)

Mọi thứ công bố ngày 16/3 trở đi hoàn toàn không tồn tại đối với model.

Cũng cần để ý một vấn đề khác: publication lag giữa các doanh nghiệp không giống nhau. Nếu ngày 30/4 rebalance portfolio, công ty A đã công bố Q1 nhưng công ty B chưa công bố, không thể lấy Q1 của A và Q1 tương lai của B chỉ để dataset “đồng đều”. Tại thời điểm đó strategy phải chấp nhận A có dữ liệu Q1 còn B vẫn đang ở báo cáo cũ. Đây mới là trạng thái thông tin mà thị trường thực sự nhìn thấy.

Một cách đơn giản hơn khi không có dataset PIT chuẩn là áp dụng conservative lag. Ví dụ cố tình delay dữ liệu quarterly thêm 45 hoặc 60 ngày sau period end. Cách này không chính xác bằng ngày công bố thực tế nhưng thường an toàn hơn việc gắn báo cáo ngay tại ngày kết thúc kỳ. Tuy nhiên khi đã có actual announcement/publication date thì nên dùng ngày thực tế thay vì một lag cố định, vì tốc độ công bố giữa doanh nghiệp và giữa các kỳ khác nhau.

PIT cũng phải đi cùng survivorship-bias-free universe. Nếu backtest năm 2018 nhưng universe lại lấy toàn bộ cổ phiếu đang niêm yết năm 2026, strategy đã loại khỏi lịch sử những doanh nghiệp từng phá sản, hủy niêm yết hoặc biến mất. Một backtest fundamentals nghiêm túc vì vậy thường phải giải quyết đồng thời ba trục thời gian:

Giá nào tồn tại tại T?
Cổ phiếu nào tồn tại tại T?
Thông tin tài chính nào đã được công bố tại T?

Chỉ khi cả ba câu trả lời đều đúng, portfolio tại ngày T mới thực sự có thể được tái tạo.

Một test rất hữu ích trước khi chạy strategy là chọn ngẫu nhiên vài ngày lịch sử rồi tự hỏi:

test_date = "2022-05-10"
ticker = "ABC"

Sau đó in toàn bộ fundamentals model đang nhìn thấy:

snapshot = get_pit_snapshot(
    financials,
    pd.Timestamp(test_date)
)

print(
    snapshot[snapshot["ticker"] == ticker]
)

Nếu xuất hiện báo cáo được công bố sau 2022-05-10, pipeline có leakage.

Đây thường là test đáng làm trước khi tối ưu bất kỳ factor nào.

Point-in-Time Data vì vậy không phải một kỹ thuật nâng cao chỉ dành cho quỹ lớn. Nó là điều kiện tối thiểu khi muốn backtest dữ liệu báo cáo tài chính một cách có ý nghĩa. Một strategy dùng MA20 nhưng sai vài basis point transaction cost có thể vẫn cho ta một số thông tin. Một strategy dùng ROE, EPS hay Revenue Growth từ tương lai thì kết quả về cơ bản đang mô phỏng một nhà đầu tư có khả năng biết trước báo cáo tài chính.

Với price data, câu hỏi thường là “giá lúc đó là bao nhiêu?”. Với fundamental data phải thêm một câu khó hơn: “tại ngày đó, thị trường đã biết con số này chưa?”

Nếu dataset không trả lời được câu thứ hai, chưa nên tin backtest.