Trang chủBlogChiến Lược TradingNUMPY GIÚP TÍNH TOÁN DỮ LIỆU THỊ TRƯỜNG NHANH HƠN NHƯ THẾ NÀO?
NUMPY GIÚP TÍNH TOÁN DỮ LIỆU THỊ TRƯỜNG NHANH HƠN NHƯ THẾ NÀO?
Chiến Lược Trading

NUMPY GIÚP TÍNH TOÁN DỮ LIỆU THỊ TRƯỜNG NHANH HƠN NHƯ THẾ NÀO?

NUMPY

Ngọc Hải LươngNgọc Hải Lương
28/7/2026
10 phút phút đọc
54 lượt xem

Phần 1: Khi dữ liệu lên tới hàng triệu dòng, vòng lặp Python bắt đầu xin nghỉ phép

Một cổ phiếu có vài nghìn phiên dữ liệu ngày thì Excel vẫn xử lý khá nhẹ nhàng. Nhưng khi nghiên cứu 1.000 cổ phiếu, dữ liệu phút, nhiều tham số và hàng trăm lần backtest, số phép tính có thể tăng lên hàng triệu hoặc hàng trăm triệu. Lúc này, vấn đề không còn nằm ở công thức lợi suất khó tới đâu, mà nằm ở việc máy tính phải lặp lại phép tính đó bao nhiêu lần.

a3b49880-2ca2-480e-983a-676cda637ec8.png

Giả sử cần tính lợi suất hằng ngày của một chuỗi giá. Cách dễ hình dung nhất là dùng vòng lặp Python: lấy giá hôm nay chia giá hôm qua, trừ một rồi chuyển sang phiên tiếp theo. Cách này đúng, nhưng Python phải xử lý từng phần tử riêng biệt. Mỗi lần lặp đều có chi phí kiểm tra kiểu dữ liệu, truy cập phần tử và thực thi lệnh. Một vòng lặp vài nghìn lần không đáng kể, nhưng một triệu lần thì máy bắt đầu có thời gian suy ngẫm về cuộc đời.

NumPy xử lý vấn đề theo cách khác. Thay vì yêu cầu Python đi qua từng phần tử, toàn bộ chuỗi giá được lưu trong một mảng có cấu trúc đồng nhất. Phép tính được chuyển xuống các đoạn mã tối ưu ở tầng thấp hơn, thường được thực thi bằng C hoặc các thư viện số học chuyên dụng. Vì vậy, biểu thức:

returns = prices[1:] / prices[:-1] - 1

không phải là một vòng lặp bị thiếu. Vòng lặp vẫn tồn tại, nhưng nó chạy bên dưới NumPy thay vì chạy từng bước trong Python. Cách làm này được gọi là vectorization: mô tả phép tính trên cả mảng dữ liệu thay vì tự viết cách đi qua từng phần tử.

Ví dụ, nếu prices chứa một triệu mức giá, NumPy có thể tính toàn bộ lợi suất chỉ bằng một dòng. Trong khi đó, cách dùng vòng lặp phải liên tục thêm kết quả vào danh sách hoặc mảng mới. Khoảng chênh lệch tốc độ cụ thể phụ thuộc máy, phiên bản thư viện và phép tính, nên file code đi kèm không ghi sẵn một con số quảng cáo. Chương trình sẽ tự đo thời gian vòng lặp Python và NumPy trên chính máy đang chạy. Bộ phim này gọi là không tranh luận NumPy nhanh hơn bao nhiêu bằng cảm giác; cứ để time.perf_counter() VAR trực tiếp.

Điểm quan trọng hơn là NumPy không chỉ giúp code chạy nhanh. Nó còn giúp công thức gần với cách biểu diễn toán học hơn. Thay vì viết nhiều vòng lặp để tính lợi suất, đường vốn, drawdown hoặc tín hiệu, các phép tính được thể hiện thành những bước ngắn và dễ kiểm tra. Điều này đặc biệt hữu ích trong Quant, nơi một backtest sai đôi khi không tạo lỗi đỏ. Nó vẫn chạy, vẫn vẽ biểu đồ đẹp và chỉ âm thầm tính sai tài khoản.

Phần 2: Từ chuỗi giá đến một chiến lược MA20–MA100 bằng NumPy

Ví dụ thực hành đi kèm xây một quy trình nhỏ nhưng tương đối đầy đủ. Chương trình có thể đọc cột giá đóng cửa từ file CSV. Nếu chưa có dữ liệu thật, code sẽ tạo một chuỗi giá giả lập để người học chạy thử ngay. Sau đó NumPy được dùng để tính lợi suất, trung bình trượt, biến động 20 phiên, tín hiệu giao dịch, chi phí, đường tăng trưởng vốn, Sharpe Ratio và Maximum Drawdown.

