| Backtest Hệ Thống Giao Dịch Hoàn Chỉnh Bằng Python Entry Exit Và Quản Trị Rủi Ro

Được viết bởi vào ngày 14/08/2026 lúc 19:03 | 5 lượt xem

Backtest Hệ Thống Giao Dịch Hoàn Chỉnh Bằng Python — Entry, Exit & Quản Trị Rủi Ro

Ngày 60 – 2026-08-13 | Chuỗi Python 365 ngày | Hướng Nghiệp Dữ Liệu

📺 Video: https://www.youtube.com/shorts/WbZUREfZdgI

Mục lục

  1. Tổng kết 4 chỉ báo đã học
  2. Chỉ báo ≠ hệ thống giao dịch
  3. Kiến trúc hệ thống: Entry + Exit + Risk
  4. Chiến lược mẫu: Bollinger + RSI + ATR
  5. Code backtest từng bước
  6. Risk Management: định cỡ lệnh theo % rủi ro
  7. Đo lường hiệu suất: Sharpe, Max DD, Win Rate
  8. Những lỗi backtest kinh điển phải tránh
  9. Cải thiện hiệu suất hệ thống
  10. Script Ngày 60 hoàn chỉnh
  11. FAQ
  12. Kết luận

1. Tổng kết 4 chỉ báo đã học

Trong 59 ngày qua, bạn đã xây được một bộ công cụ chỉ báo kỹ thuật hoàn chỉnh bằng Python thuần. Đây là hành trang đáng giá nhất của chuỗi, vì không phải ai cũng chịu khó tự code từng chỉ báo từ số 0.

Chỉ báo Ngày Câu hỏi trả lời Vai trò
MA 48 Giá đang đi đâu? Xu hướng
RSI 57 Giá đang mạnh hay yếu? Sức mạnh
MACD 58 Động lượng đổi chiều khi nào? Động lượng
Bollinger Bands 59 Biến động nhiều hay ít? Biến động

Mỗi chỉ báo là một cảm biến đo một khía cạnh khác nhau của thị trường. Giống như chiếc xe cần nhiều đồng hồ đo — tốc độ, nhiên liệu, nhiệt độ động cơ — một hệ thống giao dịch chuyên nghiệp cũng cần nhiều loại tín hiệu bổ sung cho nhau.

Nhưng có một sự thật quan trọng mà người mới hay quên:

Chỉ báo chỉ là công cụ đo lường. Hệ thống giao dịch mới là thứ tạo ra lợi nhuận (hoặc thua lỗ) có kiểm soát.

Ngày 60 hôm nay, chúng ta ghép chúng lại thành một hệ thống giao dịch hoàn chỉnh — và quan trọng nhất là học cách backtest để biết hệ thống đó có đáng dùng hay không, trước khi bỏ một đồng tiền thật nào.

Vì sao phải học “hệ thống” thay vì chỉ “chỉ báo”?

Phần lớn người mới chỉ dừng ở việc “biết cách tính RSI”, rồi tự tin vào lệnh theo cảm tính. Kết quả là thua lỗ mà không hiểu vì sao. Khi bạn có một hệ thống, bạn có:

  1. Tính lặp lại — mọi quyết định đều theo quy tắc, không phụ thuộc tâm trạng.
  2. Khả năng đo lường — bạn biết chính xác win rate, drawdown, lợi nhuận kỳ vọng.
  3. Khả năng cải tiến — chỉ cần sửa một tầng (Entry, Exit hay Risk) là kiểm chứng lại ngay.

2. Chỉ báo ≠ hệ thống giao dịch

Một chỉ báo như RSI cho bạn biết “giá đang quá mua”. Nhưng nó không trả lời 3 câu hỏi sống còn của một giao dịch:

  1. Khi nào VÀO lệnh? (Entry) — điều kiện cụ thể, không cảm tính.
  2. Khi nào THOÁT lệnh? (Exit) — chốt lời và cắt lỗ ra sao.
  3. Vào BAO NHIÊU? (Risk) — mỗi lệnh rủi ro bao nhiêu % tài khoản.

Một hệ thống giao dịch = Entry + Exit + Risk. Thiếu một trong ba, bạn chỉ đang “đoán” chứ không phải “giao dịch”.

Vì sao phải backtest?

