| Tính Dải Bollinger Bands Bằng Python – Middle Band, Upper/Lower, %B, BandWidth

Được viết bởi Đặng Trí Thanh vào ngày 12/08/2026 lúc 22:54 | 43 lượt xem

Mục lục

1. Bollinger Bands là gì?

2. Công thức Bollinger — từng bước

3. Code Bollinger bằng Python thuần

4. Code Bollinger bằng pandas — vectorized

5. Dùng pandas_ta — 1 dòng là xong

6. Tín hiệu: Squeeze & Bounce

7. %B và BandWidth — 2 thước đo nâng cao

8. Kết hợp Bollinger + RSI + MACD + MA

9. Script Ngày 59 hoàn chỉnh

10. FAQ

11. Kết luận


1. Bollinger Bands là gì?

Bollinger Bands (Dải Bollinger) do John Bollinger phát triển đầu thập niên 1980, là một trong những chỉ báo biến động (volatility) phổ biến nhất thế giới. Thay vì đo xu hướng như MA hay đo sức mạnh như RSI, Bollinger đo độ rộng của biến động giá — giúp bạn biết lúc nào thị trường “yên ắng” (sideway hẹp) và lúc nào “sôi động” (trend mạnh).

Nếu MA trả lời “giá đang đi đâu?”, RSI trả lời “giá đang mạnh hay yếu?”, MACD trả lời “động lượng đổi chiều khi nào?”, thì Bollinger trả lời: “giá đang biến động nhiều hay ít — và nằm ở đâu trong vùng đó?”

Bollinger gồm 3 đường:

Middle Band = SMA(20)                  ← đường giữa (trung bình 20 phiên)
Upper Band  = SMA(20) + 2 × StdDev(20) ← dải trên
Lower Band  = SMA(20) - 2 × StdDev(20) ← dải dưới

Trực quan hóa:

  ↑ Giá
  │         ╱╲
  │        ╱  ╲    ╱╲
  │  ──── ╱────╲──╱──╲──────  Upper Band (+2σ)
  │      ╱      ╲╱    ╲
  │  ────────────────╱──────  Middle Band (SMA20)
  │                 ╱
  │  ──────────────╱────────  Lower Band (-2σ)
  │
  └─────────────────────────→ thời gian

  Khi dải HẸP lại (Squeeze) → sắp có biến động LỚN
  Khi dải MỞ RỘNG (Expansion) → đang có trend mạnh

Ý tưởng cốt lõi: khoảng 95% giá nằm trong dải ±2 độ lệch chuẩn (theo phân phối chuẩn). Giá chạm dải trên → có thể quá mua tạm thời; giá chạm dải dưới → có thể quá bán tạm thời. Quan trọng nhất: dải co hẹp là dấu hiệu báo trước một cú bùng nổ biến động.


2. Công thức Bollinger — từng bước

Để code chính xác, ta cần hiểu 4 thành phần toán học:

2.1. Middle Band (đường giữa)

trung bình động đơn giản 20 phiên — giống Ngày 48 đã học:

$$text{Middle}_t = frac{1}{20}sum_{i=0}^{19} text{Close}_{t-i}$$

2.2. Độ lệch chuẩn (Standard Deviation)

Đo mức độ “tản mác” của giá quanh trung bình:

$$sigma_t = sqrt{frac{1}{20}sum_{i=0}^{19}(text{Close}_{t-i} – text{Middle}_t)^2}$$

2.3. Upper & Lower Band

Cộng/trừ 2 lần độ lệch chuẩn:

$$text{Upper}_t = text{Middle}_t + 2sigma_t$$

$$text{Lower}_t = text{Middle}_t – 2sigma_t$$

2.4. %B và BandWidth (2 chỉ số phái sinh)

%B cho biết vị trí của giá trong dải (0 = chạm dưới, 1 = chạm trên):

$$%B_t = frac{text{Close}_t – text{Lower}_t}{text{Upper}_t – text{Lower}_t}$$

BandWidth đo độ rộng của dải (thấp = squeeze):

