Bài viết gần đây
Trang chủ → Bài Viết → Tối ưu Tham Số Hệ Thống Giao Dịch Bằng Python Grid Search Và Walk-Forward Chống Overfitting
| Tối ưu Tham Số Hệ Thống Giao Dịch Bằng Python Grid Search Và Walk-Forward Chống Overfitting
Tối Ưu Tham Số Hệ Thống Giao Dịch Bằng Python — Grid Search & Walk-Forward (Chống Overfitting)
Ngày 61 – 2026-08-13 | Chuỗi Python 365 ngày | Hướng Nghiệp Dữ Liệu
📺 Video: https://www.youtube.com/shorts/Jtw4jJD6gnE
Mục lục
- Từ backtest đến tối ưu tham số
- Grid Search là gì?
- Code Grid Search từng bước
- Kết quả thực tế — bảng 16 tổ hợp
- Bẫy overfitting
- Walk-Forward: kiểm tra trên dữ liệu chưa thấy
- Script Ngày 61 hoàn chỉnh
- FAQ
- Kết luận
1. Từ backtest đến tối ưu tham số
Ngày 60, bạn đã dựng được hệ thống Entry + Exit + Risk và đo lường bằng Sharpe, Max Drawdown. Nhưng hệ thống đó có tham số: ngưỡng RSI, hệ số ATR cho stop loss, hệ số cho take profit…
Câu hỏi tự nhiên: bộ tham số nào tốt nhất?
Có 2 cách trả lời:
- Đoán — “RSI 30 với TP 3 lần ATR nghe có vẻ ổn”. Kết quả: không có căn cứ.
- Grid Search — chạy thử mọi tổ hợp tham số, đo Sharpe từng bộ, chọn bộ tốt nhất. Kết quả: có bằng chứng.
Hôm nay chúng ta học cách thứ 2 — và quan trọng hơn, học cách không bị Grid Search lừa (overfitting).
Vì sao tham số lại quan trọng?
Hệ thống Ngày 60 có ít nhất 3 tham số có thể tối ưu:
- Ngưỡng RSI — 35 (long) / 65 (short) có thể đổi thành 30/70 hoặc 40/60.
- Hệ số SL — 2×ATR có thể đổi thành 1.5× hoặc 3×.
- Hệ số TP — 3×ATR có thể đổi thành 2× hoặc 4×.
Mỗi cách đổi cho một kết quả khác nhau. Thay vì đoán, ta để Python chạy thử tất cả và đo lường.
Một lưu ý quan trọng trước khi bắt đầu: đừng nhầm lẫn giữa “có nhiều tham số” và “hệ thống tốt”. Một hệ thống với 2 tham số tối ưu đúng cách và kiểm chứng out-of-sample thường bền hơn một hệ thống 10 tham số “khớp đẹp” với quá khứ. Nguyên tắc KISS (Keep It Simple, Stupid) cực kỳ đúng trong giao dịch định lượng: càng ít tham số, càng ít cách để tự lừa mình.
2. Grid Search là gì?
Grid Search (tìm kiếm lưới) = chạy vòng lặp qua mọi tổ hợp trong một “lưới” tham số:
ngưỡng RSI ∈ {25, 30, 35, 40}
TP (×ATR) ∈ {2.0, 2.5, 3.0, 3.5}
→ tổng 4 × 4 = 16 tổ hợp cần backtest
Với mỗi tổ hợp, ta chạy backtest (tái sử dụng hàm Ngày 60) và ghi lại Sharpe, Win rate, Return, số lệnh. Cuối cùng xếp hạng và chọn bộ có Sharpe cao nhất.
💡 Chi phí duy nhất là thời gian CPU. 16 tổ hợp × vài giây = chưa tới 1 phút. Đây chính là sức mạnh của Python — con người không thể thủ công 16 lần, nhưng máy thì chuyện nhỏ.
Khi nào nên dùng Grid Search?
- Không gian tham số nhỏ (2–3 tham số, vài chục tổ hợp) → Grid Search là tối ưu.
- Không gian lớn (hàng trăm nghìn tổ hợp) → nên dùng Random Search hoặc Bayesian Optimization (sẽ nói ở FAQ).
3. Code Grid Search từng bước
3.1. Tái sử dụng hàm backtest Ngày 60
def backtest(df, rsi_th=35, tp_atr=3.0, sl_atr=2.0, risk=0.01):
eq = 10000.0
pos = None
pnl = []
for row in df.itertuples():
if pos:
if pos["side"] == "long":
if row.low <= pos["sl"]:
pnl.append((pos["sl"] - pos["entry"]) / pos["entry"])
eq += pos["size"] * pnl[-1]; pos = None
elif row.high >= pos["tp"]:
pnl.append((pos["tp"] - pos["entry"]) / pos["entry"])
eq += pos["size"] * pnl[-1]; pos = None
else:
if row.high >= pos["sl"]:
pnl.append((pos["entry"] - pos["sl"]) / pos["entry"])
eq += pos["size"] * pnl[-1]; pos = None
elif row.low <= pos["tp"]:
pnl.append((pos["entry"] - pos["tp"]) / pos["entry"])
eq += pos["size"] * pnl[-1]; pos = None
if pos is None:
long_sig = row.close <= row.lower and row.rsi < rsi_th
short_sig = row.close >= row.upper and row.rsi > (100 - rsi_th)
if long_sig:
entry = row.close
sl, tp = entry - sl_atr * row.atr, entry + tp_atr * row.atr
pos = {"side": "long", "entry": entry, "sl": sl, "tp": tp,
"size": (eq * risk) / ((entry - sl) / entry)}
elif short_sig:
entry = row.close
sl, tp = entry + sl_atr * row.atr, entry - tp_atr * row.atr
pos = {"side": "short", "entry": entry, "sl": sl, "tp": tp,
"size": (eq * risk) / ((sl - entry) / entry)}
return eq, pnl
3.2. Vòng lặp lưới tham số
import pandas as pd
import numpy as np
print("RSI TP*ATR Sharpe Win% Return Trades")
for rsi_th in [25, 30, 35, 40]:
for tp_atr in [2.0, 2.5, 3.0, 3.5]:
eq, pnl = backtest(df, rsi_th=rsi_th, tp_atr=tp_atr)
p = pd.Series(pnl)
if len(p) > 1:
sharpe = round(p.mean() / p.std() * np.sqrt(len(p)), 2)
win = f"{int((p > 0).mean() * 100)}%"
else:
sharpe, win = 0, "-"
print(f"{rsi_th:<4} {tp_atr:<6.1f} {sharpe:<6.2f} {win:<6} "
f"{eq/10000-1:+.1%} {len(p)}")
Lưu ý: khi len(p) <= 1 (quá ít lệnh), Sharpe không có nghĩa — ta đặt 0 và in “-“. Đây là cách tránh tự lừa mình bằng mẫu quá nhỏ.
4. Kết quả thực tế — bảng 16 tổ hợp
Chạy thật trên dữ liệu mean-reverting 2.000 nến, kết quả:
RSI TP*ATR Sharpe Win% Return Trades
----------------------------------------------
25 2.0 -0.10 50% -0.0% 4
25 2.5 0.09 50% +0.5% 4
25 3.0 0.25 50% +1.0% 4
25 3.5 0.37 50% +1.5% 4
30 2.0 3.06 78% +11.5% 19
30 2.5 2.16 68% +10.6% 19
30 3.0 2.56 68% +14.3% 19
30 3.5 2.89 68% +18.0% 19
35 2.0 2.23 64% +14.8% 48
35 2.5 2.29 59% +17.0% 47
35 3.0 2.68 58% +23.5% 46
35 3.5 3.17 58% +32.0% 46
40 2.0 2.41 62% +21.7% 78
40 2.5 2.21 56% +22.2% 74
40 3.0 2.52 54% +29.6% 71
40 3.5 3.33 56% +44.5% 67
Top 3 bộ tham số:
| Hạng | Bộ tham số | Sharpe | Win | Return | Số lệnh |
|---|---|---|---|---|---|
| 1 | RSI=40, TP=3.5×ATR | 3.33 | 56% | +44.5% | 67 |
| 2 | RSI=35, TP=3.5×ATR | 3.17 | 58% | +32.0% | 46 |
| 3 | RSI=30, TP=2.0×ATR | 3.06 | 78% | +11.5% | 19 |
Đọc kết quả thế nào?
- Sharpe cao + nhiều lệnh = đáng tin hơn. Bộ #1 (67 lệnh) đáng tin hơn bộ #3 (chỉ 19 lệnh).
- Xu hướng rõ: TP càng xa (3.5×ATR) → lợi nhuận càng cao nhưng Win% giảm. Đây là trade-off kinh điển giữa lợi nhuận và xác suất thắng.
- RSI=25 gần như không có lệnh (4 lệnh) → mẫu quá nhỏ, không nên dùng để kết luận.
5. Bẫy overfitting
Giờ là phần quan trọng nhất. Khi bạn chạy 16 tổ hợp và chọn bộ Sharpe 3.33, có một cái bẫy:
Bạn đang chọn bộ tham số “khớp đúng” với quá khứ — nhưng quá khứ không lặp lại y hệt.
Nếu bạn thử 1.000 tổ hợp, kiểu gì cũng có 1 bộ Sharpe > 5. Nhưng đó là may mắn ngẫu nhiên, không phải kỹ năng. Đưa bộ đó vào tiền thật → thua.
Dấu hiệu overfitting:
- Sharpe rất cao (> 5) nhưng số lệnh ít (< 20) → nghi ngờ.
- Một tham số thay đổi nhỏ → kết quả tụt thảm hại → không ổn định.
- Tối ưu trên toàn bộ dữ liệu rồi khoe kết quả đó → đây chính là gian lận.
Công thức “độ tin cậy” khi tối ưu
Càng thử nhiều tổ hợp, khả năng tìm thấy “Sharpe đẹp giả” càng cao. Nguyên tắc thực dụng: số lệnh trung bình mỗi tổ hợp càng lớn, độ tin cậy càng cao. Một tổ hợp Sharpe 3.33 từ 67 lệnh đáng tin hơn nhiều so với Sharpe 4 từ 5 lệnh.
6. Walk-Forward: kiểm tra trên dữ liệu chưa thấy
Cách chuẩn để chống overfitting: Walk-Forward Analysis.
Dữ liệu: [===== IN-SAMPLE =====][===== OUT-OF-SAMPLE =====]
Bước 1: Tối ưu tham số trên NỬA ĐẦU (in-sample)
Bước 2: Dùng bộ tham số đó test trên NỬA SAU (out-of-sample)
Bước 3: Nếu out-of-sample vẫn tốt → hệ thống đáng tin
half = len(df) // 2
for name, sub in [("In-sample", df.iloc[:half]), ("Out-of-sample", df.iloc[half:])]:
eq, pnl = backtest(sub, rsi_th=40, tp_atr=3.5) # bộ tham số đã chọn
p = pd.Series(pnl)
sharpe = round(p.mean() / p.std() * np.sqrt(len(p)), 2) if len(p) > 1 else 0
print(f"{name:<14} Sharpe {sharpe}, Return {eq/10000-1:+.1%}, {len(p)} trades")
Kết quả thực tế:
In-sample Sharpe 0.81, Return +3.5%, 9 trades
Out-of-sample Sharpe 3.15, Return +10.4%, 10 trades
✅ Out-of-sample vẫn dương (Sharpe 3.15, +10.4%) → hệ thống chưa bị overfitting, đáng nghiên cứu tiếp. Nếu out-of-sample âm nặng trong khi in-sample đẹp lung linh — đó là overfitting điển hình.
Walk-Forward cuộn (rolling) — phiên bản chuẩn hơn
Chia đôi chỉ là khởi đầu. Bản chuyên nghiệp dùng walk-forward cuộn: chia dữ liệu thành nhiều cửa sổ, mỗi bước tối ưu trên cửa sổ quá khứ rồi test trên cửa sổ kế tiếp, lặp lại. Kết quả cuối là chuỗi nhiều đoạn out-of-sample — đánh giá khách quan hơn nhiều.
def rolling_walk_forward(df, window=800, step=400):
"""Tối ưu trên cửa sổ quá khứ, test trên đoạn kế tiếp, lặp lại."""
results = []
start = 0
while start + window + step <= len(df):
train = df.iloc[start : start + window]
test = df.iloc[start + window : start + window + step]
best = None
for rsi in [25, 30, 35, 40]:
for tp in [2.0, 2.5, 3.0, 3.5]:
eq, pnl = backtest(train, rsi, tp)
p = pd.Series(pnl)
sh = p.mean()/p.std()*np.sqrt(len(p)) if len(p) > 1 else 0
if best is None or sh > best[0]:
best = (sh, rsi, tp)
eq, pnl = backtest(test, best[1], best[2])
p = pd.Series(pnl)
out_sh = p.mean()/p.std()*np.sqrt(len(p)) if len(p) > 1 else 0
results.append({"window": start, "best": best, "out_sharpe": round(out_sh, 2)})
start += step
return results
res = rolling_walk_forward(df)
for r in res:
print(r)
Điểm mạnh của walk-forward cuộn: mỗi đoạn out-of-sample là một bài kiểm tra độc lập. Nếu phần lớn các đoạn đều dương và ổn định, bạn đã có bằng chứng rất mạnh rằng hệ thống khái quát được, không chỉ thuộc bài một đoạn dữ liệu nào đó. Đây là tiêu chuẩn mà nhiều quỹ định lượng dùng trước khi đưa chiến lược vào production.
6.5 Phân tích trade-off: lợi nhuận vs win rate
Nhìn kỹ bảng kết quả Ngày 61, bạn sẽ thấy một mô hình rất quen thuộc: TP càng xa (từ 2 lên 3.5×ATR), lợi nhuận càng tăng nhưng Win% càng giảm. Ví dụ với RSI=40: TP 2.0×ATR cho Return +21.7% với Win 62%; nhưng TP 3.5×ATR cho Return +44.5% với Win chỉ 56%.
Đây chính là trade-off kinh điển giữa lợi nhuận và xác suất thắng — không có bộ tham số nào thắng cả hai. Cách đọc đúng:
- Win rate cao nhưng return thấp → bạn đang chốt lời quá sớm, hệ thống “an toàn” nhưng kém hiệu quả.
- Win rate thấp nhưng return cao → bạn đang để lời chạy, hệ thống chịu được chuỗi thua dài.
- Sharpe cao nhất thường nằm giữa — vì Sharpe đo “lợi nhuận trên mỗi đơn vị rủi ro”, nó tự cân bằng cả hai.
Thay vì chọn theo cảm xúc (“tôi thích thắng nhiều”), hãy chọn theo risk tolerance của bạn: nếu bạn khó chịu với chuỗi 3 lệnh thua liên tiếp, chọn bộ Win cao hơn dù Return thấp hơn. Sự thoải mái tâm lý là một phần của hệ thống.
6.6 Cách chạy Grid Search nhanh và an toàn
Khi không gian tham số lớn dần, vài lưu ý giúp bạn không tự hại mình:
- Luôn giữ số lệnh tối thiểu. Lọc kết quả: chỉ giữ các tổ hợp có ≥ 30 lệnh. Một Sharpe 3 từ 4 lệnh là vô nghĩa.
- Đừng tối ưu quá nhiều tham số cùng lúc. Mỗi tham số thêm vào làm không gian bùng nổ theo cấp số nhân và tăng overfitting. Bắt đầu 2 tham số.
- Tối ưu từng bước (stepwise). Tối ưu tham số A với B cố định, rồi tối ưu B với A đã chọn. Nhanh và ổn định hơn grid đầy đủ.
- Lưu lại toàn bộ kết quả, không chỉ bộ tốt nhất — vì “vùng ổn định” (nhiều tổ hợp lân cận cùng tốt) đáng tin hơn một đỉnh nhọn cô lập.
- Kiểm tra độ nhạy. Thay đổi một tham số 1 bậc, kết quả có sụp không? Nếu sụp → hệ thống mỏng manh, không nên tin.
Một hệ thống tốt không chỉ có Sharpe cao — nó có vùng tham số rộng mà trong đó kết quả vẫn ổn. Đây là dấu hiệu mạnh mẽ của hệ thống thực sự, không phải overfit.
6.7 Ví dụ đọc bảng kết quả như chuyên gia
Giả sử bạn đang có 3 bộ tham số sau từ grid search:
| Bộ | RSI | TP | Sharpe | Win | Return | Lệnh |
|---|---|---|---|---|---|---|
| A | 40 | 3.5 | 3.33 | 56% | +44.5% | 67 |
| B | 30 | 2.0 | 3.06 | 78% | +11.5% | 19 |
| C | 25 | 3.0 | 0.25 | 50% | +1.0% | 4 |
Người mới thường chọn ngay bộ A vì Sharpe cao nhất. Người chuyên nghiệp đọc sâu hơn:
- Bộ B có 78% win nhưng chỉ 19 lệnh — win rate ấn tượng nhưng mẫu nhỏ, dễ bị nhiễu. Không vội kết luận.
- Bộ C chỉ 4 lệnh — hoàn toàn vô nghĩa về thống kê, loại ngay.
- Bộ A có 67 lệnh + Sharpe cao nhất — đáng tin nhất trong ba, nhưng vẫn phải qua bước Walk-Forward.
- So sánh A và B về mặt tâm lý — A chịu chuỗi thua dài hơn (win 56%), B dễ chịu hơn (78%). Bạn chịu được loại nào?
Bài học: kết quả tối ưu không bao giờ là một con số, mà là một bức tranh gồm Sharpe, số lệnh, win rate, drawdown và sự thoải mái tâm lý của chính bạn. Hãy đọc toàn bộ bức tranh, đừng nhìn mỗi con số sáng nhất.
6.8 Công cụ và thư viện hỗ trợ
Ngoài code tay, bạn có thể tận dụng các thư viện Python sẵn có để tăng tốc:
- itertools.product — sinh tự động mọi tổ hợp tham số từ nhiều danh sách (thay vì viết vòng lặp lồng thủ công).
- joblib / multiprocessing — chạy song song 16 tổ hợp trên nhiều lõi CPU, giảm thời gian từ phút xuống giây khi dữ liệu lớn.
- Optuna — thư viện tối ưu thông minh (bayesian + pruning), rất mạnh khi không gian tham số lớn, thay thế Grid Search ở quy mô lớn.
- backtesting.py / vectorbt — framework backtest sẵn có giúp bạn tập trung vào chiến lược thay vì tự viết event loop.
Tuy nhiên, với mục tiêu học hỏi, mình khuyên bạn vẫn nên tự viết code tay trước (như Ngày 60–61) để hiểu chính xác từng bước. Thư viện chỉ nên dùng khi bạn đã hiểu nguyên lý bên trong — nếu không, bạn sẽ không biết mình đang tin vào cái gì.
7. Script Ngày 61 hoàn chỉnh
"""Python 365 Ngay 61: Grid Search + Walk-Forward."""
import numpy as np
import pandas as pd
def backtest(df, rsi_th=35, tp_atr=3.0, sl_atr=2.0, risk=0.01):
eq, pos, pnl = 10000.0, None, []
for row in df.itertuples():
if pos:
if pos["side"] == "long":
if row.low <= pos["sl"]:
pnl.append((pos["sl"] - pos["entry"]) / pos["entry"]); eq += pos["size"] * pnl[-1]; pos = None
elif row.high >= pos["tp"]:
pnl.append((pos["tp"] - pos["entry"]) / pos["entry"]); eq += pos["size"] * pnl[-1]; pos = None
else:
if row.high >= pos["sl"]:
pnl.append((pos["entry"] - pos["sl"]) / pos["entry"]); eq += pos["size"] * pnl[-1]; pos = None
elif row.low <= pos["tp"]:
pnl.append((pos["entry"] - pos["tp"]) / pos["entry"]); eq += pos["size"] * pnl[-1]; pos = None
if pos is None:
if row.close <= row.lower and row.rsi < rsi_th:
sl, tp = row.close - sl_atr * row.atr, row.close + tp_atr * row.atr
pos = {"side": "long", "entry": row.close, "sl": sl, "tp": tp,
"size": (eq * risk) / ((row.close - sl) / row.close)}
elif row.close >= row.upper and row.rsi > (100 - rsi_th):
sl, tp = row.close + sl_atr * row.atr, row.close - tp_atr * row.atr
pos = {"side": "short", "entry": row.close, "sl": sl, "tp": tp,
"size": (eq * risk) / ((sl - row.close) / row.close)}
return eq, pnl
# (tính chỉ báo Bollinger/RSI/ATR như Ngày 60)
best = None
for rsi_th in [25, 30, 35, 40]:
for tp_atr in [2.0, 2.5, 3.0, 3.5]:
eq, pnl = backtest(df, rsi_th, tp_atr)
p = pd.Series(pnl)
sharpe = p.mean() / p.std() * np.sqrt(len(p)) if len(p) > 1 else 0
if best is None or sharpe > best[0]:
best = (sharpe, rsi_th, tp_atr, len(p))
print("Best:", best) # (3.33, 40, 3.5, 67)
# Walk-Forward kiểm chứng
half = len(df) // 2
for name, sub in [("In-sample", df.iloc[:half]), ("Out-of-sample", df.iloc[half:])]:
eq, pnl = backtest(sub, best[1], best[2])
p = pd.Series(pnl)
print(name, "Sharpe", round(p.mean()/p.std()*np.sqrt(len(p)),2) if len(p)>1 else 0,
f"{eq/10000-1:+.1%}", len(p))
8. FAQ
Q: Grid Search khác gì Random Search?
Grid Search thử mọi tổ hợp trong lưới — chậm nhưng chắc chắn. Random Search thử ngẫu nhiên một số tổ hợp — nhanh hơn khi không gian tham số lớn. Với vài chục tổ hợp, Grid Search là đủ.
Q: Bao nhiêu tham số thì nên tối ưu?
Bắt đầu với 2–3 tham số. Mỗi tham số thêm vào làm không gian tổ hợp bùng nổ theo cấp số nhân và tăng nguy cơ overfitting. Ít tham số, dễ hiểu, dễ tin.
Q: Sharpe bao nhiêu trong grid search thì đáng nghi?
Sharpe > 3 mà kèm số lệnh < 20 → nghi ngờ. Luôn xem số lệnh đi kèm Sharpe. Mẫu càng lớn, Sharpe càng đáng tin.
Q: Walk-Forward chia mấy phần?
Tối thiểu chia đôi. Chuẩn hơn là walk-forward cuộn: nhiều cửa sổ liên tiếp, mỗi cửa sổ tối ưu trên quá khứ và test trên đoạn kế tiếp.
Q: Out-of-sample dương nghĩa là chắc chắn lời?
Không. Nó chỉ nghĩa là hệ thống chưa có dấu hiệu overfitting trên mẫu này. Vẫn cần thêm: phí/spread, slippage, nhiều dữ liệu hơn.
Q: Có cách nào ngoài Grid Search không?
Có — Random Search (thử ngẫu nhiên) và Bayesian Optimization (thông minh, học từ kết quả trước). Với 16 tổ hợp thì Grid Search là đủ và dễ giải thích nhất.
Q: Walk-Forward phát hiện overfitting thế nào?
Nếu in-sample Sharpe cao (ví dụ 3.5) nhưng out-of-sample âm (ví dụ -1.2) → hệ thống chỉ “thuộc bài” quá khứ, không khái quát. Ngược lại, cả hai dương và gần nhau → hệ thống ổn định.
Q: Số lệnh bao nhiêu thì đủ tin?
Quy tắc ngón tay cái: ít nhất 30 lệnh cho kết luận sơ bộ, 100+ lệnh cho kết luận đáng tin. Dưới 30 lệnh, Sharpe dễ bị nhiễu ngẫu nhiên.
Q: Tôi có nên tự động hóa toàn bộ quá trình tối ưu không?
Có thể, nhưng đừng để máy “tự chọn bộ tham số” rồi đưa thẳng vào live. Luôn có bước kiểm chứng out-of-sample và đánh giá tâm lý của bạn với kết quả đó.
Q: Nếu không có dữ liệu đủ dài thì sao?
Hãy thành thật với giới hạn của mình. Dữ liệu ngắn → kết luận kém tin cậy → giảm size lệnh khi live. Đừng cố “vẽ” thêm dữ liệu để làm đẹp backtest.
Q: Số lệnh giảm nhiều sau khi lọc, có sao không?
Đó là trade-off bình thường: lọc tốt (ví dụ ADX > 25) làm giảm số lệnh nhưng tăng chất lượng. Miễn là còn ≥ 30 lệnh và Sharpe ổn định, ít lệnh chất lượng vẫn tốt hơn nhiều lệnh nhiễu.
Q: Grid Search trên symbol khác ra kết quả khác, vì sao?
Mỗi symbol có đặc tính riêng về biến động, spread, thanh khoản. Bộ tham số tối ưu trên XAUUSD có thể không tối ưu trên EURUSD. Hãy tối ưu riêng cho từng symbol và ghi rõ trong runbook.
Q: Bao lâu nên tối ưu lại một lần?
Không nên tối ưu liên tục theo cảm xúc. Chu kỳ hợp lý: 1–3 tháng một lần, hoặc khi thị trường có thay đổi cấu trúc lớn (thay đổi chế độ biến động). Tối ưu quá thường xuyên = tự overfit theo nhiễu mới nhất.
Q: Nếu out-of-sample dương nhưng thấp hơn in-sample nhiều thì sao?
Đó là dấu hiệu vừa phải của overfit — hệ thống vẫn có giá trị nhưng mức độ tin cậy giảm. Hãy giảm size lệnh khi live và theo dõi sát. Nếu out-of-sample tiếp tục xấu đi ở các cửa sổ sau, nên quay lại đơn giản hóa hệ thống.
Q: Tôi có cần giữ nguyên bộ tham số khi chuyển sang demo không?
Có. Demo chính là bước kiểm chứng cuối cùng trước live. Nếu bạn đổi tham số ngay khi vào demo, bạn vừa phá hỏng ý nghĩa của bước kiểm chứng này — hãy giữ nguyên và so sánh kết quả thật với backtest.
9. Kết luận
Ngày 61 hoàn thành bộ ba quan trọng nhất của chuỗi: Backtest (60) → Tối ưu tham số (61) → Kiểm chứng Walk-Forward (61).
Ba nguyên tắc vàng:
- Tối ưu có bằng chứng — dùng Grid Search, đừng đoán.
- Chống overfitting — luôn Walk-Forward, đừng khoe kết quả in-sample.
- Sharpe không đứng một mình — phải đi cùng số lệnh và out-of-sample.
Từ Ngày 62, chúng ta sẽ bước sang quản lý danh mục nhiều hệ thống (portfolio of strategies). Bài tập: lấy hệ thống Ngày 60 của bạn, chạy Grid Search trên 2 tham số, rồi Walk-Forward kiểm chứng — ghi lại bộ tham số tốt nhất.
Quy trình 5 bước tối ưu đúng chuẩn
Để chốt lại toàn bộ bài học hôm nay, đây là quy trình 5 bước bạn nên làm theo đúng thứ tự:
- Chạy backtest gốc (Ngày 60) — có một đường cơ sở để so sánh.
- Grid Search 2 tham số — ghi toàn bộ bảng, không chỉ bộ tốt nhất.
- Lọc số lệnh — loại tổ hợp < 30 lệnh.
- Walk-Forward kiểm chứng — chia đôi, rồi nâng cấp lên rolling nếu dữ liệu đủ.
- Đánh giá vùng ổn định — chọn bộ tham số nằm giữa vùng nhiều tổ hợp cùng tốt, thay vì đỉnh cô lập.
Nếu làm đúng 5 bước này, bạn sẽ tránh được 90% cái bẫy tối ưu quá tay mà đa số người tự học mắc phải. Sự khác biệt giữa người nghiệp dư và người chuyên nghiệp không nằm ở việc có chiến lược thần thánh hay không, mà nằm ở quy trình kiểm chứng nghiêm túc.
Cuối cùng, hãy nhớ một câu mà các nhà định lượng thường truyền nhau: “Hệ thống của bạn chỉ tốt như bước kiểm chứng yếu nhất của nó.” Bạn có thể có bộ tham số đẹp, nhưng nếu bỏ qua Walk-Forward, bỏ qua phí, bỏ qua kiểm tra tâm lý — toàn bộ “vẻ đẹp” đó sụp đổ ngay khi gặp thị trường thật. Hôm nay bạn đã có đủ công cụ: Grid Search để tìm, Walk-Forward để kiểm chứng. Phần còn lại là kỷ luật áp dụng đều đặn mỗi ngày. Hãy chạy lại toàn bộ script trên dữ liệu của chính bạn, ghi lại bộ tham số tốt nhất và kết quả walk-forward — đó chính là bài tập kết thúc Ngày 61.
📺 Xem video: https://www.youtube.com/shorts/Jtw4jJD6gnE
Weekly Digest — Nhận Bản Tin Hàng Tuần
Nhận các bài viết phân tích kỹ thuật chuyên sâu, thuật toán giao dịch tự động (Trading Bot) và các giải pháp công nghệ mới nhất từ Hướng Nghiệp Dữ Liệu.