0

Thực Nghiệm Chiến Lược DCA 100 Cổ Phiếu Việt Nam Với Python

Lời Mở Đầu

Trong bài viết này, mục tiêu của mình là sử dụng một phương pháp dễ tiếp cận với đa số mọi người, do đó mình chọn Dollar-Cost Averaging (DCA - Chiến lược bình quân giá) là phương pháp chính. Ngoài ra còn có các phương pháp khác như EDCA (Enhanced Dollar-Cost Averaging), VA (Value Averaging), Pyramiding / Scale-in .... mọi người có thể tìm hiểu thêm hoặc mình sẽ áp dụng cho những bài viết tiếp theo.

Mô hình cụ thể như sau: mình đã xây dựng một pipeline bằng Python với mỗi tuần đầu tư 1.000.000 đồng cho từng mã, thực hiện 56 lần trên một tập dữ liệu gồm 100 cổ phiếu, với rỗ VN30 và 70 mã cổ phiếu khác, với khoản thời gian mô phỏng 1 năm (56 tuần) từ tháng 5/2025 đến tháng 5/2026.

Tóm tắt kết quả

  • Không ngoài mong đợi thì đóng góp lợi nhuận áp đảo thuộc về nhóm VN30 Index, tỷ lệ thắng của nhóm này đạt 60% (18/30 mã có lãi).
  • Đột phá từ nhóm VinGroup: Hai mã trụ cột VIC (ROI +142,25%, lãi +79.660.800 VNĐ) và VHM (ROI +56,13%, lãi +31.433.000 VNĐ)

  • Nhóm Bất Động Sản & KCN gây thiệt hại nặng nhất cho danh mục với 17/18 mã báo lỗ (tỷ lệ thua 94,4%). Các mã giảm thảm hại nhất gồm SZC (-28,72%, -16.082.100 VNĐ), NHA (-28,14%, -15.756.400 VNĐ), SCR (-27,42%, -15.355.800 VNĐ), HDC (-25,30%, -14.168.300 VNĐ) và DIG (-22,79%, -12.761.800 VNĐ).

  • Do đó cũng kéo theo Nhóm Thép & Xây dựng ghi nhận 100% cổ phiếu bị LỖ (11/11 mã)

link báo cáo chi tiết

gồm ba sheet:

DCA_Summary_Dashboard: KPI tổng hợp và kết quả 100 mã, được tô màu theo pool và dấu lãi/lỗ.

Weekly_Prices_VND: 56 cột giá, từ E đến BH; tiếp theo là giá thấp nhất, cao nhất và trung bình.

Weekly_Shares_Bought: lượng cổ phiếu mua từng tuần; BI cộng tổng cổ phiếu và BJ tính giá trị cuối kỳ.

Thiết kế Pipeline

  • DCA với ngân sách 1 triệu đồng/tuần/mã, nếu xét riêng một cổ phiếu, số tiền bỏ vào sau 56 tuần là 56 triệu. Dữ liệu bắt đầu từ thứ Hai 05/05/2025, tuy nhiên data chỉ chứa mã cổ phiếu, sàn và danh sách giá, không chứa ngày giao dịch đi kèm từng giá. Vì vậy, để đơn giản mốc thời gian giao dịch trong blog này được gán theo mỗi tuần.

Công thức

Gọi số tiền mua mỗi tuần là B, giá tại tuần t là P[t]. Số cổ phiếu mô hình mua trong tuần đó là: q[t] = B / P[t]

Sau 56 tuần, số cổ phiếu tích lũy và giá vốn bình quân được xác định bằng:

Q = SUM(q[1], ..., q[56])
Giá vốn DCA = 56.000.000 / Q

Giá trị cuối kỳ được tính bằng tổng cổ phiếu nhân với giá cuối chuỗi:

V = Q × P[56]
Lãi/lỗ = V − 56.000.000
ROI = (V / 56.000.000 − 1) × 100%

Gải thích 1 chút, với khoản mua bằng nhau ở mọi kỳ, giá vốn DCA là trung bình điều hòa của các mức giá và nó khác với trung bình cộng của 56 giá tuần. Trong sheet, cột giá trung bình của bảng giá và cột giá vốn DCA ở dashboard vì thế trả lời hai câu hỏi khác nhau.

Ví dụ minh họa, trong 1 mã bất kỳ nếu hai lần mua đều dùng 1 triệu đồng, lần đầu giá 20.000 đồng và lần sau giá 10.000 đồng, tổng lượng mua là 50 + 100 = 150 cổ phiếu. Giá vốn DCA là khoảng 13.333 đồng, còn trung bình cộng hai mức giá là 15.000 đồng. Nếu giá định giá sau đó vẫn chỉ là 10.000 đồng, khoản đầu tư còn 1,5 triệu đồng trên 2 triệu đồng đã bỏ vào, tức lỗ 25%.

Việc giá vốn giảm không tự động tạo ra lợi nhuận. Trong mô hình này, điều kiện có lãi cuối kỳ là giá cuối cùng cao hơn giá vốn DCA ( tức định giá sau cùng thay vì 10.000 mà chỉ cần > 13.333 là có lãi )

Thu thập dữ liệu giá

  • Dữ liệu giá cổ phiếu Việt Nam trong dự án được lấy từ Yahoo Finance API, đây là API có thể truy cập trực tiếp mà không cần tạo trước API key. Không bị chặn IP/Captcha so với việc cào trực tiếp từ CafeF hay các web chứng khoán trong nước vốn hay đổi cấu trúc HTML hoặc chặn bot

  • Định dạng giá mã cổ phiếu Việt Nam theo json, tất cả các cổ phiếu niêm yết trên các sàn HOSE, HNX và UPCOM đều được Yahoo Finance chuẩn hóa bằng cách thêm hậu tố .VN (Ví dụ: HPG.VN, VCB.VN, VIC.VN, FPT.VN, MWG.VN...).

    https://query1.finance.yahoo.com/v8/finance/chart/{ticker}.VN?interval=1wk&period1=1746403200&period2=1780272000

  • có thể thay thế các biến trong endpoint sau để tùy biến thu thập thông tin

  • interval=1wk : Lấy nến tuần (giá đóng cửa phiên cuối tuần).

  • period1=1746403200: Timestamp bắt đầu (05/05/2025).

  • period2=1780272000: Timestamp kết thúc (01/06/2026).

Function python xử lý

def fetch_stock_klines(ticker):
    # Endpoint truy vấn nến tuần của mã cổ phiếu Việt Nam trên Yahoo Finance
    url = f"https://query1.finance.yahoo.com/v8/finance/chart/{ticker}.VN?interval=1wk&period1={START_TIMESTAMP}&period2={END_TIMESTAMP}"
    req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'})
    try:
        with urllib.request.urlopen(req, timeout=8) as resp:
            data = json.loads(resp.read().decode())
            res = data['chart']['result'][0]
            # Mảng danh sách giá đóng cửa từng tuần
            closes = res['indicators']['quote'][0]['close']
            
            clean_closes = []
            last_valid = None
            for c in closes:
                if c is not None and c > 0:
                    clean_closes.append(float(c))
                    last_valid = float(c)
                elif last_valid is not None:
                    clean_closes.append(last_valid)
            
            if len(clean_closes) >= TOTAL_WEEKS:
                return clean_closes[:TOTAL_WEEKS]
    except Exception as e:
        return None
    return None


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.