Trang chủBlogChiến Lược TradingMatplotlib: Tự vẽ biểu đồ giá và đường lợi nhuận chiến lược bằng Python
Matplotlib: Tự vẽ biểu đồ giá và đường lợi nhuận chiến lược bằng Python
Chiến Lược Trading

Matplotlib: Tự vẽ biểu đồ giá và đường lợi nhuận chiến lược bằng Python

Matplotlib

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

Trong Quant Trading, xây dựng chiến lược mới chỉ là một nửa công việc. Phần còn lại là trực quan hóa dữ liệu để trả lời những câu hỏi quan trọng: Giá đã vận động như thế nào? Chiến lược bắt đầu mua tại đâu? Khi nào hệ thống thoát khỏi thị trường? Một đồng vốn ban đầu đã tăng hay giảm ra sao?

Matplotlib là một trong những thư viện phổ biến nhất để trực quan hóa dữ liệu bằng Python. Với các biểu đồ phức tạp, tài liệu chính thức của Matplotlib khuyến nghị sử dụng giao diện hướng đối tượng thông qua FigureAxes. Biểu đồ sau khi hoàn thành có thể được lưu thành PNG, SVG hoặc PDF bằng phương thức savefig.

Trong bài viết này, chúng ta sẽ xây dựng một ví dụ hoàn chỉnh gồm ba bước:

  1. Tải và xử lý dữ liệu giá.

  2. Vẽ biểu đồ giá cùng tín hiệu giao dịch.

  3. Tính toán và vẽ đường lợi nhuận của chiến lược.

Chiến lược được sử dụng là giao cắt đường trung bình động SMA 20 ngày và SMA 50 ngày. Đây là ví dụ phục vụ mục đích học tập, không phải khuyến nghị đầu tư.

Phần 1: Chuẩn bị dữ liệu và xây dựng tín hiệu giao dịch

Trước tiên, chúng ta cần cài đặt bốn thư viện:

pip install yfinance pandas numpy matplotlib

Trong đó:

  • yfinance dùng để tải dữ liệu thị trường.

  • pandas dùng để xử lý dữ liệu dạng bảng.

  • numpy hỗ trợ các phép tính số học.

  • matplotlib dùng để vẽ biểu đồ.

Thư viện yfinance cung cấp hàm download để tải dữ liệu lịch sử của một hoặc nhiều mã chứng khoán.

Sau khi cài đặt, hãy tạo một file Python, chẳng hạn:

matplotlib_quant_trading.py

Tiếp theo, nhập các thư viện và tải dữ liệu:

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import yfinance as yf


# =========================
# 1. CẤU HÌNH
# =========================

TICKER = "AAPL"
START_DATE = "2020-01-01"
END_DATE = None

SHORT_WINDOW = 20
LONG_WINDOW = 50

# Chi phí cho mỗi lần thay đổi vị thế: 0,05%
TRANSACTION_COST = 0.0005

# Thư mục lưu biểu đồ
OUTPUT_DIR = Path("charts")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)


# =========================
# 2. TẢI DỮ LIỆU
# =========================

raw_data = yf.download(
    TICKER,
    start=START_DATE,
    end=END_DATE,
    auto_adjust=True,
    progress=False,
)

if raw_data.empty:
    raise RuntimeError(
        f"Không tải được dữ liệu cho mã {TICKER}. "
        "Hãy kiểm tra mã chứng khoán hoặc kết nối Internet."
    )


# Xử lý trường hợp yfinance trả về cột MultiIndex
close_price = raw_data["Close"]

if isinstance(close_price, pd.DataFrame):
    close_price = close_price.iloc[:, 0]


df = pd.DataFrame({
    "Close": close_price.astype(float)
}).dropna()


# =========================
# 3. TÍNH ĐƯỜNG SMA
# =========================

df["SMA_20"] = df["Close"].rolling(
    window=SHORT_WINDOW
).mean()

df["SMA_50"] = df["Close"].rolling(
    window=LONG_WINDOW
).mean()


# =========================
# 4. TẠO TÍN HIỆU
# =========================

# Signal = 1: SMA 20 nằm trên SMA 50
# Signal = 0: SMA 20 nằm dưới hoặc bằng SMA 50
df["Signal"] = (
    df["SMA_20"] > df["SMA_50"]
).astype(int)