Backtest là chạy lại chiến lược trên dữ liệu lịch sử để trả lời câu hỏi: “Nếu tôi giao dịch đúng quy tắc này trong 2 năm qua, kết quả thế nào?”

  • Không backtest → bạn giao dịch bằng niềm tin.
  • Backtest đúng → bạn có bằng chứng về win rate, drawdown, lợi nhuận kỳ vọng.
  • Backtest sai → bạn có “bằng chứng giả”, còn nguy hiểm hơn không có gì.

⚠️ Backtest tốt không đảm bảo lợi nhuận tương lai, nhưng không backtest thì bạn gần như chắc chắn đang mù. Và backtest sai (xem mục 8) còn tệ hơn — vì bạn tưởng mình an toàn trong khi thực tế không phải vậy.


3. Kiến trúc hệ thống: Entry + Exit + Risk

Trước khi code, hãy vẽ rõ kiến trúc. Tư duy “tách tầng” này là thứ phân biệt lập trình viên thực thụ với người viết script theo cảm hứng:

DỮ LIỆU (OHLCV)
   │
   ▼
CHỈ BÁO (MA, RSI, MACD, Bollinger, ATR)
   │
   ▼
SIGNAL (Entry / Exit)
   │
   ▼
RISK (định cỡ lệnh theo % rủi ro)
   │
   ▼
PORTFOLIO (equity theo thời gian)
   │
   ▼
METRICS (Sharpe, Max DD, Win rate, Profit factor)

Mỗi tầng là một hàm riêng — đây là nguyên tắc single responsibility. Khi muốn thử chiến lược mới, bạn chỉ sửa tầng SIGNAL; khi muốn đổi cách quản trị rủi ro, chỉ sửa tầng RISK. Hệ thống trở nên dễ thử nghiệm, dễ gỡ lỗi và dễ giải thích.

Lợi ích của kiến trúc tách tầng

  1. Dễ debug — khi kết quả lạ, bạn biết lỗi nằm ở tầng nào.
  2. Dễ A/B test — đổi Entry nhưng giữ nguyên Exit/Risk để đo ảnh hưởng.
  3. Dễ tái sử dụng — hàm backtest dùng chung cho mọi chiến lược.

4. Chiến lược mẫu: Bollinger + RSI + ATR

Để minh họa, ta dùng một chiến lược đơn giản nhưng đủ “hoàn chỉnh”. Logic chọn chiến lược này rất có chủ đích:

  • Bollinger Bands đóng vai trò “bản đồ biến động” — cho biết giá đang ở đâu trong vùng giá kỳ vọng.
  • RSI đóng vai trò “máy đo sức mạnh” — xác nhận trạng thái quá mua/quá bán.
  • ATR đóng vai trò “thước đo rủi ro” — tự động co giãn stop loss theo biến động thực tế.

Entry (Long — mua)

  • Giá đóng cửa chạm dải dưới Bollinger (price ≤ Lower Band)
  • RSI(14) < 35 (quá bán xác nhận)
  • Xu hướng dài hạn tăng (Close > SMA(200))

Entry (Short — bán)

  • Giá đóng cửa chạm dải trên Bollinger (price ≥ Upper Band)
  • RSI(14) > 65 (quá mua xác nhận)
  • Xu hướng dài hạn giảm (Close < SMA(200))

Exit

  • Stop Loss (SL): cách điểm vào 2 × ATR(14) — để biến động “thở”, không bị quét lệnh nhiễu.
  • Take Profit (TP): cách điểm vào 3 × ATR(14) — tỷ lệ lời/lỗ 1.5:1.
  • Thoát theo tín hiệu ngược: khi RSI trở lại vùng trung lập hoặc giá chạm dải ngược.

💡 Chọn ATR làm thước đo khoảng cách stop là chuẩn mực của trader chuyên nghiệp — vì ATR tự co giãn theo độ biến động. Một stop 20 pip cố định có thể hợp lý lúc thị trường êm, nhưng bị “quét” ngay khi biến động mạnh.


5. Code backtest từng bước

5.1. Chuẩn bị dữ liệu

import pandas as pd
import numpy as np

df = pd.read_csv("XAUUSD_H1.csv")
df["time"] = pd.to_datetime(df["time"])
df = df.set_index("time").sort_index()

Dữ liệu là nền tảng của mọi backtest. Hai nguyên tắc về dữ liệu: đủ dài (ít nhất 1–2 năm, trải qua cả tăng, giảm, đi ngang) và sạch (loại outlier, xử lý missing, đảm bảo thứ tự thời gian).