Phần tính lợi suất là ví dụ vectorization cơ bản nhất:

import numpy as np
prices = np.array([50_000, 51_000, 49_500, 52_000], dtype=float)
returns = prices[1:] / prices[:-1] - 1
print(returns)

Kết quả cho biết lợi suất từ phiên trước sang phiên sau. Không cần vòng lặp, không cần tạo biến tạm cho từng ngày. Khi thay bốn mức giá bằng bốn triệu mức giá, cấu trúc code vẫn giữ nguyên.

Đường trung bình cũng có thể được tính bằng NumPy. Trong file mẫu, MA được tính bằng cumulative sum, tức tổng tích lũy. Thay vì mỗi ngày cộng lại toàn bộ 20 hoặc 100 phiên gần nhất, chương trình chỉ lấy hiệu giữa hai mức tổng tích lũy:

def rolling_mean(values, window):
    output = np.full(values.shape, np.nan)
    cumulative_sum = np.cumsum(
        np.insert(values, 0, 0.0)
    )
    output[window - 1:] = (
        cumulative_sum[window:]
        - cumulative_sum[:-window]
    ) / window
    return output

Ý tưởng rất đơn giản. Nếu tổng từ phiên đầu đến phiên 120 là 1.000 và tổng từ phiên đầu đến phiên 100 là 800, tổng của 20 phiên còn lại là 200. Không cần cộng lại 20 giá trị từ đầu. Với một đường MA, chênh lệch có thể chưa đáng kể. Khi phải tính nhiều cửa sổ trên hàng nghìn mã, lợi ích bắt đầu hiện rõ.

Chiến lược ví dụ sử dụng quy tắc:

  • Nắm giữ cổ phiếu khi MA20 lớn hơn MA100.
    Chuyển sang tiền mặt khi MA20 nhỏ hơn hoặc bằng MA100.
    Tín hiệu được dịch sang phiên kế tiếp.
    Mỗi lần thay đổi vị thế đều bị trừ chi phí.

  • Phần cốt lõi của backtest được viết như sau:

ma20 = rolling_mean(prices, 20)
ma100 = rolling_mean(prices, 100)

signal = (ma20 > ma100).astype(float)

signal[
    ~np.isfinite(ma20)
    | ~np.isfinite(ma100)
] = 0

position = np.roll(signal, 1)
position[0] = 0

returns = prices[1:] / prices[:-1] - 1

position = position[:-1]

turnover = np.abs(
    np.diff(
        np.insert(position, 0, 0.0)
    )
)

cost = 0.0015

strategy_returns = (
    position * returns
    - turnover * cost
)

equity_curve = np.cumprod(
    1 + strategy_returns
)

Dòng np.roll(signal, 1) là chi tiết nhỏ nhưng rất quan trọng. Tín hiệu được tạo sau khi biết giá của phiên hiện tại, nên không thể giả định đã nắm giữ từ trước khi tín hiệu tồn tại. Dịch tín hiệu một phiên giúp hạn chế look-ahead bias. Không làm bước này, chiến lược có thể sử dụng thông tin của ngày hôm nay để kiếm lợi nhuận ngay từ chính ngày hôm nay. Thị trường chưa phát hành gói quyền năng đó cho nhà đầu tư cá nhân.

Chi phí được tính thông qua turnover. Khi vị thế đổi từ 0 sang 1, chương trình ghi nhận một lần mua. Khi đổi từ 1 sang 0, chương trình ghi nhận một lần bán. Nếu chiến lược thay đổi trạng thái liên tục, tổng chi phí sẽ tăng theo. Đây là cách một chiến lược nhìn rất linh hoạt trước phí có thể chuyển sang hoạt động từ thiện cho công ty chứng khoán sau phí.

Maximum Drawdown được tính bằng vài phép toán trên mảng:

running_peak = np.maximum.accumulate(
    equity_curve
)

drawdown = (
    equity_curve / running_peak - 1
)

maximum_drawdown = drawdown.min()

np.maximum.accumulate() lưu lại mức cao nhất của đường vốn tại từng thời điểm. So sánh giá trị tài khoản hiện tại với đỉnh gần nhất sẽ cho ra drawdown. Không cần viết vòng lặp để hỏi từng phiên rằng “đỉnh tài khoản đến hôm nay là bao nhiêu?”. NumPy xử lý toàn bộ chuỗi trong một lần.

Biến động trượt 20 phiên trong file mẫu được tính bằng sliding_window_view. Hàm này tạo ra các cửa sổ dữ liệu mà không phải sao chép toàn bộ mảng nhiều lần:

