Cỡ vị thế cố định bỏ qua điều kiện thị trường. Nhắm mục tiêu biến động điều chỉnh lượng vốn bạn đưa vào theo mức biến động hiện tại của thị trường.
Cùng một chiến lược: trong mấy tháng thị trường yên ắng thì chán tới mức bạn muốn tắt nó đi; rồi đến tuần thị trường bắt đầu nhảy dữ dội, drawdown sâu hơn hẳn mức trung bình mà backtest gợi ra. Tín hiệu không đổi, thứ đổi là thị trường — còn cỡ mỗi lần bạn đặt cược thì vẫn y nguyên.
Nhắm mục tiêu biến động là cho cỡ vị thế chuyển động nghịch với biến động thực hiện của thị trường: biến động cao hơn mục tiêu thì vào ít hơn, thấp hơn mục tiêu thì vào nhiều hơn — nhưng có trần.
Một chiến lược Type A cơ bản xuất ra tín hiệu 1.0 (long) hoặc 0.0 (đứng ngoài). Runner đưa toàn bộ vốn được phân bổ vào khi tín hiệu là 1.0. Cách này chạy được, nhưng có một vấn đề ngầm: chiến lược đặt cược cùng một cỡ bất kể thị trường yên ắng hay hỗn loạn.
Trong giai đoạn biến động thấp, một nhịp 1% mỗi ngày là lớn. Trong giai đoạn biến động cao, nhịp 1% chỉ là nhiễu. Nếu chiến lược vào lệnh với cùng cỡ vị thế ở cả hai môi trường, rủi ro tính bằng tiền thật của bạn dao động rất mạnh — và drawdown trong các giai đoạn biến động sẽ lớn hơn nhiều so với mức trung bình mà backtest gợi ra.
Blave Agent triển khai nhắm mục tiêu biến động qua hai bước:
log_return = log(Close / Close.shift(1)) realized_vol = log_return.rolling(lookback).std() * sqrt(periods_per_year)
Phép tính này lấy độ lệch chuẩn trượt của log return, rồi quy về năm bằng cách nhân với sqrt(periods_per_year). Kết quả là biến động thực hiện quy năm — cùng đơn vị với biến động dùng trong tỷ lệ Sharpe.
Mặc định: lookback=720 nến, periods_per_year=8760. Các giá trị này được hiệu chỉnh cho chiến lược khung 1h: 720 nến = 30 ngày × 24 giờ; 8760 = 24 × 365.
scale = (target_vol / realized_vol).clip(upper=vol_cap) scaled_signal = signal * scale
Tín hiệu (1.0 = long toàn phần) được nhân với tỷ lệ biến động, giới hạn ở vol_cap. Kết quả là một cỡ vị thế dạng phân số.
Mặc định: target_vol=0.30 (30% quy năm), vol_cap=2.0 (đòn bẩy tối đa 2×).
| Biến động thực hiện (quy năm) | Biến động mục tiêu | Hệ số thô | Sau trần | Cỡ vị thế |
|---|---|---|---|---|
| 10% | 30% | 3.0× | 2.0× (chạm trần) | 200% vốn phân bổ |
| 20% | 30% | 1.5× | 1.5× | 150% vốn phân bổ |
| 30% | 30% | 1.0× | 1.0× | 100% — mức chuẩn |
| 60% | 30% | 0.5× | 0.5× | 50% vốn phân bổ |
| 90% | 30% | 0.33× | 0.33× | 33% vốn phân bổ |
Ở mức biến động thực hiện 30%, chiến lược chạy toàn cỡ. Trong một đợt khủng hoảng khi biến động thực hiện vọt lên 90%, chiến lược tự động thu vị thế xuống còn một phần ba. Trong giai đoạn yên ắng ở mức biến động 10%, nó tăng lên tới trần 2×.
Các giá trị mặc định (lookback=720, periods_per_year=8760) dành cho chiến lược 1h. Với những khung khác, cả hai giá trị đều phải tính lại:
| Khung | periods_per_year | lookback (cửa sổ 30 ngày) |
|---|---|---|
5min | 105,120 (12 × 24 × 365) | 8,640 nến |
1h (mặc định) | 8,760 | 720 nến |
4h | 2,190 | 180 nến |
8h | 1,095 | 90 nến |
1d | 365 | 30 nến |
VOL_TARGETING = True
TARGET_VOL = 0.30 # mục tiêu biến động quy năm 30%
VOL_CAP = 2.0 # đòn bẩy tối đa 2×
VOL_LOOKBACK = 720 # số nến trong cửa sổ lookback
PERIODS_PER_YEAR = 8760 # cho khung 1h
def fetch_data(hdrs):
from lib.data import fetch_kline
from lib.strategy import add_realized_vol
df = fetch_kline(SYMBOL, INTERVAL, START, END, hdrs)
if VOL_TARGETING:
add_realized_vol(df, VOL_LOOKBACK, PERIODS_PER_YEAR)
return df
def compute_signals(df):
from lib.strategy import apply_vol_scaling
signal = pd.Series(np.nan, index=df.index)
# ... logic tín hiệu của bạn ở đây ...
if VOL_TARGETING:
return apply_vol_scaling(signal, df, TARGET_VOL, VOL_CAP)
return signal
| Tham số | Mặc định | Nghĩa là gì | Khi nào phải đổi |
|---|---|---|---|
TARGET_VOL | 0.30 | mục tiêu biến động quy năm 30% | Đặt theo MDD bạn chịu được ÷ 2 |
VOL_CAP | 2.0 | Đòn bẩy tối đa 2 lần | Hoàn toàn không muốn đòn bẩy thì đặt 1.0 |
VOL_LOOKBACK | 720 | Độ dài cửa sổ ước lượng biến động (số nến) | Đổi khung là phải tính lại, xem bảng phía trên |
PERIODS_PER_YEAR | 8760 | Số kỳ mỗi năm dùng để quy năm (khung 1h) | Đổi khung là phải tính lại, xem bảng phía trên |
Lưu ý rằng add_realized_vol phải được gọi trong fetch_data (trước khi tính tín hiệu), vì apply_vol_scaling đọc df['realized_vol'], cột do hàm đầu tiên ghi trực tiếp vào df.
Mức trần tồn tại vì nhắm mục tiêu biến động có thể tạo ra đòn bẩy cực đoan trong môi trường biến động thấp kéo dài. Nếu biến động thực hiện rơi xuống 5%, hệ số không giới hạn sẽ là 6× — gấp ba lần vốn phân bổ. Chỉ một nhịp bất lợi trong 1 ngày cũng có thể gây drawdown thảm khốc, ngay cả khi tín hiệu về cơ bản là đúng.
Trần mặc định 2× giới hạn chiến lược chỉ đưa vào tối đa gấp đôi vốn được phân bổ. Với những chiến lược bạn muốn hoàn toàn không dùng đòn bẩy, hãy đặt vol_cap=1.0. Khi đó nhắm mục tiêu biến động trở thành việc định cỡ vị thế thuần theo biến động, không đòn bẩy — cỡ vị thế chỉ co lại khi biến động cao, không bao giờ nở quá mức toàn phần.
Một quy tắc ước lượng hữu ích: target_vol ≈ MDD chấp nhận được ÷ 2. Nó đến từ quan hệ thực nghiệm giữa biến động quy năm và MDD trong các chiến lược theo xu hướng.
| target_vol | MDD điển hình hàm ý | Trường hợp dùng |
|---|---|---|
| 10% | ~−20% | Thận trọng, ưu tiên bảo toàn vốn |
| 20% | ~−40% | Trung tính |
| 30% (mặc định) | ~−60% | Mạnh tay — dùng hết ngân sách rủi ro |
periods_per_year có khớp với khung của bạn không? Mặc định 8760 là dành cho 1h. Với 5min là 105,120, 4h là 2,190, 8h là 1,095, 1d là 365. Điền sai thì bước quy năm sai theo, và mọi ô phía sau đều sai.
lookback vẫn là 30 ngày chứ? 720 nến = 1h × 30 ngày. Đổi khung thì phải đổi theo: 5min là 8,640 nến, 4h là 180, 8h là 90, 1d là 30. Trên khung ngày mà chỉ 30 nến thì hơi ngắn — có thể cân nhắc 252 nến (một năm) để có ước lượng ổn định hơn.
target_vol có khớp với MDD bạn chịu được không? Quy tắc ước lượng là target_vol ≈ MDD chấp nhận được ÷ 2: 10% ứng với khoảng −20%, 20% ứng với khoảng −40%, còn mức mặc định 30% ứng với khoảng −60%. (Cách đọc MDD, xem Cách đọc kết quả backtest.)
vol_cap cho bạn mức đòn bẩy bạn muốn chứ? Mặc định 2.0 nghĩa là dùng tối đa gấp đôi vốn được phân bổ. Đặt 1.0 thì nó chỉ thu vị thế khi biến động cao, không bao giờ phóng quá mức toàn phần khi biến động thấp.
TARGET_VOL của từng chiến lược có nhất quán với target_vol_pct của portfolio Manager không? Manager quyết định đòn bẩy tổng thể bằng đúng logic đó (đòn bẩy = target_vol ÷ biến động thực hiện của danh mục). Hai bên không khớp thì ghép nhiều chiến lược lại sẽ đếm trùng rủi ro.
lookback nến gần nhất: cửa sổ càng dài thì ước lượng càng ổn định và càng chậm phản ứng, cửa sổ càng ngắn thì càng dễ phản ứng thái quá với một cú vọt ngắn hạn. Cả hai bên đều không cho bạn biết trước rằng biến động sắp nhảy.