Giá điều chỉnh cổ phiếu: cách tính bằng Python cho backtest

· · Backtest & giao dịch thuật toán

Giá điều chỉnh cổ phiếu: cách tính bằng Python cho backtest

> Tóm tắt nhanh: Giá điều chỉnh là chuỗi giá lịch sử đã được nhân với một hệ số để loại bỏ các cú giảm giá “giả” do chia cổ tức, thưởng cổ phiếu, phát hành quyền mua hoặc tách cổ phiếu. Với mỗi sự kiện, bạn lấy giá tham chiếu ngày giao dịch không hưởng quyền (GDKHQ) chia cho giá đóng cửa phiên liền trước để ra hệ số, rồi nhân lũy kế các hệ số đó cho mọi phiên trước ngày GDKHQ. Backtest trên giá chưa điều chỉnh có thể cho tín hiệu, drawdown và lợi nhuận sai lệch.

Người mới backtest cổ phiếu HOSE/HNX thường tải giá đóng cửa về rồi chạy chiến lược ngay. Vấn đề là mỗi lần doanh nghiệp chia cổ tức bằng cổ phiếu hay phát hành thêm, giá thô “sập” một mảng trong một phiên dù tài sản của cổ đông gần như không đổi. Bài này giải thích sở giao dịch điều chỉnh giá tham chiếu ra sao, đưa công thức cho từng loại sự kiện và một đoạn Python ngắn để bạn tự tính và tự kiểm tra dữ liệu.

Giá điều chỉnh cổ phiếu là gì và vì sao backtest cần nó?

Giá điều chỉnh (adjusted price) là giá quá khứ đã được quy đổi về cùng “đơn vị” với giá hiện tại, sao cho thay đổi giá giữa hai phiên chỉ phản ánh biến động thị trường, không phản ánh sự kiện quyền.

Ví dụ: bạn có 100 cổ phiếu giá 30.000 đồng. Doanh nghiệp chia cổ tức bằng cổ phiếu tỷ lệ 2:1 (50%). Theo cách tính được Tuổi Trẻ dẫn giải, giá tham chiếu ngày GDKHQ là 30.000/(1 + 1/2) = 20.000 đồng. Bạn sẽ có 150 cổ phiếu, tổng giá trị vẫn 3.000.000 đồng. Nhưng trên biểu đồ giá thô, đó là một phiên giảm 33%.

Nếu chiến lược của bạn dùng đường trung bình, stop-loss hay breakout, phiên “giảm 33%” này sẽ kích hoạt tín hiệu bán giả, làm drawdown phình to và kéo lợi nhuận xuống. Giá điều chỉnh khắc phục bằng cách nhân toàn bộ giá trước ngày GDKHQ với hệ số 20.000/30.000 ≈ 0,667, để chuỗi giá liền mạch.

Giá tham chiếu ngày giao dịch không hưởng quyền được sở điều chỉnh thế nào?

Sở giao dịch lấy giá đóng cửa phiên gần nhất rồi trừ đi giá trị quyền mà người mua ngày GDKHQ không còn được hưởng. Giá điều chỉnh trong backtest thực chất là tái tạo lại phép tính này.

Về khung pháp lý, Thông tư 120/2020/TT-BTC (hiệu lực từ 15/02/2021, thay thế Thông tư 203/2015/TT-BTC) định nghĩa giá tham chiếu là “mức giá do Sở giao dịch chứng khoán xác định” làm cơ sở tính giá trần, giá sàn. Công thức cụ thể nằm trong quy chế giao dịch của từng sở. Bản Quy chế giao dịch HOSE được đăng lại trên Studocu nêu nguyên tắc: “lấy giá đóng cửa của ngày giao dịch gần nhất điều chỉnh theo giá trị cổ tức được nhận hoặc giá trị các quyền kèm theo”. Với tách/gộp cổ phiếu, giá tham chiếu là giá đóng cửa trước ngày tách/gộp điều chỉnh theo tỷ lệ tách/gộp. Lưu ý: đây là bản đăng lại, không phải văn bản gốc trên hsx.vn.

Những điểm cần nắm khi xử lý dữ liệu:

Các công thức dưới đây được các công ty chứng khoán và báo chí mô tả; chúng tôi chưa đối chiếu được với văn bản gốc trên hsx.vn hay hnx.vn, nên bạn nên so kết quả với giá tham chiếu thực tế của sở.

Công thức điều chỉnh cho từng loại sự kiện là gì?