$$text{BandWidth}_t = frac{text{Upper}_t – text{Lower}_t}{text{Middle}_t}$$

Hai chỉ số này là “vũ khí bí mật” để phát hiện Squeeze và đo sức mạnh trend mà nhiều người mới bỏ qua.


3. Code Bollinger bằng Python thuần

Bắt đầu từ con số 0 — chỉ dùng Python chuẩn (không pandas, không numpy) để hiểu bản chất:

import csv
from math import sqrt

def read_closes(path):
    """Đọc cột Close từ file CSV (dạng date,open,high,low,close,volume)."""
    closes = []
    with open(path, encoding="utf-8") as f:
        reader = csv.DictReader(f)
        for row in reader:
            closes.append(float(row["close"]))
    return closes

def sma(values, period):
    """Trung bình động đơn giản."""
    if len(values) < period:
        return None
    return sum(values[-period:]) / period

def std(values, period):
    """Độ lệch chuẩn mẫu (sample) của period phần tử cuối."""
    if len(values) < period:
        return None
    window = values[-period:]
    mean = sum(window) / period
    variance = sum((x - mean) ** 2 for x in window) / period
    return sqrt(variance)

def bollinger(closes, period=20, num_std=2.0):
    """Trả về 4 list: middle, upper, lower, bandwidth."""
    middle, upper, lower, bw = [], [], [], []
    for i in range(len(closes)):
        window = closes[: i + 1]
        m = sma(window, period)
        s = std(window, period)
        if m is None or s is None:
            middle.append(None); upper.append(None)
            lower.append(None); bw.append(None)
        else:
            u = m + num_std * s
            l = m - num_std * s
            middle.append(m); upper.append(u); lower.append(l)
            bw.append((u - l) / m)
    return middle, upper, lower, bw

# Sử dụng
closes = read_closes("xauusd_h1.csv")
middle, upper, lower, bw = bollinger(closes)

for i in range(-5, 0):  # 5 phiên cuối
    print(f"Close={closes[i]:.2f} Middle={middle[i]:.2f} "
          f"Upper={upper[i]:.2f} Lower={lower[i]:.2f} BW={bw[i]:.4f}")

Giải thích từng bước

1. sma cộng period giá cuối rồi chia — giống code Ngày 48.

2. std tính trung bình bình phương độ lệch rồi lấy căn — đây là “độ lệch chuẩn dân số” (chia cho N). Một số thư viện dùng “độ lệch chuẩn mẫu” (chia N-1) — khác biệt nhỏ khi N=20.

3. bollinger lặp qua từng mốc thời gian, dùng dữ liệu tính đến thời điểm đó (không nhìn tương lai — tránh look-ahead bias).

⚠️ Lưu ý quan trọng: Khi tự code, luôn dùng `closes[:i+1]` chứ không phải `closes` đầy đủ — nếu không bạn vô tình dùng dữ liệu tương lai, kết quả backtest sẽ “đẹp giả tạo”.


4. Code Bollinger bằng pandas — vectorized

Code thuần chạy chậm vì vòng lặp Python. Với 100.000 nến, hãy dùng pandas:

import pandas as pd

# Đọc dữ liệu
df = pd.read_csv("xauusd_h1.csv", parse_dates=["date"])
df = df.sort_values("date").reset_index(drop=True)

# Bollinger Bands vectorized — 3 dòng
df["middle"] = df["close"].rolling(20).mean()
df["std"] = df["close"].rolling(20).std(ddof=0)   # ddof=0: chia N (dân số)
df["upper"] = df["middle"] + 2 * df["std"]
df["lower"] = df["middle"] - 2 * df["std"]

# %B và BandWidth
df["pct_b"] = (df["close"] - df["lower"]) / (df["upper"] - df["lower"])
df["bandwidth"] = (df["upper"] - df["lower"]) / df["middle"]

# Xem 5 dòng cuối
cols = ["date", "close", "middle", "upper", "lower", "pct_b", "bandwidth"]
print(df[cols].tail())