5.2. Tính chỉ báo (vectorized)

# Bollinger Bands (Ngày 59)
df["sma20"] = df["close"].rolling(20).mean()
df["std20"] = df["close"].rolling(20).std(ddof=0)
df["upper"] = df["sma20"] + 2 * df["std20"]
df["lower"] = df["sma20"] - 2 * df["std20"]

# RSI (Ngày 57) — công thức Wilder
delta = df["close"].diff()
gain = delta.clip(lower=0).ewm(alpha=1/14, adjust=False).mean()
loss = (-delta.clip(upper=0)).ewm(alpha=1/14, adjust=False).mean()
df["rsi"] = 100 - 100 / (1 + gain / loss)

# SMA200 cho xu hướng dài hạn
df["sma200"] = df["close"].rolling(200).mean()

# ATR(14) cho stop loss
tr = pd.concat([
    df["high"] - df["low"],
    (df["high"] - df["close"].shift()).abs(),
    (df["low"]  - df["close"].shift()).abs(),
], axis=1).max(axis=1)
df["atr"] = tr.ewm(alpha=1/14, adjust=False).mean()

Chú ý ddof=0 khi tính độ lệch chuẩn — đây là cách Bollinger định nghĩa (population std), khớp với hầu hết nền tảng như TradingView. Chi tiết nhỏ này khiến kết quả của bạn khác biệt nếu sai.

5.3. Tạo tín hiệu Entry

df["long_signal"]  = (df["close"] <= df["lower"]) & (df["rsi"] < 35) & (df["close"] > df["sma200"])
df["short_signal"] = (df["close"] >= df["upper"]) & (df["rsi"] > 65) & (df["close"] < df["sma200"])

Ta dùng giá đóng cửa để đánh giá điều kiện — vì tại thời điểm vào lệnh, bạn chỉ có dữ liệu đã đóng nến, chưa có nến tương lai.

5.4. Mô phỏng vòng lặp giao dịch (event loop)

capital = 10_000.0
risk_pct = 0.01
position = None
trades = []
equity_curve = []

for row in df.itertuples():
    if position:
        if position["side"] == "long":
            if row.low <= position["sl"]:
                capital += position["size"] * (position["sl"] - position["entry"]) / position["entry"] * 100
                trades.append({"exit": "SL", "pnl_pct": (position["sl"] - position["entry"]) / position["entry"] * 100})
                position = None
            elif row.high >= position["tp"]:
                capital += position["size"] * (position["tp"] - position["entry"]) / position["entry"] * 100
                trades.append({"exit": "TP", "pnl_pct": (position["tp"] - position["entry"]) / position["entry"] * 100})
                position = None
        else:
            if row.high >= position["sl"]:
                capital += position["size"] * (position["entry"] - position["sl"]) / position["entry"] * 100
                trades.append({"exit": "SL", "pnl_pct": (position["entry"] - position["sl"]) / position["entry"] * 100})
                position = None
            elif row.low <= position["tp"]:
                capital += position["size"] * (position["entry"] - position["tp"]) / position["entry"] * 100
                trades.append({"exit": "TP", "pnl_pct": (position["entry"] - position["tp"]) / position["entry"] * 100})
                position = None

    if position is None:
        if row.long_signal:
            entry = row.close
            sl, tp = entry - 2 * row.atr, entry + 3 * row.atr
            size = (capital * risk_pct) / ((entry - sl) / entry)
            position = {"side": "long", "entry": entry, "sl": sl, "tp": tp, "size": size}
        elif row.short_signal:
            entry = row.close
            sl, tp = entry + 2 * row.atr, entry - 3 * row.atr
            size = (capital * risk_pct) / ((sl - entry) / entry)
            position = {"side": "short", "entry": entry, "sl": sl, "tp": tp, "size": size}

    equity_curve.append(capital)

⚠️ Đoạn trên là bản rút gọn để dạy logic. Script hoàn chỉnh ở Mục 10 xử lý thêm spread, commission và dùng itertuples() thay vì iterrows() (chậm).

5.5. Vì sao không nên dùng iterrows()

iterrows() tạo một Series mới cho mỗi dòng — rất chậm với dữ liệu lớn. Thay bằng itertuples() (nhanh gấp ~10 lần) hoặc vectorize hoàn toàn. Với backtest 10.000 nến, khác biệt có thể từ vài giây đến vài phút.