Mọi sự kiện đều quy về một hệ số k = P'/P, trong đó P là giá đóng cửa phiên liền trước ngày GDKHQ và P' là giá tham chiếu ngày GDKHQ.

| Sự kiện | Giá tham chiếu P' | Hệ số k | Ví dụ có nguồn | |---|---|---|---| | Cổ tức tiền C đồng/cp | P − C | (P − C)/P | MBB: 14,65 − 0,6 = 14,05 nghìn đồng | | Cổ tức cổ phiếu / thưởng tỷ lệ β | P/(1 + β) | 1/(1 + β) | 30.000/1,5 = 20.000 đồng | | Phát hành thêm (quyền mua) giá P_a, tỷ lệ r cp mới/cp cũ | (P + P_a × r)/(1 + r) | P'/P | Xem giải thích bên dưới | | Tách cổ phiếu 1 thành s | P/s | 1/s | Theo tỷ lệ tách |

Giải thích từng dòng:

  1. Cổ tức tiền: công thức P' = P − C. Tỷ lệ cổ tức tiền thường tính trên mệnh giá 10.000 đồng, nên 6% tương đương 600 đồng, như ví dụ MBB. PPC trả 4,5% tương ứng 450 đồng/cp.
  2. Cổ tức bằng cổ phiếu và cổ phiếu thưởng: P' = P/(1 + tỷ lệ). Ví dụ DSH có ngày GDKHQ 28/10/2026, cổ tức cổ phiếu tỷ lệ 12% (100 cp nhận 12 cp), nên giá tham chiếu = giá đóng cửa phiên trước / 1,12.
  3. Phát hành thêm cho cổ đông hiện hữu: CafeF nêu P' = (P × N_cũ + P_a × N_mới)/(N_cũ + N_mới). Chia cả tử và mẫu cho N_cũ, với r = N_mới/N_cũ, ta được dạng (P + P_a × r)/(1 + r) dễ code hơn.
  4. Tách/gộp: điều chỉnh theo tỷ lệ tách/gộp, như nguyên tắc trong quy chế HOSE nêu ở trên.

Khi nhiều sự kiện rơi vào cùng một ngày (ký hiệu 03, 05, 06, 07), chúng tôi chưa xác minh được công thức hợp nhất chính thức. Cách an toàn nhất là lấy thẳng giá tham chiếu sở công bố cho ngày đó.

Bước 1: Chuẩn bị chuỗi giá thô và bảng sự kiện quyền

Bạn cần hai bảng: giá OHLCV thô theo ngày và bảng sự kiện gồm ngày GDKHQ, loại sự kiện, tỷ lệ và giá phát hành.

Với bảng giá, xem các nguồn trong bài dữ liệu chứng khoán miễn phí cho backtest. Một lưu ý quan trọng: nhiều nguồn đã trả về giá điều chỉnh sẵn. Tài liệu vnstock_data nói dữ liệu lịch sử “đã được điều chỉnh kỹ thuật (adjusted price)” nhưng không nêu phương pháp. Bảng trade_history() của vnstock có cả cột close, reference_price và các cột *_adjusted. Simplize ghi giá lịch sử đã điều chỉnh cổ tức và chia tách. Vì vậy bước đầu tiên là xác định bạn đang cầm giá thô hay giá đã điều chỉnh, tránh điều chỉnh hai lần.

Nếu nguồn có cột reference_price, bạn có lối tắt: hệ số k = giá tham chiếu ngày GDKHQ / giá đóng cửa phiên trước, không cần tự tính theo công thức.

Bước 2: Tính hệ số điều chỉnh cho từng sự kiện bằng Python

Hàm dưới đây nhận giá đóng cửa phiên trước và một sự kiện, trả về hệ số k. Nếu bạn chưa quen thao tác DataFrame, đọc trước bài pandas cho chứng khoán.

import pandas as pd

# prices: index là ngày, cột open, high, low, close, volume (giá THÔ)
prices = pd.read_csv('gia_tho.csv', parse_dates=['date']).set_index('date').sort_index()

# Bảng sự kiện minh họa (tự nhập từ thông báo của sở/doanh nghiệp)
events = pd.DataFrame([
    {'ex_date': '2026-10-28', 'type': 'stock', 'ratio': 0.12},
    {'ex_date': '2026-11-11', 'type': 'cash', 'cash': 450},
])
events['ex_date'] = pd.to_datetime(events['ex_date'])