Lưu ý ddof

Tham số Công thức Dùng khi
`ddof=0` chia cho N khớp định nghĩa gốc của Bollinger
`ddof=1` (mặc định) chia cho N-1 ước lượng mẫu thống kê

John Bollinger dùng chia cho N (population std). Nếu bạn để mặc định ddof=1, dải sẽ hơi rộng hơn một chút — không sai, nhưng không khớp chart chuẩn.

Vẽ nhanh bằng matplotlib

import matplotlib.pyplot as plt

last = df.tail(200)  # 200 nến gần nhất
plt.figure(figsize=(14, 7))
plt.plot(last["date"], last["close"], label="Close", color="black", lw=1)
plt.plot(last["date"], last["middle"], label="Middle (SMA20)", color="blue", lw=1)
plt.plot(last["date"], last["upper"], label="Upper (+2σ)", color="red", lw=1, ls="--")
plt.plot(last["date"], last["lower"], label="Lower (-2σ)", color="green", lw=1, ls="--")
plt.fill_between(last["date"], last["upper"], last["lower"], alpha=0.15, color="blue")
plt.legend()
plt.title("Bollinger Bands — XAU/USD H1")
plt.tight_layout()
plt.show()


5. Dùng pandas_ta — 1 dòng là xong

Nếu chỉ cần kết quả nhanh, pandas_ta làm hết trong 1 dòng:

import pandas as pd
import pandas_ta as ta

df = pd.read_csv("xauusd_h1.csv")

# 1 dòng tính Bollinger Bands (kèm bandwidth và %B)
df.ta.bbands(close=df["close"], length=20, std=2, append=True)

# Kết quả tự thêm các cột:
# BBU_20_2.0  (Upper)
# BBM_20_2.0  (Middle)
# BBL_20_2.0  (Lower)
# BBB_20_2.0  (Bandwidth)
# BBP_20_2.0  (%B)

print(df[["close", "BBU_20_2.0", "BBM_20_2.0", "BBL_20_2.0", "BBP_20_2.0"]].tail())

Đối chiếu code tay vs pandas_ta

Tiêu chí Code tay pandas pandas_ta
Số dòng ~30 ~8 1
Tốc độ (100k nến) chậm (vòng lặp) nhanh nhanh
Hiểu bản chất ✅ sâu nhất
Kiểm soát ddof cấu hình sẵn

Khuyến nghị: viết code tay một lần để hiểu, rồi dùng pandas_ta cho công việc thực tế.


6. Tín hiệu: Squeeze & Bounce

Bollinger cho 2 nhóm tín hiệu chính:

6.1. Bollinger Squeeze (bóp dải) — báo hiệu bùng nổ

Khi BandWidth thu hẹp xuống mức thấp nhất trong N phiên, thị trường đang “tích nén” năng lượng — sắp có cú phá vỡ mạnh:

def detect_squeeze(bandwidth, lookback=20):
    """True khi bandwidth hiện tại là thấp nhất trong lookback phiên."""
    signals = []
    for i in range(len(bandwidth)):
        if i < lookback:
            signals.append(False)
            continue
        window = bandwidth[i - lookback : i + 1]
        signals.append(bandwidth[i] <= min(window))
    return signals

df["squeeze"] = detect_squeeze(df["bandwidth"].tolist())

Cách trade Squeeze: khi phát hiện squeeze, đặt lệnh chờ 2 chiều (buy stop trên đỉnh, sell stop dưới đáy vùng tích lũy) — giá phá hướng nào thì đi theo hướng đó.

6.2. Bollinger Bounce (nảy từ dải)

Trong thị trường sideway (không trend), giá thường nảy ngược khi chạm dải trên/dưới:

df["touch_upper"] = df["close"] >= df["upper"]
df["touch_lower"] = df["close"] <= df["lower"]

# Tín hiệu nảy: chạm dưới → mua kỳ vọng về giữa; chạm trên → bán
df["bounce_buy"] = df["touch_lower"] & df["close"].shift(1) > df["lower"].shift(1)
df["bounce_sell"] = df["touch_upper"] & df["close"].shift(1) < df["upper"].shift(1)