windows = np.lib.stride_tricks\
    .sliding_window_view(
        returns,
        20
    )

volatility_20 = (
    windows.std(axis=1, ddof=1)
    * np.sqrt(252)
)

Mỗi hàng trong windows tương ứng với 20 phiên liên tiếp. NumPy tính độ lệch chuẩn cho tất cả các hàng cùng lúc. Đây là kiểu bài toán rất hợp với mảng: cùng một phép tính được áp dụng lặp lại trên nhiều nhóm dữ liệu có cấu trúc giống nhau.

Phần 3: NumPy làm phép tính nhanh hơn, nhưng không làm giả định trở nên đúng hơn

File thực hành sẽ in ra Total Return, CAGR, biến động năm hóa, Sharpe Ratio và Maximum Drawdown của chiến lược MA20–MA100, sau đó so sánh với Buy and Hold. Chương trình cũng tạo biểu đồ giá, hai đường trung bình và đường tăng trưởng vốn. Cuối cùng, nó sinh một triệu lợi suất ngẫu nhiên rồi đo tốc độ tính tổng bình phương bằng vòng lặp Python và NumPy.

Điều cần nhìn không chỉ là NumPy nhanh hơn bao nhiêu lần. Quan trọng hơn là sau khi tốc độ được cải thiện, quy trình nghiên cứu có thể mở rộng thế nào. Một chiến lược trước đây mất 30 phút để chạy một bộ tham số có thể được kiểm tra trên nhiều mã, nhiều giai đoạn và nhiều mức chi phí hơn. Thời gian tiết kiệm được nên dùng để kiểm tra độ bền, không phải để thử thêm hàng nghìn tham số cho đến khi tìm được một đường lợi nhuận đẹp nhất.

Tốc độ cao cũng có mặt trái. Khi một backtest chạy trong vài giây, người nghiên cứu rất dễ thử MA20–MA100, sau đó MA21–MA99, rồi MA23–MA107, tiếp tục cho tới khi một tổ hợp tình cờ thắng đẹp trong quá khứ. NumPy không gây overfitting, nhưng nó giúp overfitting chạy nhanh hơn nếu quy trình nghiên cứu không có kỷ luật. Bộ phim này gọi là máy mạnh hơn, nhưng người lái vẫn có thể chọn nhầm đường với tốc độ cao hơn.

Code mẫu sử dụng dữ liệu giả lập khi không có CSV, nên kết quả mặc định không đại diện cho bất kỳ cổ phiếu thật nào. Khi dùng dữ liệu thị trường, cần kiểm tra ít nhất các vấn đề sau: giá đã điều chỉnh cổ tức và chia tách hay chưa, có phiên thiếu không, mã cổ phiếu đã đổi sàn hoặc hủy niêm yết không, thanh khoản có đủ để khớp lệnh không và mức chi phí có phản ánh đúng thị trường không. NumPy xử lý con số rất trung thực. Nếu đưa dữ liệu sai vào, nó cũng sẽ tính sai với tốc độ rất ấn tượng.

NumPy phù hợp nhất cho các phép tính mảng như lợi suất, chỉ báo, ma trận tương quan, mô phỏng Monte Carlo, tạo tín hiệu và xử lý nhiều kịch bản. Pandas lại tiện hơn khi cần làm việc với ngày tháng, tên cột, ghép nhiều bảng hoặc xử lý dữ liệu tài chính có cấu trúc. Trong thực tế, hai thư viện thường đi cùng nhau: Pandas dùng để lấy và làm sạch dữ liệu; NumPy đảm nhiệm phần tính toán nặng bên dưới.

Khi mô hình lớn hơn nữa, có thể nghiên cứu thêm Numba để tăng tốc các vòng lặp khó vector hóa, Polars cho xử lý bảng dữ liệu lớn, CuPy để đưa phép tính NumPy lên GPU hoặc các engine backtest chuyên dụng. Nhưng NumPy vẫn là nền móng đáng học đầu tiên vì phần lớn hệ sinh thái khoa học dữ liệu Python đều được xây xung quanh cách tư duy bằng mảng.

NumPy không biến một ý tưởng giao dịch thành chiến lược tốt. Nó chỉ giúp kiểm tra ý tưởng đó nhanh, rõ và trên quy mô đủ lớn để những điểm yếu khó trốn hơn.

numpy_market_example.py

Chạy trực tiếp bằng:

python numpy_market_example.py

Trường hợp có file CSV chứa cột Close:

python numpy_market_example.py \
  --csv du_lieu.csv \
  --close-column Close

Chương trình sẽ xuất kết quả đánh giá trên màn hình và lưu biểu đồ thành file numpy_quant_example.png.