6. Risk Management: định cỡ lệnh theo % rủi ro

Công thức định cỡ lệnh quan trọng nhất bạn cần thuộc lòng:

size = (Capital × risk%) / stop_distance_pct

Ví dụ: vốn $10,000, rủi ro 1% = $100. Nếu stop loss cách entry 2% giá, thì size = 10,000 × 1% / 2% = $5,000. Nếu dính stop loss 2%, bạn mất đúng $100 (1% vốn). Đây là nguyên tắc rủi ro cố định (fixed fractional risk): thắng hay thua, bạn không bao giờ mất quá 1% vốn một lệnh.

Vì sao cố định % rủi ro quan trọng?

Hệ quả thú vị của fixed fractional risk: khi bạn thua, bạn rủi ro ít hơn (vì vốn giảm); khi thắng, bạn rủi ro nhiều hơn (vì vốn tăng). Đây chính là compounding tự động — vốn tăng trưởng theo cấp số nhân nhưng luôn an toàn.

Bảng rủi ro theo chuỗi thua

% rủi ro / lệnh Mất bao nhiêu để -50% vốn
1% ~69 lệnh thua liên tiếp
2% ~35 lệnh thua liên tiếp
5% ~14 lệnh thua liên tiếp
10% ~7 lệnh thua liên tiếp

Đây là lý do trader chuyên nghiệp hiếm khi rủi ro quá 1–2% mỗi lệnh: sống sót lâu dài quan trọng hơn thắng nhanh. Một hệ thống win rate 40% vẫn có thể có chuỗi 10 lệnh thua liên tiếp — nếu rủi ro 10%/lệnh, bạn đã mất 65% vốn trước khi hệ thống “đúng”.


7. Đo lường hiệu suất: Sharpe, Max DD, Win Rate

eq = pd.Series(equity_curve)
returns = eq.pct_change().dropna()

total_return = eq.iloc[-1] / eq.iloc[0] - 1
sharpe = returns.mean() / returns.std() * np.sqrt(252 * 24)  # H1

rolling_max = eq.cummax()
max_dd = (eq / rolling_max - 1).min()

trades_df = pd.DataFrame(trades)
win_rate = (trades_df["pnl_pct"] > 0).mean()
gross_win  = trades_df.loc[trades_df["pnl_pct"] > 0, "pnl_pct"].sum()
gross_loss = -trades_df.loc[trades_df["pnl_pct"] < 0, "pnl_pct"].sum()
profit_factor = gross_win / gross_loss

Giải thích từng chỉ số

  1. Total Return — tổng lợi nhuận cuối kỳ. Ấn tượng nhưng dễ gây hiểu lầm nếu đứng một mình.
  2. Sharpe Ratio — lợi nhuận trên mỗi đơn vị rủi ro (độ lệch chuẩn). “Chỉ số thông minh” nhất vì cân bằng giữa lợi nhuận và rủi ro.
  3. Max Drawdown — mức sụt giảm tệ nhất từ đỉnh đến đáy. Cho bạn biết “bạn có chịu nổi không?” về mặt tâm lý.
  4. Win Rate — tỷ lệ lệnh thắng. Không phải càng cao càng tốt — win rate 40% với tỷ lệ lời/lỗ 1.5:1 vẫn có lợi nhuận.
  5. Profit Factor — tổng lời / tổng lỗ. > 1 là có lời, > 1.5 là tốt.

Ngưỡng đánh giá nhanh

Metric Tệ Tạm được Tốt
Sharpe < 1 1 – 2 > 2
Max Drawdown > -30% -15% → -30% < -15%
Profit Factor < 1 1.2 – 1.8 > 1.8
Số lệnh mẫu < 30 30 – 100 > 100

Một hệ thống Sharpe 1.5, Max DD -12%, Profit Factor 1.6 là hệ thống “dùng được”. Sharpe 0.5 với Max DD -40% thì đừng đưa vào tiền thật. Kết quả từ ít hơn 30 lệnh không có ý nghĩa thống kê — dù Sharpe có đẹp đến mấy.


8. Những lỗi backtest kinh điển phải tránh

8.1. Look-ahead bias (nhìn về tương lai)

Dùng dữ liệu tương lai để quyết định quá khứ. Ví dụ: tính tín hiệu bằng cả nến hiện tại rồi vào lệnh ở chính nến đó — trong thực tế bạn chỉ có thể vào lệnh ở nến SAU khi nến đóng cửa. Cách tránh: luôn dùng .shift(1).