⚠️ Cảnh báo quan trọng: Bounce chỉ đúng khi KHÔNG có trend. Trong trend mạnh, giá “cưỡi” dải (walk the band) — chạm dải rồi đi tiếp, không nảy. Luôn kết hợp với chỉ báo trend (MA200, ADX) để phân loại chế độ thị trường trước.

6.3. Walk the Band (cưỡi dải) — tín hiệu trend mạnh

Khi %B > 1 liên tục nhiều phiên (giá bám trên dải trên), đó là trend tăng mạnh — đừng bán ngược:

df["walk_up"] = (df["pct_b"] > 1).rolling(5).sum() >= 3  # ≥3/5 phiên trên dải
df["walk_down"] = (df["pct_b"] < 0).rolling(5).sum() >= 3


7. %B và BandWidth — 2 thước đo nâng cao

7.1. %B — vị trí giá trong dải

%B Ý nghĩa
> 1 Giá trên dải trên (mạnh/breakout)
0.8 – 1.0 Gần dải trên (áp lực)
0.2 – 0.8 Vùng trung lập
0 – 0.2 Gần dải dưới (hỗ trợ)
< 0 Giá dưới dải dưới (yếu/breakdown)

7.2. BandWidth — đo độ co giãn

# Phân vị bandwidth hiện tại so với 6 tháng
rolling_pct = df["bandwidth"].rolling(250).rank(pct=True)
df["bw_percentile"] = rolling_pct

# Squeeze mạnh: bandwidth dưới phân vị 10%
df["strong_squeeze"] = df["bw_percentile"] < 0.10

Bảng diễn giải BandWidth percentile:

Phân vị Trạng thái Hành động
< 10% Squeeze cực mạnh Chuẩn bị breakout
10–30% Dải hẹp Theo dõi sát
30–70% Bình thường Trade theo chiến lược
70–90% Dải rộng Trend đang mạnh
> 90% Dải cực rộng Cảnh báo đảo chiều


8. Kết hợp Bollinger + RSI + MACD + MA

Bộ 4 chỉ báo (MA trend, RSI sức mạnh, MACD động lượng, Bollinger biến động) là “hệ điều hành” hoàn chỉnh:

8.1. Chiến lược ví dụ: Bollinger Bounce có lọc trend

# Điều kiện BUY (bounce từ dải dưới, chỉ khi trend tăng)
cond_trend_up = df["close"] > df["close"].rolling(200).mean()  # trên MA200
cond_rsi_ok = (df["rsi14"] > 30) & (df["rsi14"] < 70)          # không quá bán/mua
cond_bounce = df["bounce_buy"]                                  # nảy từ lower

df["signal_buy"] = cond_trend_up & cond_rsi_ok & cond_bounce

# Điều kiện SELL (bounce từ dải trên, chỉ khi trend giảm)
cond_trend_down = df["close"] < df["close"].rolling(200).mean()
df["signal_sell"] = cond_trend_down & cond_rsi_ok & df["bounce_sell"]

8.2. Luồng quyết định

Dữ liệu giá
    │
    ├─ MA200 → Trend UP / DOWN / SIDEWAY?
    │
    ├─ Nếu SIDEWAY: dùng Bollinger Bounce (%B > 1 bán, < 0 mua)
    │
    ├─ Nếu TREND: chỉ theo hướng trend
    │     ├─ MACD xác nhận động lượng cùng hướng
    │     └─ Bollinger walk-the-band xác nhận trend mạnh
    │
    └─ RSI: lọc vùng quá mua/quá bán tránh đuổi giá

8.3. Bảng tổng hợp 4 chỉ báo

Chỉ báo Câu hỏi trả lời Ngày học
MA (SMA/EMA) Xu hướng đi đâu? Ngày 48
RSI Mạnh hay yếu? Quá mua/bán? Ngày 57
MACD Động lượng đổi chiều? Ngày 58
Bollinger Biến động nhiều hay ít? Ngày 59