# Xác định thời điểm tín hiệu thay đổi
df["Trade"] = df["Signal"].diff()

print(df.tail())

Logic của chiến lược tương đối đơn giản:

  • Khi SMA 20 vượt lên trên SMA 50, hệ thống tạo tín hiệu mua.

  • Khi SMA 20 cắt xuống dưới SMA 50, hệ thống tạo tín hiệu bán.

  • Trong thời gian SMA 20 tiếp tục nằm trên SMA 50, chiến lược duy trì vị thế.

  • Khi SMA 20 nằm dưới SMA 50, chiến lược giữ tiền mặt.

Ở đây, cột Signal cho biết trạng thái của hệ thống, còn cột Trade dùng để xác định thời điểm trạng thái thay đổi.

Nếu Trade bằng 1, chiến lược vừa chuyển từ không nắm giữ sang nắm giữ tài sản.

Nếu Trade bằng -1, chiến lược vừa chuyển từ nắm giữ tài sản sang tiền mặt.

Phần 2: Vẽ biểu đồ giá, đường SMA và tín hiệu mua bán

Sau khi đã có dữ liệu, chúng ta có thể dùng Matplotlib để vẽ giá đóng cửa, SMA 20, SMA 50 và các điểm giao dịch.

Thêm đoạn code sau vào file Python:

# =========================
# 5. XÁC ĐỊNH ĐIỂM MUA BÁN
# =========================

buy_points = df[df["Trade"] == 1]
sell_points = df[df["Trade"] == -1]


# =========================
# 6. VẼ BIỂU ĐỒ GIÁ
# =========================

fig, ax = plt.subplots(figsize=(14, 7))

ax.plot(
    df.index,
    df["Close"],
    label=f"Giá đóng cửa {TICKER}",
    linewidth=1.2,
)

ax.plot(
    df.index,
    df["SMA_20"],
    label=f"SMA {SHORT_WINDOW}",
    linewidth=1.1,
)

ax.plot(
    df.index,
    df["SMA_50"],
    label=f"SMA {LONG_WINDOW}",
    linewidth=1.1,
)

ax.scatter(
    buy_points.index,
    buy_points["Close"],
    marker="^",
    s=70,
    label="Tín hiệu mua",
    zorder=3,
)

ax.scatter(
    sell_points.index,
    sell_points["Close"],
    marker="v",
    s=70,
    label="Tín hiệu bán",
    zorder=3,
)

ax.set_title(
    f"Biểu đồ giá và chiến lược SMA "
    f"{SHORT_WINDOW}–{LONG_WINDOW}"
)

ax.set_xlabel("Thời gian")
ax.set_ylabel("Giá")
ax.grid(alpha=0.25)
ax.legend()

fig.tight_layout()

price_chart_path = OUTPUT_DIR / "price_and_signals.png"

fig.savefig(
    price_chart_path,
    dpi=200,
    bbox_inches="tight",
)

plt.show()

print(f"Đã lưu biểu đồ giá tại: {price_chart_path}")

Kết quả thu được sẽ có dạng như sau:

matplotlib_bieu_do_gia_sma.png

Trên biểu đồ:

  • Đường giá thể hiện giá đóng cửa của tài sản.

  • Đường SMA 20 phản ứng nhanh hơn trước biến động giá.

  • Đường SMA 50 phản ứng chậm hơn và đại diện cho xu hướng trung hạn.

  • Tam giác hướng lên thể hiện tín hiệu mua.

  • Tam giác hướng xuống thể hiện tín hiệu bán.

Việc đặt giá, SMA và tín hiệu trên cùng một biểu đồ giúp chúng ta kiểm tra trực quan xem chiến lược có đang hoạt động đúng với logic ban đầu hay không.

Chẳng hạn, nếu xuất hiện tín hiệu mua trong khi SMA 20 vẫn nằm dưới SMA 50, có thể phần tính toán hoặc cách xác định giao điểm đang có lỗi.

Biểu đồ cũng giúp phát hiện nhược điểm thường gặp của chiến lược giao cắt trung bình động. Khi thị trường có xu hướng rõ ràng, hệ thống có thể duy trì vị thế trong thời gian dài. Nhưng khi thị trường đi ngang và liên tục đảo chiều, SMA 20 và SMA 50 có thể cắt nhau nhiều lần, tạo ra các giao dịch thua lỗ nhỏ liên tiếp.