df["signal"] = df["signal"].shift(1)  # chỉ vào lệnh ở phiên sau

8.2. Overfitting (khớp quá khứ)

Tinh chỉnh tham số tới mức chỉ khớp đúng quá khứ. Hệ thống quá phức tạp + ít dữ liệu = overfitting. Cách tránh: giữ hệ thống đơn giản, dùng Walk-Forward (học ở Ngày 61).

8.3. Bỏ qua chi phí giao dịch

Không tính spread, commission, slippage. Với H1 forex, spread 1 pip mỗi round-trip có thể “ăn” 30–50% lợi nhuận backtest.

COST = 0.0001
pnl -= 2 * COST  # mỗi round-trip chịu 2 chiều spread

8.4. Survivorship bias & dữ liệu nhiễu

Chỉ test trên các cặp “đã từng thắng” (survivorship bias), hoặc dùng dữ liệu đã được làm sạch không khớp thực tế. Luôn test trên nhiều cặp và dữ liệu thô.


9. Cải thiện hiệu suất hệ thống

Khi hệ thống cơ bản đã chạy, bạn có 3 hướng cải thiện theo thứ tự ưu tiên:

  1. Cải thiện Exit (rẻ nhất, an toàn nhất) — tối ưu SL/TP theo ATR, thêm trailing stop.
  2. Cải thiện Entry (khó hơn) — thêm bộ lọc để giảm lệnh thua, không phải thêm chỉ báo chồng chất.
  3. Cải thiện Risk (quan trọng nhất về lâu dài) — thử các mức risk 0.5%, 1%, 2% để tìm điểm cân bằng giữa tăng trưởng và drawdown.

Một mẹo thực tế: ít lệnh nhưng chất lượng cao hơn nhiều lệnh nhiễu. Một bộ lọc tốt (ví dụ chỉ trade khi ADX > 25, tức có xu hướng) thường tăng Sharpe đáng kể dù giảm số lệnh.


10. Script Ngày 60 hoàn chỉnh

"""
Python 365 — Ngày 60: Backtest hệ thống giao dịch hoàn chỉnh.
Entry: Bollinger + RSI + SMA200. Exit: ATR stop/target. Risk: fixed fractional.
"""
import pandas as pd
import numpy as np

def add_indicators(df):
    df = df.copy()
    df["sma20"] = df["close"].rolling(20).mean()
    df["std20"] = df["close"].rolling(20).std(ddof=0)
    df["upper"] = df["sma20"] + 2 * df["std20"]
    df["lower"] = df["sma20"] - 2 * df["std20"]
    delta = df["close"].diff()
    gain = delta.clip(lower=0).ewm(alpha=1/14, adjust=False).mean()
    loss = (-delta.clip(upper=0)).ewm(alpha=1/14, adjust=False).mean()
    df["rsi"] = 100 - 100 / (1 + gain / loss)
    df["sma200"] = df["close"].rolling(200).mean()
    tr = pd.concat([
        df["high"] - df["low"],
        (df["high"] - df["close"].shift()).abs(),
        (df["low"] - df["close"].shift()).abs(),
    ], axis=1).max(axis=1)
    df["atr"] = tr.ewm(alpha=1/14, adjust=False).mean()
    df["long_signal"]  = (df["close"] <= df["lower"]) & (df["rsi"] < 35) & (df["close"] > df["sma200"])
    df["short_signal"] = (df["close"] >= df["upper"]) & (df["rsi"] > 65) & (df["close"] < df["sma200"])
    return df