def ex_factor(p, ev):
    t = ev['type']
    if t == 'cash':
        ref = p - ev['cash']
    elif t in ('stock', 'bonus'):
        ref = p / (1 + ev['ratio'])
    elif t == 'rights':
        ref = (p + ev['price'] * ev['ratio']) / (1 + ev['ratio'])
    elif t == 'split':
        ref = p / ev['ratio']  # ratio = số cp mới cho 1 cp cũ
    else:
        raise ValueError(t)
    return ref / p

factors = []
for _, ev in events.iterrows():
    prev_close = prices.loc[prices.index < ev['ex_date'], 'close'].iloc[-1]
    factors.append(ex_factor(prev_close, ev))
events['factor'] = factors

Hai sự kiện trong bảng chỉ để minh họa cấu trúc; bạn thay bằng sự kiện thật của mã đang backtest. Nhớ bỏ qua các trường hợp sở không điều chỉnh (cổ phiếu quỹ, cổ tức tiền lớn hơn hoặc bằng giá đóng cửa), nếu không bạn sẽ tạo ra cú nhảy giả theo chiều ngược lại.

Bước 3: Nhân hệ số lũy kế ngược về quá khứ và kiểm tra kết quả

Mỗi phiên trước ngày GDKHQ được nhân với tích của tất cả hệ số có ngày GDKHQ sau phiên đó; giá từ ngày GDKHQ mới nhất trở đi giữ nguyên.

adj = pd.Series(1.0, index=prices.index)
for _, ev in events.iterrows():
    adj.loc[adj.index < ev['ex_date']] *= ev['factor']

for col in ['open', 'high', 'low', 'close']:
    prices[col + '_adj'] = prices[col] * adj
prices['volume_adj'] = prices['volume'] / adj  # tùy chọn

# Kiểm tra: phiên giảm quá 7% trên giá thô là nghi vấn sự kiện quyền
r_raw = prices['close'].pct_change()
r_adj = prices['close_adj'].pct_change()
check = pd.DataFrame({'raw': r_raw, 'adj': r_adj})
print(check[r_raw < -0.07])

Cách đọc kết quả kiểm tra:

  • Ngày GDKHQ có raw giảm mạnh nhưng adj chỉ dao động bình thường: điều chỉnh đúng.
  • Có phiên raw giảm quá 7% trên HOSE mà không nằm trong bảng sự kiện: nhiều khả năng bạn thiếu một sự kiện quyền (hoặc đó là ngày có biên độ đặc biệt).
  • adj tại ngày GDKHQ tăng vọt: có thể bạn đã điều chỉnh dữ liệu vốn đã được điều chỉnh sẵn.

Vì hệ số nhân ngược về quá khứ, mỗi khi có sự kiện mới, toàn bộ lịch sử giá điều chỉnh sẽ thay đổi. Hãy lưu giá thô và bảng sự kiện, rồi tính lại giá điều chỉnh mỗi lần cập nhật dữ liệu.

Lợi nhuận backtest trên giá thô và giá điều chỉnh khác nhau thế nào?

Giá thô làm lợi nhuận và rủi ro bị đánh giá sai theo hướng bi quan vào đúng ngày GDKHQ, trong khi giá điều chỉnh phản ánh giá trị thực của vị thế. Dùng lại ví dụ cổ tức cổ phiếu 50% ở trên:

| Chỉ tiêu | Giá thô | Giá điều chỉnh | |---|---|---| | Giá trước ngày GDKHQ | 30.000 | 20.000 (đã nhân 0,667) | | Giá ngày GDKHQ (đứng giá tham chiếu) | 20.000 | 20.000 | | Lợi nhuận ghi nhận trong phiên | −33% | 0% | | Giá trị thực của 100 cp ban đầu | 3.000.000 đ | 3.000.000 đ (150 cp × 20.000) |

Hệ quả trên chiến lược:

  • Tín hiệu: đường MA ngắn cắt xuống MA dài, giá phá đáy hay chạm stop-loss chỉ vì sự kiện quyền.
  • Drawdown: max drawdown bị cộng thêm cú giảm giả, làm chiến lược trông rủi ro hơn thực tế.
  • Lợi nhuận buy-and-hold: bị đánh giá thấp vì bỏ qua cổ phiếu và tiền nhận được.

Ngược lại, giá điều chỉnh cũng có giới hạn: cổ phiếu mới thường về tài khoản khoảng 30–60 ngày sau và cổ tức tiền được thanh toán sau ngày GDKHQ (PPC dự kiến 30/11/2026), nên trong thực tế bạn không thể bán ngay phần đó. Nếu chiến lược giao dịch tần suất cao quanh ngày GDKHQ, nên mô phỏng cả dòng tiền và cổ phiếu chờ về. Phần khung backtest đầy đủ có trong bài backtest chiến lược bằng Python.