Đây là lý do một biểu đồ đẹp chưa đủ để chứng minh chiến lược có hiệu quả. Chúng ta cần tiếp tục tính toán lợi nhuận và đường tăng trưởng vốn.

Phần 3: Tính toán và vẽ đường lợi nhuận chiến lược

Đường lợi nhuận, hay equity curve, thể hiện sự thay đổi của giá trị danh mục theo thời gian.

Giả sử chúng ta bắt đầu với một đơn vị vốn:

  • Giá trị bằng 1.00 nghĩa là danh mục chưa tăng hoặc giảm.

  • Giá trị bằng 1.20 nghĩa là danh mục tăng 20%.

  • Giá trị bằng 0.80 nghĩa là danh mục giảm 20%.

Trước tiên, tính lợi nhuận thị trường, vị thế và lợi nhuận chiến lược:

# =========================
# 7. TÍNH LỢI NHUẬN
# =========================

df["Market_Return"] = (
    df["Close"]
    .pct_change()
    .fillna(0)
)


# Dịch tín hiệu xuống một phiên
# Tín hiệu cuối ngày hôm nay được thực hiện từ ngày tiếp theo
df["Position"] = (
    df["Signal"]
    .shift(1)
    .fillna(0)
)


# Mức độ thay đổi vị thế
df["Turnover"] = (
    df["Position"]
    .diff()
    .abs()
    .fillna(0)
)


# Lợi nhuận chiến lược sau chi phí giao dịch
df["Strategy_Return"] = (
    df["Position"] * df["Market_Return"]
    - TRANSACTION_COST * df["Turnover"]
)


# Đường tăng trưởng vốn
df["Equity_Strategy"] = (
    1 + df["Strategy_Return"]
).cumprod()

df["Equity_BuyHold"] = (
    1 + df["Market_Return"]
).cumprod()

Điểm quan trọng nhất trong đoạn code trên là:

df["Position"] = df["Signal"].shift(1)

Lệnh shift(1) giúp trì hoãn việc thực hiện tín hiệu sang phiên tiếp theo.

Giả sử SMA 20 vượt SMA 50 khi phiên giao dịch ngày thứ Hai kết thúc. Chúng ta chỉ biết chắc tín hiệu này sau khi đã có giá đóng cửa ngày thứ Hai. Vì vậy, chiến lược không thể hưởng toàn bộ lợi nhuận của chính phiên thứ Hai dựa trên một tín hiệu chỉ được xác nhận vào cuối phiên.

Nếu sử dụng tín hiệu của ngày hiện tại để tính trực tiếp lợi nhuận của chính ngày đó, kết quả có thể chứa sai lệch nhìn trước, hay look-ahead bias.

Tiếp theo, vẽ đường tăng trưởng vốn:

# =========================
# 8. VẼ ĐƯỜNG LỢI NHUẬN
# =========================

fig, ax = plt.subplots(figsize=(14, 7))

ax.plot(
    df.index,
    df["Equity_Strategy"],
    label="Chiến lược SMA",
    linewidth=1.4,
)

ax.plot(
    df.index,
    df["Equity_BuyHold"],
    label="Buy & Hold",
    linewidth=1.2,
)

ax.axhline(
    1.0,
    linestyle="--",
    linewidth=0.8,
)

ax.set_title(
    f"Đường tăng trưởng vốn: "
    f"Chiến lược SMA và Buy & Hold"
)

ax.set_xlabel("Thời gian")
ax.set_ylabel("Giá trị danh mục")
ax.grid(alpha=0.25)
ax.legend()

fig.tight_layout()

equity_chart_path = OUTPUT_DIR / "strategy_equity_curve.png"

fig.savefig(
    equity_chart_path,
    dpi=200,
    bbox_inches="tight",
)

plt.show()

print(f"Đã lưu đường lợi nhuận tại: {equity_chart_path}")

Kết quả minh họa:

matplotlib_duong_loi_nhuan_chien_luoc.png

Để đánh giá chiến lược đầy đủ hơn, chúng ta có thể tính thêm tổng lợi nhuận, CAGR, biến động, Sharpe Ratio và mức sụt giảm tối đa.

# =========================
# 9. CÁC CHỈ SỐ ĐÁNH GIÁ
# =========================