9. Script Ngày 59 hoàn chỉnh

Dưới đây là script hoàn chỉnh, tích hợp tất cả:

"""
Python 365 — Ngày 59: Bollinger Bands đầy đủ
Tính Middle/Upper/Lower + %B + BandWidth + Squeeze + Bounce
"""
import pandas as pd
import numpy as np

def compute_bollinger(df, period=20, num_std=2.0):
    """Tính toàn bộ Bollinger Bands + chỉ số phái sinh."""
    df = df.copy()
    df["middle"] = df["close"].rolling(period).mean()
    df["std"] = df["close"].rolling(period).std(ddof=0)
    df["upper"] = df["middle"] + num_std * df["std"]
    df["lower"] = df["middle"] - num_std * df["std"]

    # %B và BandWidth
    df["pct_b"] = (df["close"] - df["lower"]) / (df["upper"] - df["lower"])
    df["bandwidth"] = (df["upper"] - df["lower"]) / df["middle"]

    # Squeeze: bandwidth thấp nhất trong 20 phiên
    df["squeeze"] = df["bandwidth"].rolling(20).min() == df["bandwidth"]

    # Bounce: chạm dải rồi nảy
    df["touch_upper"] = df["close"] >= df["upper"]
    df["touch_lower"] = df["close"] <= df["lower"]
    df["bounce_sell"] = df["touch_upper"] & (df["close"].shift(1) < df["upper"].shift(1))
    df["bounce_buy"] = df["touch_lower"] & (df["close"].shift(1) > df["lower"].shift(1))

    # Walk the band: %B ngoài dải ≥3/5 phiên
    df["walk_up"] = (df["pct_b"] > 1).rolling(5).sum() >= 3
    df["walk_down"] = (df["pct_b"] < 0).rolling(5).sum() >= 3

    return df


# ===== Chạy thử =====
if __name__ == "__main__":
    # Tạo dữ liệu mẫu (nếu chưa có file CSV)
    np.random.seed(42)
    n = 500
    dates = pd.date_range("2025-01-01", periods=n, freq="h")
    # Giá mô phỏng: random walk + vài cú trend
    ret = np.random.normal(0, 0.001, n)
    ret[200:300] += 0.002  # trend tăng
    close = 2000 * np.exp(np.cumsum(ret))
    df = pd.DataFrame({"date": dates, "close": close})

    df = compute_bollinger(df)

    # Thống kê tín hiệu
    print("=== Bollinger Bands Ngày 59 ===")
    print(f"Số nến: {len(df)}")
    print(f"Squeeze phát hiện: {df['squeeze'].sum()} lần")
    print(f"Bounce buy: {df['bounce_buy'].sum()} | Bounce sell: {df['bounce_sell'].sum()}")
    print(f"Walk up: {df['walk_up'].sum()} | Walk down: {df['walk_down'].sum()}")
    print("n5 nến cuối:")
    cols = ["date", "close", "upper", "lower", "pct_b", "bandwidth"]
    print(df[cols].tail().to_string(index=False))

Kết quả mẫu (minh họa)

=== Bollinger Bands Ngày 59 ===
Số nến: 500
Squeeze phát hiện: 9 lần
Bounce buy: 23 | Bounce sell: 21
Walk up: 61 | Walk down: 14

5 nến cuối:
date                close    upper    lower    pct_b   bandwidth
2025-01-21 21:00   2534.81  2548.12  2467.35  0.8351    0.0319
2025-01-21 22:00   2541.20  2551.03  2469.11  0.8799    0.0322
2025-01-21 23:00   2549.44  2553.98  2470.82  0.9455    0.0326
2025-01-22 00:00   2557.03  2557.15  2472.63  1.0000    0.0331
2025-01-22 01:00   2563.11  2560.11  2474.29  1.0345    0.0335


10. FAQ

Hỏi: Bollinger Bands có dự đoán được giá không?