Checklist kiểm tra dữ liệu giá trước khi backtest

  • [ ] Xác định nguồn trả giá thô hay giá điều chỉnh; đọc tài liệu và chạy thử một mã có cổ tức tiền để xem giá có bị trừ không.
  • [ ] Không điều chỉnh hai lần trên dữ liệu đã được điều chỉnh sẵn.
  • [ ] Bảng sự kiện đủ cổ tức tiền, cổ tức cổ phiếu, thưởng, quyền mua, tách/gộp.
  • [ ] Loại trừ các trường hợp sở không điều chỉnh (cổ phiếu quỹ, cổ tức tiền lớn hơn hoặc bằng giá đóng cửa).
  • [ ] Dùng giá đóng cửa phiên liền trước ngày GDKHQ, không phải ngày đăng ký cuối cùng.
  • [ ] Với sự kiện cùng ngày, đối chiếu với giá tham chiếu sở công bố.
  • [ ] Quét các phiên giảm vượt biên độ thông thường để tìm sự kiện bị thiếu.
  • [ ] Tín hiệu tính trên giá điều chỉnh; khối lượng lệnh và giá khớp mô phỏng cần quy về giá thô nếu bạn mô phỏng bước giá hoặc lô.

Câu hỏi thường gặp

Giá điều chỉnh có dùng để đặt lệnh được không?

Không. Giá điều chỉnh là giá quy đổi để phân tích và backtest, giá quá khứ của nó không phải giá từng khớp trên sàn. Khi đặt lệnh hay mô phỏng bước giá, biên độ trần/sàn, bạn phải dùng giá thô.

Có cần điều chỉnh cho cổ tức tiền không?

Nếu muốn lợi nhuận backtest phản ánh cả phần cổ tức, nên điều chỉnh, vì theo nguyên tắc chung của Quy chế HOSE, giá tham chiếu được điều chỉnh theo giá trị cổ tức được nhận (chúng tôi chưa xác minh mốc thời gian HOSE bắt đầu áp dụng cho cổ tức tiền). Một số nhà cung cấp chỉ điều chỉnh chia tách và cổ tức cổ phiếu, nên hãy kiểm tra trước. Trường hợp cổ tức tiền lớn hơn hoặc bằng giá đóng cửa thuộc nhóm ngoại lệ theo FPTS.

Thưởng bằng cổ phiếu quỹ có làm thay đổi giá điều chỉnh không?

Theo FPTS (với HOSE) và CafeF (với HNX), cổ phiếu quỹ nằm trong nhóm không điều chỉnh giá tham chiếu, đồng thời ngày GDKHQ có biên độ rộng hơn (±20% ở HOSE, ±30% ở HNX). Việc HOSE không điều chỉnh được suy ra từ cấu trúc ngoại lệ của nguồn thứ cấp, nên hãy đối chiếu với giá tham chiếu thực tế của sở trước khi loại sự kiện này khỏi bảng hệ số.

Dữ liệu miễn phí ở Việt Nam đã có giá điều chỉnh chưa?

Một số nguồn như vnstock và Simplize cho biết có giá điều chỉnh, nhưng tài liệu không nêu rõ phương pháp và có chỗ mâu thuẫn giữa cột close và close_price_adjusted. Cách chắc chắn là tải một mã từng chia cổ tức, so giá phiên trước và ngày GDKHQ, rồi so với công thức trong bài.

Vì sao giá điều chỉnh quá khứ thay đổi sau mỗi lần cập nhật dữ liệu?

Vì hệ số được nhân ngược về quá khứ: mỗi sự kiện mới làm toàn bộ giá trước nó thay đổi theo. Lợi nhuận phần trăm giữa các phiên vẫn giữ nguyên, chỉ mức giá tuyệt đối thay đổi. Do đó nên dùng tín hiệu dựa trên tỷ lệ thay vì ngưỡng giá cố định.

Khi chuỗi giá đã sạch, bước tiếp theo là đưa chiến lược vào môi trường backtest có dữ liệu HOSE/HNX để so kết quả với phiên bản bạn tự viết. Bạn có thể tạo và backtest bot trên QuantVN, rồi đối chiếu tín hiệu quanh các ngày GDKHQ để chắc rằng chiến lược không bị đánh lừa bởi các cú giảm giả.