def calculate_max_drawdown(
    equity_curve: pd.Series
) -> float:
    """
    Tính mức sụt giảm tối đa của danh mục.
    """

    running_max = equity_curve.cummax()

    drawdown = (
        equity_curve / running_max
    ) - 1

    return float(drawdown.min())


def calculate_metrics(
    returns: pd.Series,
    equity_curve: pd.Series,
) -> dict:
    """
    Tính một số chỉ số đánh giá cơ bản.
    Giả định một năm có 252 phiên giao dịch.
    """

    returns = returns.dropna()

    number_of_days = len(returns)

    if number_of_days == 0:
        raise ValueError(
            "Không có dữ liệu để tính chỉ số."
        )

    total_return = equity_curve.iloc[-1] - 1

    cagr = (
        equity_curve.iloc[-1]
        ** (252 / number_of_days)
        - 1
    )

    annualized_return = returns.mean() * 252

    annualized_volatility = (
        returns.std() * np.sqrt(252)
    )

    if annualized_volatility == 0:
        sharpe_ratio = np.nan
    else:
        sharpe_ratio = (
            annualized_return
            / annualized_volatility
        )

    max_drawdown = calculate_max_drawdown(
        equity_curve
    )

    return {
        "Total Return": total_return,
        "CAGR": cagr,
        "Annualized Volatility": annualized_volatility,
        "Sharpe Ratio": sharpe_ratio,
        "Max Drawdown": max_drawdown,
    }


strategy_metrics = calculate_metrics(
    df["Strategy_Return"],
    df["Equity_Strategy"],
)

buy_hold_metrics = calculate_metrics(
    df["Market_Return"],
    df["Equity_BuyHold"],
)


results = pd.DataFrame({
    "SMA Strategy": strategy_metrics,
    "Buy & Hold": buy_hold_metrics,
})


print("\nKẾT QUẢ BACKTEST")
print("=" * 50)

for metric in [
    "Total Return",
    "CAGR",
    "Annualized Volatility",
    "Max Drawdown",
]:
    results.loc[metric] = (
        results.loc[metric] * 100
    )

print(results.round(2))

Khi đọc kết quả, không nên chỉ nhìn vào tổng lợi nhuận.

Một chiến lược có tổng lợi nhuận cao nhưng Max Drawdown quá lớn có thể khiến nhà đầu tư không đủ khả năng hoặc tâm lý để tiếp tục duy trì hệ thống.

Một chiến lược có Sharpe Ratio cao trong dữ liệu quá khứ cũng chưa chắc tiếp tục hoạt động trong tương lai. Kết quả có thể bị ảnh hưởng bởi lựa chọn mã chứng khoán, giai đoạn kiểm định, thông số SMA, chi phí giao dịch và điều kiện thị trường.

Để bài kiểm định thực tế hơn, có thể phát triển thêm các nội dung sau:

  • Kiểm tra nhiều mã chứng khoán thay vì một mã.

  • Tách dữ liệu thành giai đoạn phát triển và giai đoạn kiểm định.

  • So sánh nhiều bộ thông số SMA.

  • Bổ sung phí giao dịch, spread và slippage.

  • Tính thời gian nắm giữ và số lượng giao dịch.

  • Kiểm tra chiến lược trong thị trường tăng, giảm và đi ngang.

  • So sánh với benchmark phù hợp.

  • Không lựa chọn thông số chỉ vì chúng cho kết quả đẹp nhất trong quá khứ.

Matplotlib không trực tiếp quyết định một chiến lược có tốt hay không. Giá trị lớn nhất của thư viện này là biến những con số khô khan thành hình ảnh có thể quan sát và kiểm tra.

Biểu đồ giá giúp chúng ta hiểu hệ thống vào và thoát lệnh tại đâu. Đường lợi nhuận cho biết chiến lược đã trải qua những giai đoạn tăng trưởng, đi ngang và sụt giảm như thế nào.

Khi kết hợp biểu đồ với các chỉ số như CAGR, Sharpe Ratio và Max Drawdown, chúng ta có được một quy trình đánh giá chiến lược rõ ràng hơn:

Hình thành giả thuyết → viết quy tắc → backtest → trực quan hóa → đánh giá rủi ro → kiểm định ngoài mẫu.

Đó cũng là bước chuyển quan trọng từ việc nhìn biểu đồ theo cảm tính sang xây dựng một quy trình Quant Trading có thể đo lường, kiểm tra và cải tiến.