def backtest(df, capital=10_000, risk_pct=0.01, sl_atr=2.0, tp_atr=3.0, cost=0.0001):
    eq = capital
    position = None
    trades = []
    curve = []
    for row in df.itertuples():
        if position:
            if position["side"] == "long":
                if row.low <= position["sl"]:
                    eq += position["size"] * (position["sl"] - position["entry"]) / position["entry"]
                    trades.append((position["sl"] - position["entry"]) / position["entry"] * 100 - 2*cost)
                    position = None
                elif row.high >= position["tp"]:
                    eq += position["size"] * (position["tp"] - position["entry"]) / position["entry"]
                    trades.append((position["tp"] - position["entry"]) / position["entry"] * 100 - 2*cost)
                    position = None
            else:
                if row.high >= position["sl"]:
                    eq += position["size"] * (position["entry"] - position["sl"]) / position["entry"]
                    trades.append((position["entry"] - position["sl"]) / position["entry"] * 100 - 2*cost)
                    position = None
                elif row.low <= position["tp"]:
                    eq += position["size"] * (position["entry"] - position["tp"]) / position["entry"]
                    trades.append((position["entry"] - position["tp"]) / position["entry"] * 100 - 2*cost)
                    position = None
        if position is None:
            if row.long_signal:
                entry = row.close
                sl, tp = entry - sl_atr * row.atr, entry + tp_atr * row.atr
                size = (eq * risk_pct) / ((entry - sl) / entry)
                position = {"side": "long", "entry": entry, "sl": sl, "tp": tp, "size": size}
            elif row.short_signal:
                entry = row.close
                sl, tp = entry + sl_atr * row.atr, entry - tp_atr * row.atr
                size = (eq * risk_pct) / ((sl - entry) / entry)
                position = {"side": "short", "entry": entry, "sl": sl, "tp": tp, "size": size}
        curve.append(eq)
    return pd.Series(curve), pd.Series(trades)

if __name__ == "__main__":
    df = pd.read_csv("XAUUSD_H1.csv", parse_dates=["time"]).set_index("time").sort_index()
    df = add_indicators(df).dropna()
    curve, trades = backtest(df)
    ret = curve.pct_change().dropna()
    print("Total Return :", f"{curve.iloc[-1]/curve.iloc[0]-1:.2%}")
    print("Sharpe       :", f"{ret.mean()/ret.std()*np.sqrt(252*24):.2f}")
    print("Max Drawdown :", f"{(curve/curve.cummax()-1).min():.2%}")
    print("Win Rate     :", f"{(trades>0).mean():.2%}")

11. FAQ

Q: Backtest dùng dữ liệu nào?
Dùng OHLCV lịch sử — kéo từ MetaTrader5 bằng copy_rates_from_pos (Ngày 1–10) hoặc CSV. Quan trọng là dữ liệu đủ dài và sạch.

Q: Backtest tốt nhưng live thua, tại sao?
Thường do 4 nguyên nhân: (1) look-ahead bias, (2) overfitting, (3) bỏ qua phí/spread/slippage, (4) tâm lý không theo đúng hệ thống khi live.

Q: Vì sao phải dùng ATR cho stop loss?
Vì ATR co giãn theo biến động. Stop cố định 20 pip có thể bị “quét” khi biến động lớn. ATR tự điều chỉnh theo điều kiện thị trường.

Q: Sharpe bao nhiêu là đủ?

1 là chấp nhận được, > 2 là tốt. Nhưng phải xem cùng Max Drawdown và số lệnh mẫu (vài trăm lệnh mới có ý nghĩa).

Q: Tôi nên backtest bao nhiêu lịch sử?
Tối thiểu 1–2 năm, lý tưởng là đủ cả giai đoạn tăng, giảm và sideway.

Q: Win rate thấp có phải hệ thống xấu?
Không nhất thiết. Win rate 40% nhưng tỷ lệ lời/lỗ 1.5:1 vẫn có lợi nhuận dương. Quan trọng là expectancy: Expectancy = WinRate × AvgWin − (1−WinRate) × AvgLoss.

Q: ddof=0 trong Bollinger khác gì?
ddof=0 là độ lệch chuẩn tổng thể (population), ddof=1 là mẫu (sample). Bollinger chuẩn dùng ddof=0, khớp với TradingView.

Q: Backtest có cần tính spread không?
BẮT BUỘC nếu muốn kết quả gần thực tế. Spread + commission + slippage có thể ăn 30–50% lợi nhuận.

Q: Hệ thống của tôi không có lệnh nào thì sao?
Nghĩa là bộ điều kiện Entry quá chặt so với dữ liệu. Hãy nới dần: tăng ngưỡng RSI, bỏ bớt một bộ lọc, hoặc đổi symbol. Mục tiêu có ít nhất 30–50 lệnh để đánh giá mới có ý nghĩa.

Q: Nên backtest trên timeframe nào?
Mỗi timeframe có đặc tính riêng: M5–M15 nhiều lệnh nhưng nhiễu và phí nặng; H1–H4 cân bằng tốt nhất cho người mới; D1 ít lệnh nhưng tín hiệu sạch. Khuyến nghị bắt đầu từ H1.

