DỰ ĐOÁN CỔ PHIẾU TĂNG HAY GIẢM BẰNG SCIKIT-LEARN: BÀI TOÁN CLASSIFICATION ĐƠN GIẢN NHƯNG RẤT DỄ LÀM SAI
DỰ ĐOÁN CỔ PHIẾU TĂNG HAY GIẢM BẰNG SCIKIT-LEARN
Khi bắt đầu tìm hiểu Machine Learning trong Quant Trading, nhiều bạn thường nghĩ ngay đến những mô hình phức tạp như LSTM, XGBoost hay Deep Learning. Nhưng thực tế, trước khi đi đến những mô hình đó, chúng ta hoàn toàn có thể bắt đầu bằng một bài toán khá cơ bản với scikit-learn: dự đoán liệu một cổ phiếu có tăng giá trong những phiên giao dịch tiếp theo hay không.

Đây là bài toán Classification (phân loại), một trong những dạng bài toán phổ biến nhất của Machine Learning. Thay vì cố gắng dự báo chính xác giá FPT sẽ là bao nhiêu vào tuần sau, chúng ta chỉ cần trả lời một câu hỏi đơn giản hơn: Dựa trên những thông tin đang có, liệu cổ phiếu này có khả năng tăng vượt một mức nhất định trong 5 phiên tới không?
Nghe có vẻ đơn giản, bởi chỉ cần chuẩn bị dữ liệu, tạo nhãn, huấn luyện mô hình rồi xem kết quả dự đoán. Tuy nhiên, khi áp dụng vào dữ liệu tài chính, có khá nhiều vấn đề cần chú ý. Một mô hình đạt Accuracy 80% chưa chắc đã có giá trị giao dịch, còn một mô hình có Accuracy chỉ 55% đôi khi vẫn đáng để nghiên cứu tiếp, tùy vào mục tiêu dự báo, phân phối lợi nhuận và cách sử dụng tín hiệu.
Vậy chúng ta có thể xây dựng một bài toán Classification thực tế với Python và scikit-learn như thế nào?
PHẦN 1. BIẾN DỮ LIỆU CHỨNG KHOÁN THÀNH BÀI TOÁN CLASSIFICATION
Giả sử chúng ta muốn nghiên cứu một mô hình dự báo biến động của cổ phiếu FPT, sử dụng dữ liệu giá và khối lượng giao dịch lịch sử từ năm 2018 đến nay. Mục tiêu là sau khi kết thúc phiên giao dịch hôm nay, mô hình sẽ đưa ra một dự báo về khả năng cổ phiếu tăng trên 2% trong 5 phiên tiếp theo.
Khác với Regression, nơi mô hình dự báo một giá trị liên tục như lợi suất 1,5% hay -2,3%, Classification sẽ phân loại kết quả thành các nhóm đã được xác định trước. Ở bài toán này, chúng ta có thể bắt đầu với Binary Classification, tức phân loại nhị phân gồm hai nhóm: nhãn 1 nếu lợi suất trong 5 phiên tới vượt 2%, nhãn 0 nếu không vượt ngưỡng đó.
Điều cần hiểu là nhãn 0 không có nghĩa cổ phiếu chắc chắn giảm giá. Nếu cổ phiếu tăng 1%, đi ngang hoặc giảm 3%, cả ba trường hợp đều thuộc nhóm 0 theo định nghĩa trên. Đây là lý do việc lựa chọn nhãn (target) rất quan trọng: mô hình chỉ học cách trả lời câu hỏi mà chúng ta đã đặt ra, chứ không tự hiểu thế nào là một cơ hội đầu tư tốt.
Đối với dữ liệu chứng khoán, chúng ta thường có những cột cơ bản như ngày giao dịch, giá mở cửa, cao nhất, thấp nhất, đóng cửa và khối lượng. Từ những dữ liệu này, có thể xây dựng thêm các biến đầu vào, thường được gọi là Features.
Chẳng hạn, một bộ Features ban đầu có thể gồm lợi suất 1 phiên, lợi suất 5 phiên, momentum 20 phiên, khoảng cách giữa giá hiện tại với MA20, độ biến động lợi suất trong 20 phiên gần nhất và tỷ lệ khối lượng hiện tại so với khối lượng bình quân 20 phiên. Nếu muốn bổ sung yếu tố thị trường, có thể thêm lợi suất VN-Index, biến động thị trường hoặc sức mạnh tương đối của cổ phiếu so với chỉ số chung.
Giả sử sau phiên giao dịch hôm nay, FPT có momentum 20 phiên là +6%, giá nằm trên MA20 khoảng 3%, khối lượng cao hơn trung bình 20 phiên và thị trường chung đang có xu hướng tích cực. Chúng ta muốn mô hình học từ hàng nghìn quan sát trong quá khứ để đánh giá xem các điều kiện tương tự thường dẫn đến kết quả gì trong những phiên tiếp theo.
Ở đây có một điểm rất dễ gây sai lệch: tất cả Features phải được tính bằng những thông tin đã thực sự có tại thời điểm dự báo. Nếu tạo tín hiệu sau khi phiên hôm nay đóng cửa, chúng ta được phép sử dụng giá đóng cửa và khối lượng của phiên hôm nay. Nhưng tuyệt đối không được sử dụng giá, khối lượng hoặc bất kỳ thông tin nào của ngày mai để tính Features cho hôm nay.
Lỗi này được gọi là Data Leakage, tức dữ liệu tương lai hoặc thông tin không hợp lệ lọt vào quá trình xây dựng mô hình. Trong Quant Trading, đây là một trong những nguyên nhân phổ biến khiến kết quả backtest rất đẹp nhưng mô hình lại hoạt động kém khi sử dụng thực tế.
Ví dụ, với pandas, chúng ta có thể dùng pct_change(5) để tính momentum 5 phiên và rolling(20).std() để tính độ biến động. Còn shift(-5) có thể được dùng để lấy giá tương lai phục vụ tạo nhãn. Tuy nhiên, thông tin lấy bằng shift(-5) chỉ được phép xuất hiện ở phần target, không được đưa vào Features.
Một vấn đề khác là cách xác định lợi suất tương lai. Nếu tạo nhãn bằng giá đóng cửa hôm nay và giá đóng cửa sau 5 phiên, thì mô hình đang dự báo biến động close-to-close. Nhưng khi giao dịch thực tế, nếu tín hiệu chỉ xuất hiện sau khi đóng cửa hôm nay, chúng ta thường phải đặt lệnh từ phiên tiếp theo. Khi đó, giá mua thực tế có thể khác giá đóng cửa hôm nay. Để mô hình phục vụ một chiến lược giao dịch cụ thể, nên xác định nhãn phù hợp với thời điểm có thể thực hiện lệnh, chẳng hạn dựa trên giá mở cửa phiên kế tiếp và giá thoát vị thế theo quy tắc đã định.
Sau khi xử lý xong, chúng ta có một bảng dữ liệu mà mỗi dòng tương ứng với một ngày giao dịch, gồm các Features quan sát được tại thời điểm đó và một nhãn 0 hoặc 1 được xác định từ diễn biến tương lai.
Đây chính là bộ dữ liệu để huấn luyện mô hình Classification.
Điều thú vị là phần tạo dữ liệu này thường quan trọng hơn cả việc lựa chọn thuật toán. Nếu Features không hợp lý hoặc target không phản ánh đúng mục tiêu, việc thay Logistic Regression bằng Random Forest hay một mô hình phức tạp hơn chưa chắc giải quyết được vấn đề.
PHẦN 2. HUẤN LUYỆN MÔ HÌNH VỚI SCIKIT-LEARN: KHÔNG PHẢI CỨ ACCURACY CAO LÀ TỐT
Sau khi có dữ liệu, chúng ta bắt đầu sử dụng scikit-learn. Đây là thư viện Machine Learning của Python với nhiều công cụ phục vụ Classification, Regression, Clustering, xử lý dữ liệu, lựa chọn mô hình và đánh giá kết quả.
Với bài toán đầu tiên, mình nghĩ nên bắt đầu bằng Logistic Regression. Mặc dù có chữ Regression trong tên, đây là một thuật toán thường dùng cho Classification, đặc biệt với bài toán phân loại nhị phân. Mô hình có thể tạo ra điểm dự báo dưới dạng xác suất ước lượng cho một sự kiện, ví dụ khả năng nhãn bằng 1. Nó tương đối đơn giản, huấn luyện nhanh và thuận tiện để làm mô hình tham chiếu trước khi thử những thuật toán phức tạp hơn.
Sau đó, có thể so sánh với Random Forest hoặc HistGradientBoostingClassifier để xem các mô hình có khả năng học quan hệ phi tuyến có cải thiện kết quả hay không.
Nhưng trước khi huấn luyện, chúng ta cần chia dữ liệu thành các tập phù hợp. Với dữ liệu tài chính, không nên tùy tiện sử dụng train_test_split() với shuffle=True như trong nhiều ví dụ Classification cơ bản. Việc trộn ngẫu nhiên các ngày giao dịch có thể khiến mô hình được học từ dữ liệu ở tương lai để dự báo dữ liệu trong quá khứ, dẫn đến kết quả đánh giá thiếu thực tế.
Một cách đơn giản hơn là chia dữ liệu theo thời gian: dùng giai đoạn 2018–2022 để huấn luyện, năm 2023 để điều chỉnh mô hình và lựa chọn ngưỡng dự báo, sau đó sử dụng dữ liệu từ 2024 đến thời điểm nghiên cứu làm tập kiểm tra cuối cùng. Đây chỉ là cách chia minh họa; khoảng thời gian thực tế phải phù hợp với số lượng dữ liệu và mục tiêu nghiên cứu.
Ngoài ra, do target của chúng ta sử dụng lợi suất 5 phiên trong tương lai, các quan sát gần ranh giới giữa hai tập có thể dùng những khoảng thời gian kết quả bị chồng lấn. Vì vậy, cần loại hoặc tách những quan sát có khoảng xác định nhãn vượt qua ranh giới kiểm định. Scikit-learn có công cụ TimeSeriesSplit với tham số gap để hỗ trợ chừa khoảng cách giữa tập huấn luyện và tập kiểm tra. Với dữ liệu một cổ phiếu, mỗi dòng tương ứng một phiên giao dịch, có thể bắt đầu nghiên cứu việc chừa khoảng cách 5 quan sát cho target 5 phiên. Nếu dữ liệu gồm nhiều cổ phiếu trong cùng một ngày, cần chia và chừa khoảng cách theo ngày giao dịch, vì 5 dòng dữ liệu không còn tương đương 5 phiên.
Sau khi chia dữ liệu, chúng ta có thể sử dụng StandardScaler để chuẩn hóa các Features và Pipeline để kết hợp bước xử lý dữ liệu với mô hình Logistic Regression. Đây là một thói quen tốt, vì các tham số chuẩn hóa chỉ nên được học từ dữ liệu huấn luyện, sau đó áp dụng cho dữ liệu validation và test. Nếu chuẩn hóa trên toàn bộ dữ liệu trước khi chia tập, thông tin từ giai đoạn kiểm tra có thể ảnh hưởng đến quá trình huấn luyện.
Với scikit-learn, một khung chương trình sau khi đã chuẩn bị xong dữ liệu có thể đơn giản như sau:
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, roc_auc_score
model = make_pipeline(
StandardScaler(),
LogisticRegression(max_iter=1000)
)
model.fit(X_train, y_train)
proba = model.predict_proba(X_valid)[:, 1]
pred = (proba >= 0.60).astype(int)
print(classification_report(y_valid, pred))
print(roc_auc_score(y_valid, proba))
Đoạn code này sử dụng các tập dữ liệu đã được chuẩn bị và tách đúng theo thời gian. Mô hình sẽ học từ X_train, y_train, sau đó đưa ra xác suất ước lượng trên X_valid. Ngưỡng 0,60 ở đây chỉ là một ví dụ để minh họa cách chuyển điểm dự báo thành nhãn, chưa phải ngưỡng tối ưu. Trong nghiên cứu thực tế, cần chọn ngưỡng dựa trên tập validation, kiểm tra tính ổn định và không điều chỉnh lại theo kết quả của tập test cuối cùng.
Vậy làm thế nào để biết mô hình hoạt động tốt?
Nhiều bạn sẽ nhìn ngay vào Accuracy. Nhưng trong Classification, đặc biệt là dữ liệu tài chính, chỉ số này rất dễ gây hiểu nhầm.
Giả sử trong 100 quan sát kiểm tra, chỉ có 25 trường hợp cổ phiếu tăng trên 2% trong 5 phiên tiếp theo, còn 75 trường hợp không đạt điều kiện. Một mô hình luôn dự báo nhãn 0 sẽ đạt Accuracy 75%, mặc dù hoàn toàn không nhận diện được trường hợp nào thuộc nhóm cổ phiếu tăng mạnh.
Giờ giả sử một mô hình khác dự báo 20 trường hợp thuộc nhóm 1, trong đó có 12 trường hợp đúng. Khi đó, Precision của nhóm 1 là 60%, nghĩa là trong số 20 lần mô hình đưa ra tín hiệu tích cực, có 12 lần sự kiện mà chúng ta định nghĩa thực sự xảy ra. Recall là 48%, bởi mô hình nhận diện được 12 trong tổng số 25 trường hợp tích cực thực tế. Với các giả định này, Accuracy của mô hình là 79%.
Như vậy, Accuracy chỉ cải thiện từ mức tham chiếu 75% lên 79%, nhưng khả năng nhận diện nhóm tích cực đã khác đáng kể. Nếu mục tiêu là tìm một số ít cơ hội có xác suất đạt điều kiện cao, Precision có thể đáng quan tâm hơn việc cố dự báo đúng toàn bộ các ngày giao dịch.
Tất nhiên, những con số trên hoàn toàn là ví dụ minh họa, không phải kết quả của một mô hình đã được kiểm chứng trên FPT.
Ngoài Accuracy, Precision và Recall, chúng ta còn có thể đánh giá ROC-AUC, Average Precision, F1-score và Confusion Matrix. ROC-AUC giúp đánh giá khả năng xếp hạng các quan sát thuộc nhóm 1 cao hơn nhóm 0 của mô hình. Precision-Recall đặc biệt hữu ích khi số trường hợp thuộc nhóm tích cực khá ít so với phần còn lại.
Một công cụ nữa rất nên sử dụng là DummyClassifier. Đây là mô hình tham chiếu đơn giản, chẳng hạn luôn dự báo lớp xuất hiện nhiều nhất. Nếu một mô hình Machine Learning phức tạp không vượt qua được những cách dự báo cơ bản, chúng ta cần xem lại liệu nó đã học được thông tin có ích hay chưa.
Ngoài ra, xác suất do mô hình đưa ra cũng cần được hiểu đúng. Nếu predict_proba() trả về 0,70, điều đó không có nghĩa cổ phiếu chắc chắn có 70% cơ hội tăng hay mô hình đã đúng 70% trong thực tế. Đây là xác suất ước lượng theo mô hình và cần được kiểm tra độ hiệu chỉnh xác suất (calibration). Một mô hình được hiệu chỉnh tốt sẽ có xu hướng cho tỷ lệ sự kiện xảy ra tương ứng với những mức xác suất dự báo qua nhiều quan sát.
Trong Quant Trading, việc dự báo đúng một nhãn mới chỉ là bước đầu tiên. Điều chúng ta thực sự muốn biết là mô hình có tạo ra tín hiệu đủ tốt để cải thiện quyết định đầu tư hay không.
PHẦN 3. TỪ CLASSIFICATION ĐẾN CHIẾN LƯỢC GIAO DỊCH: MÔ HÌNH DỰ ĐOÁN ĐÚNG ĐÃ ĐỦ ĐỂ KIẾM TIỀN?
Đây là phần mình nghĩ quan trọng nhất khi ứng dụng Machine Learning vào Quant. Một mô hình Classification có thể đạt Accuracy, Precision hoặc ROC-AUC khá tốt nhưng vẫn không tạo ra chiến lược giao dịch có lợi nhuận.
Hãy thử một ví dụ khác. Giả sử chúng ta xây dựng được mô hình chọn tín hiệu mua với tỷ lệ giao dịch có lãi là 60%. Nghe có vẻ hấp dẫn, bởi nhiều người thường cho rằng tỷ lệ thắng trên 50% là đã có lợi thế. Nhưng nếu mỗi giao dịch thắng trung bình 2%, còn mỗi giao dịch thua trung bình mất 4%, lợi nhuận kỳ vọng đơn giản trên mỗi giao dịch sẽ là:
60% × 2% − 40% × 4% = −0,4%.
Như vậy, mặc dù có đến 60% giao dịch thắng, chiến lược vẫn có kỳ vọng lợi nhuận âm trước chi phí giao dịch. Tất nhiên, tỷ lệ giao dịch có lãi trong ví dụ này là một thước đo của chiến lược, không đồng nhất với Precision của nhãn “tăng trên 2%” trong mô hình Classification đã xây dựng ở trên.
Điều đó cho thấy vì sao chúng ta cần tách biệt hai vấn đề: mô hình dự báo có tốt không và chiến lược sử dụng dự báo đó có hiệu quả không.
Khi đã có xác suất dự báo, chúng ta có thể thử một quy tắc đơn giản: chỉ xem xét mua khi xác suất mô hình đưa ra vượt một ngưỡng nhất định, chẳng hạn 0,65, đồng thời cổ phiếu đáp ứng điều kiện thanh khoản. Ngưỡng này cần được lựa chọn và đánh giá trên dữ liệu validation, thay vì thử hàng chục mức rồi chọn kết quả tốt nhất trên tập test.
Sau đó, xây dựng quy trình backtest với những giả định cụ thể về thời điểm mua, thời điểm bán, tỷ trọng phân bổ, phí giao dịch, thuế, trượt giá và khả năng khớp lệnh. Với thị trường Việt Nam, còn phải chú ý đến chu kỳ thanh toán T+2, biên độ giá và giới hạn thanh khoản, bởi đây đều là những yếu tố có thể làm thay đổi hiệu quả thực tế so với kết quả mô phỏng.
Giả sử mô hình phát hiện một tín hiệu có xác suất dự báo 0,72 sau khi phiên giao dịch kết thúc. Nếu quy tắc yêu cầu mua tại phiên tiếp theo, backtest phải sử dụng mức giá thực hiện phù hợp ở phiên tiếp theo chứ không thể mặc định đã mua được tại giá đóng cửa của ngày tạo tín hiệu. Tương tự, nếu cổ phiếu tăng trần và không có thanh khoản bán phù hợp, mô hình phải xét khả năng lệnh không khớp thay vì mặc nhiên ghi nhận một giao dịch thành công.
Khi đã mô phỏng giao dịch, chúng ta mới đánh giá chiến lược qua các chỉ tiêu như CAGR, Sharpe Ratio, Maximum Drawdown, turnover, mức lợi nhuận trung bình mỗi giao dịch và kết quả sau chi phí. Ngoài ra, cần so sánh với một chiến lược tham chiếu đơn giản, ví dụ mua và nắm giữ hoặc một quy tắc momentum không sử dụng Machine Learning.
Nếu một mô hình Logistic Regression tạo ra chiến lược không tốt hơn quy tắc đơn giản, việc chuyển sang Random Forest hoặc Gradient Boosting có thể đáng thử, nhưng cũng cần đặt câu hỏi liệu các Features hiện tại có thực sự chứa thông tin dự báo hay không.
Trong thực tế, bài toán còn khó hơn bởi dữ liệu tài chính liên tục thay đổi. Một mô hình được huấn luyện trên giai đoạn thị trường tăng mạnh có thể hoạt động kém khi thị trường bước sang giai đoạn giảm hoặc biến động cao. Mối quan hệ giữa momentum, thanh khoản, độ biến động và lợi suất tương lai không nhất thiết ổn định qua thời gian.
Vì vậy, thay vì chỉ chạy backtest một lần, chúng ta nên thử phương pháp walk-forward. Ví dụ, sử dụng một khoảng thời gian lịch sử để huấn luyện, dự báo trên giai đoạn tiếp theo, sau đó cập nhật mô hình bằng những dữ liệu đã có đầy đủ nhãn và tiếp tục kiểm tra trên giai đoạn mới. Toàn bộ quá trình cần tuân thủ thứ tự thời gian để tránh sử dụng thông tin tương lai.
Nếu kết quả tốt chỉ xuất hiện ở một giai đoạn hoặc biến mất hoàn toàn khi thay đổi nhẹ ngưỡng xác suất, thời gian nắm giữ hay nhóm cổ phiếu nghiên cứu, chúng ta cần thận trọng. Mô hình có thể đang khớp với đặc điểm ngẫu nhiên của dữ liệu quá khứ thay vì học được một tín hiệu đủ ổn định.
Một hướng mở rộng thực tế là thay vì chỉ nghiên cứu một mã FPT, chúng ta có thể xây dựng mô hình trên nhiều cổ phiếu thanh khoản tốt. Mỗi ngày, mô hình đưa ra xác suất của sự kiện mục tiêu cho từng mã, sau đó xếp hạng các cơ hội và lựa chọn một danh mục phù hợp. Tuy nhiên, khi chuyển sang dữ liệu nhiều cổ phiếu, cần đặc biệt chú ý đến cách chia dữ liệu theo ngày, dữ liệu doanh nghiệp, các cổ phiếu đã hủy niêm yết và tránh để thông tin tương lai xuất hiện trong tập nghiên cứu.
Ở giai đoạn này, Machine Learning không nhất thiết phải trở thành một hệ thống giao dịch tự động. Mô hình có thể chỉ đóng vai trò bộ lọc hoặc công cụ xếp hạng cổ phiếu, hỗ trợ nhà nghiên cứu lựa chọn những trường hợp đáng xem xét. Điều quan trọng là từng bước phải được kiểm thử và có thể giải thích được.
Nếu mới học scikit-learn, mình nghĩ một dự án Classification hoàn chỉnh nên có đầu ra khá cụ thể: một notebook Python xử lý dữ liệu cổ phiếu, bảng Features và Target, mô hình Logistic Regression, báo cáo Confusion Matrix và Precision-Recall, kết quả dự báo ngoài mẫu và một backtest đơn giản với chi phí giao dịch. Sau đó có thể so sánh thêm Random Forest, thử các nhóm Features khác nhau hoặc bổ sung dashboard để theo dõi dự báo.
Làm được một dự án như vậy sẽ giúp kết nối kiến thức Python, thống kê, Machine Learning và Quant Trading vào cùng một bài toán, thay vì chỉ biết cách gọi .fit() và .predict() mà chưa hiểu kết quả được sử dụng như thế nào.
Điều mình thấy thú vị ở scikit-learn là không cần bắt đầu bằng một hệ thống quá phức tạp. Chỉ với pandas, scikit-learn, một bộ dữ liệu được xử lý cẩn thận và một ý tưởng nghiên cứu rõ ràng, chúng ta đã có thể thử nghiệm khá nhiều câu hỏi thực tế.
Vấn đề lớn nhất trong Quant không phải làm sao để mô hình dự báo đúng thật nhiều lần, mà là làm sao biết những dự báo đó có đủ giá trị để đưa vào một quyết định giao dịch hay không.