→ Không. Bollinger đo biến động, không dự đoán hướng. Nó cho biết giá “đang ở đâu so với vùng biến động” và “biến động đang co hay giãn” — từ đó bạn tự ra quyết định.

Hỏi: Tại sao dùng SMA20 mà không phải EMA20?

→ John Bollinger chọn SMA20 vì đơn giản và ổn định. 20 phiên ≈ 1 tháng giao dịch. Bạn có thể thử EMA20 nhưng kết quả gần như tương đương — quan trọng là nhất quán.

Hỏi: Nên dùng 2 độ lệch chuẩn hay 1.5 hoặc 3?

→ 2σ là chuẩn (chứa ~95% giá). 1.5σ cho dải hẹp hơn (chạm thường xuyên hơn, nhiều tín hiệu nhưng nhiều nhiễu). 3σ cho dải rộng (ít tín hiệu, độ tin cậy cao). Bắt đầu với 2σ.

Hỏi: Squeeze phát hiện rồi nhưng giá không bùng nổ thì sao?

→ Squeeze báo hiệu “khả năng” bùng nổ, không đảm bảo. Luôn chờ xác nhận: nến phá vỡ vùng tích lũy với volume tăng rồi mới vào lệnh.

Hỏi: Bollinger có dùng được cho crypto không?

→ Có, Bollinger hoạt động tốt trên mọi thị trường có dữ liệu giá: forex, crypto, chứng khoán. Riêng crypto biến động mạnh hơn nên thời gian squeeze thường ngắn hơn.

Hỏi: Bounce và Walk-the-band mâu thuẫn nhau, nghe theo cái nào?

→ Phân biệt bằng TREND: sideway → dùng Bounce; trend mạnh → dùng Walk-the-band (đi theo trend). Đây chính là lý do cần kết hợp MA200 hoặc ADX để phân loại.


11. Kết luận

Bollinger Bands là mảnh ghép thứ 4 — hoàn thiện bộ chỉ báo nền tảng:

MA (Ngày 48) → xu hướng

RSI (Ngày 57) → sức mạnh

MACD (Ngày 58) → động lượng

Bollinger (Ngày 59) → biến động

Hôm nay bạn đã học được:

1. Công thức toán đầy đủ: SMA20, độ lệch chuẩn, ±2σ.

2. Code Python thuần (hiểu bản chất) và vectorized pandas (tốc độ).

3. Hai chỉ số phái sinh quan trọng: %BBandWidth.

4. Ba tín hiệu thực chiến: Squeeze, Bounce, Walk-the-band.

5. Cách kết hợp bộ 4 chỉ báo thành hệ thống giao dịch hoàn chỉnh.

Bài tập về nhà Ngày 59:

1. Tải dữ liệu XAU/USD H1, tự code Bollinger Bands từ số 0.

2. Vẽ biểu đồ với 3 dải và tô màu vùng giữa.

3. Đếm số lần Squeeze xuất hiện trong 1 năm — kiểm tra sau mỗi squeeze giá bùng nổ bao nhiêu pip.

4. Kết hợp Bollinger Bounce + lọc MA200, backtest 2 năm và tính win rate.

Ngày mai — Ngày 60, chúng ta sẽ tổng kết bộ chỉ báo và bắt đầu học Backtest hệ thống giao dịch hoàn chỉnh (Entry + Exit + Risk Management) — bước tiến lớn nhất của chuỗi!

📺 Video: (cập nhật sau khi upload)

🌐 Website: https://www.huongnghiepdulieu.com/python365-ngay-59/

Đặng Trí Thanh

Đặng Trí Thanh

Giám đốc Công nghệ · DNT Digital · Giảng viên HNDL
852 Bài viết
15.4k Người theo dõi
120k+ Lượt đọc

Đặng Trí Thanh — Founder & CTO · Hướng Nghiệp Dữ Liệu - DNT Digital. Chuyên đào tạo và triển khai thực chiến Python, MT5 và hệ thống bot auto trading / IB cho học viên và doanh nghiệp.