Q: Tại sao kết quả backtest khác TradingView?
Thường do 3 khác biệt: (1) cách tính độ lệch chuẩn ddof=0 vs ddof=1, (2) cách tính RSI (Wilder smoothing vs EMA), (3) thời điểm vào lệnh (giá đóng nến vs giá hiện tại). Kiểm tra từng cột tín hiệu để đối chiếu và điều chỉnh.

Q: Equity curve giảm dần dù có lệnh thắng, tại sao?
Thường do một chuỗi thua lớn hoặc vài lệnh thua to hơn mức 1% do size tính sai. Hãy in danh sách từng lệnh (entry, SL, TP, size, pnl) để tìm lệnh “phá kỷ luật” — thường là lỗi code, không phải lỗi chiến lược.

Q: Có nên dùng commission thật khi backtest không?
Có. Với forex, spread 1–2 pip + commission $7/lot là chuẩn. Nếu bạn không chắc mức phí, hãy dùng mức ước tính cao hơn thực tế một chút — thà an toàn còn hơn tự lừa mình.

Q: Vì sao backtest cần chia train/test?
Để kiểm tra hệ thống trên dữ liệu “chưa từng thấy” khi tối ưu — giống như thi thử không nên ra đúng đề đã ôn. Ngày 61 sẽ học kỹ kỹ thuật Walk-Forward cho việc này.

Q: Nên backtest nhiều symbol hay một symbol?
Backtest trên 3–5 symbol khác nhau để xem hệ thống có khái quát hay chỉ “ăn may” đúng một cặp. Nếu chỉ thắng một cặp duy nhất, rất có thể là overfit theo đặc tính cặp đó.

Q: Lợi nhuận backtest lớn bao nhiêu là bất thường?
Với H1, lợi nhuận 100%+/năm với Max DD nhỏ thường là dấu hiệu lỗi (look-ahead, thiếu phí) hoặc overfit. Hãy nghi ngờ mọi kết quả “quá đẹp” — thị trường hiếm khi tặng quà dễ dàng.


12. Kết luận

Ngày 60 là cột mốc quan trọng nhất của chuỗi Python 365: bạn không còn chỉ “tính chỉ báo”, mà đã ghép chúng thành một hệ thống có Entry, Exit và Risk Management — và biết cách đo lường nó bằng backtest.

Hãy nhớ 3 nguyên tắc vàng:

  1. Không có hệ thống nào thắng mãi — chỉ có hệ thống quản trị rủi ro tốt mới sống sót.
  2. Backtest trước, tiền thật sau — luôn cảnh giác look-ahead bias cùng overfitting.
  3. Metric là la bàn — Sharpe, Max DD, Profit Factor quyết định hệ thống có đáng đưa vào tiền thật hay không.

Từ Ngày 61, chúng ta sẽ học tối ưu tham số bằng Grid Search và Walk-Forward để tìm bộ tham số tốt nhất mà không rơi vào bẫy overfitting.

Checklist 10 bước trước khi đưa hệ thống vào tiền thật

Dưới đây là checklist thực chiến mà bạn nên in ra và đánh dấu từng bước:

  1. Backtest ≥ 1 năm trên dữ liệu sạch, có trừ spread/commission.
  2. ≥ 50 lệnh — đủ mẫu để kết luận.
  3. Sharpe ≥ 1Max DD ≤ -20%.
  4. Profit Factor ≥ 1.2.
  5. Không có look-ahead bias — tín hiệu luôn dùng giá đã đóng nến.
  6. Không overfit — tham số đơn giản, vùng ổn định rộng.
  7. Walk-Forward (học Ngày 61) out-of-sample vẫn dương.
  8. Chạy demo 2–4 tuần — khớp với backtest trong sai số hợp lý.
  9. Risk 0.5–1% mỗi lệnh, không quá 2%.
  10. Kế hoạch tâm lý — biết trước chuỗi thua tối đa và cách ứng xử.

Chỉ khi đi qua đủ 10 bước này, bạn mới xứng đáng đưa hệ thống vào tiền thật. Bỏ bước nào, bạn tự chịu rủi ro — và thường là rủi ro không đáng có.

📺 Xem video: https://www.youtube.com/shorts/WbZUREfZdgI

?s=120&d=mm&r=g

Hướng Nghiệp Dữ Liệu
3 Bài viết
15.4k Người theo dõi
120k+ Lượt đọc