Vnstock Logo

Luật thị trường, phí và sự kiện

Cộng đồng

Mục lục

Trang dùng chung với v0.5.0

Nội dung trang này giữ nguyên từ bản v0.5.0, chưa viết riêng cho free: ví dụ chạy được nếu tên hàm không đổi. Bản gốc: /docs/vnstock_quant/backtest/luat-thi-truong-va-phi.

Tính năng đang thử nghiệm

Tính năng trên trang này đang trong giai đoạn phát triển thử nghiệm và chưa ổn định. Bạn dùng được để tìm hiểu và chạy thử, nhưng tên hàm hay công cụ, tham số và cấu trúc kết quả có thể còn đổi giữa các bản phát hành, cách tính và cách xử lý cũng có thể còn được hiệu chỉnh.

Hãy đối chiếu kết quả với một cách tính độc lập trước khi đưa vào quy trình nghiên cứu, và ghim phiên bản thư viện trong dự án để mã đang chạy không tự đổi theo bản mới.

Hồ sơ thị trường

profile quyết định lô, bước giá, biên độ, chu kỳ thanh toán và thuế bán. bt.list_profiles() trả đủ trường của từng hồ sơ.

profileThị trườngLôBiên độThanh toánThuế bánBán khống
vn_hoseCổ phiếu HOSE100±7%T+2.50,1%không
vn_hnxCổ phiếu HNX100±10%T+2.50,1%không
vn_upcomCổ phiếu UPCoM100±15%T+2.50,1%không
vn_futuresHợp đồng tương lai VN30F1 hợp đồng, hệ số 100.000±7%T+00có, ký quỹ ban đầu 17%
crypto_spotCrypto giao ngay0,0001khôngT+00không
crypto_perpCrypto hợp đồng vĩnh cửu0,001khôngT+00có, ký quỹ ban đầu 10%
global_equityCổ phiếu quốc tế1khôngT+00có
forexNgoại hối0,01 lot, hệ số 100.000khôngT+00có, ký quỹ 2%
frictionlessKhông luật thị trường, dùng để đối chứng1khôngT+00không

Tên ngắn cũng được nhận: hose, hnx, upcom, vn30f, futures, crypto, perp, us_equity, fx.

Sửa một trường mà không tạo hồ sơ mới:

Python
res = bt.run(df, sigs, profile="vn_hose", profile_overrides={"settlement": "historical_vn"})

Luật cổ phiếu Việt Nam

Chu kỳ thanh toán

  • t2_5 (mặc định): cổ phiếu mua ngày T bán được từ phiên chiều T+2. Lệnh bán ở phiên sáng T+2 chưa khớp; lệnh ở phiên đóng cửa T+2 thì khớp. Tiền bán về tài khoản theo cùng chu kỳ.
  • historical_vn: theo ngày giao dịch. Lệnh mua trước 25/08/2022 bán được từ mở cửa T+3, lệnh mua từ 25/08/2022 bán được từ phiên chiều T+2 (mốc theo quy định của Tổng công ty Lưu ký và Bù trừ chứng khoán Việt Nam). Dùng khi backtest trải dài qua năm 2022.
  • Các giá trị khác: t0, t1, t2, t3.

Lệnh thoát rơi vào lúc cổ phiếu chưa về được giữ lại, khớp ở phiên đầu tiên bán được, và ghi exit_reason có đuôi _delayed_by_settlement.

Mặc định, tiền bán chưa về không được dùng để mua. Muốn mô phỏng dịch vụ ứng trước tiền bán thì đặt use_unsettled_proceeds=True.

Bước giá

SànGiá dưới 10.000 đ10.000 đ đến dưới 50.000 đTừ 50.000 đ
HOSE10 đ50 đ100 đ
HNX, UPCoM100 đ100 đ100 đ

Giá khớp được làm tròn theo bước giá về phía bất lợi cho bạn: lệnh mua làm tròn lên, lệnh bán làm tròn xuống.

Biên độ, khóa trần và khóa sàn

  • Giá trần, giá sàn tính từ reference_price của phiên. Không có cột này thì dùng giá đóng cửa phiên trước. Kết quả làm tròn vào trong biên độ theo bước giá.
  • Giá khớp không vượt ra ngoài biên độ.
  • Khóa sàn: phiên có giá mở cửa, giá cao nhất và giá đóng cửa đều bằng giá sàn. Lệnh bán ở phiên này không khớp (LIMIT_DOWN_LOCKED) và được giữ lại tới phiên đầu tiên bán được, kể cả lệnh cắt lỗ.
  • Khóa trần: tương tự ở giá trần. Lệnh mua vào ở phiên này bị bỏ (LIMIT_UP_LOCKED).

Khóa sàn là điểm kết quả backtest hay lệch nhất so với thực tế. Một lệnh cắt lỗ 7% gặp ba phiên khóa sàn liên tiếp có thể khớp ở mức lỗ hơn 20%. Thư viện backtest không có luật biên độ thì cho lệnh cắt lỗ đó khớp ngay ở phiên chạm mức đã đặt.

Phiên giao dịch không hưởng quyền (GDKHQ) trên giá gốc không bị hiểu nhầm là khóa sàn: giá tham chiếu của phiên đó tính thẳng từ corporate_actions theo đúng công thức của sở ((giá đóng cửa phiên trước − cổ tức tiền + giá phát hành × tỷ lệ quyền) / (1 + tỷ lệ cổ phiếu thưởng + tỷ lệ quyền mua)), không lấy nguyên giá đóng cửa phiên trước. Nếu một phiên vẫn giao dịch ra ngoài biên độ tính từ giá tham chiếu (ví dụ bạn quên khai sự kiện quyền, hoặc dữ liệu có lỗi), thư viện bỏ áp biên độ và khóa trần/sàn cho đúng phiên đó, kèm cảnh báo trong res.warnings nêu rõ các phiên liên quan; detect_limit_events cũng bỏ qua các phiên này.

Liệt kê các phiên chạm trần, chạm sàn trên dữ liệu của bạn:

Python
events = bt.detect_limit_events(df, profile="vn_hose")
# [{'bar_index': 18, 'date': '2021-01-28', 'reference_price': 24420.0,
#   'limit_price': 22750.0, 'kind': 'closed_at_floor', 'volume': 5092400.0}, ...]

kind gồm floor_lock (cả phiên ở giá sàn), closed_at_floor, ceiling_lock (cả phiên ở giá trần), closed_at_ceiling. Giá trả về tính bằng đồng.

Thuế bán

Thuế bán 0,1% trên giá trị bán nằm trong hồ sơ cổ phiếu Việt Nam (sell_tax_rate), tính riêng với phí giao dịch. Đổi fee hay broker không làm mất thuế bán. Tổng thuế ghi ở total_tax, từng lệnh ở exit_tax.


Đơn vị giá

vnstock báo giá cổ phiếu theo nghìn đồng: 63.1 nghĩa là 63.100 đ. Bước giá và biên độ lại tính bằng đồng.

price_unitKhi nào dùng
"auto" (mặc định)Với hồ sơ cổ phiếu Việt Nam, nếu trung vị giá đóng cửa dưới 1.000 thì nhân 1.000. Việc quy đổi được ghi trong res.warnings
"thousand_vnd"Dữ liệu chắc chắn theo nghìn đồng
"vnd"Dữ liệu đã tính bằng đồng, không quy đổi

Tín hiệu luôn tính trên giá bạn đưa vào. Một điều kiện như close > 50 trong rule_dsl vẫn hiểu là 50 nghìn đồng khi dữ liệu theo nghìn đồng. Giá trong trades, equity_curve và tear sheet tính bằng đồng.


Phí giao dịch

Ba cách khai phí:

Python
bt.run(df, sigs, fee=0.0015)               # 0,15% mỗi chiều
bt.run(df, sigs, fee=(0.0015, 0.0020))     # mua 0,15%, bán 0,20%
bt.run(df, sigs, broker="vps")             # biểu phí tham khảo

bt.list_brokers() trả các biểu phí tham khảo. Mỗi mục có id, name, buy_fee_rate, sell_fee_rate, min_fee, verified và as_of:

idPhí mỗi chiềuverifiedas_of
vps0,13%có2026
ssi0,15%có2026
vnd0,15%có2026
tcbs0,03%chưa2025
vci, hsc, mbs, kis, mas, vpbanks, fpts0,15%chưa2026
dnse0,03%chưa2026
binance_spot0,10%chưa2026
binance_perp0,05%chưa2026
zero0có—

verified: false nghĩa là chưa đối chiếu được với trang biểu phí hoặc hợp đồng chính thức. Mức phí thực tế còn phụ thuộc gói dịch vụ, kênh và chương trình ưu đãi của bạn. Khi biết mức của mình, khai thẳng bằng fee.

Phái sinh VN30F thường tính phí theo hợp đồng, không theo % giá trị. Thêm fee_per_contract (mặc định 0) để cộng một mức phí cố định mỗi đơn vị, mỗi chiều, vào phí theo tỷ lệ đã khai:

Python
bt.run(df, sigs, profile="vn_futures", fee=0.0, fee_per_contract=2700)

Sự kiện quyền

Dữ liệu vnstock mặc định là giá đã điều chỉnh: cổ tức và cổ phiếu thưởng đã được phản ánh vào giá, nên không khai thêm sự kiện. Thư viện từ chối khai corporate_actions khi prices_adjusted=True, để một sự kiện không bị tính hai lần.

Với giá gốc (chưa điều chỉnh), đặt prices_adjusted=False và khai sự kiện:

Python
res = bt.run(
    raw_df, sigs, prices_adjusted=False,
    corporate_actions=[
        {"kind": "cash_dividend", "ex_date": "2026-06-10", "payment_date": "2026-07-01",
         "amount_per_share": 2000, "tax_rate": 0.05},
        {"kind": "stock_dividend", "ex_date": "2026-08-05", "listing_date": "2026-09-02", "ratio": 0.15},
        {"kind": "rights_issue", "ex_date": "2026-10-20", "ratio": 0.2, "subscription_price": 10000,
         "listing_date": "2026-11-15"},
    ],
)
  • Cổ tức tiền: người nắm giữ trước ngày giao dịch không hưởng quyền được nhận. Tiền về tài khoản ở payment_date, sau khi trừ thuế thu nhập cá nhân tax_rate (mặc định 5%). Thiếu payment_date thì ghi nhận ở ex_date và có cảnh báo. amount_per_share tính bằng đồng, cùng đơn vị với giá dữ liệu sau khi quy đổi.
  • Cổ phiếu thưởng, cổ tức bằng cổ phiếu: ratio=0.15 là thêm 15%. Cổ phiếu mới bán được từ listing_date. Thiếu ngày này thì dùng ex_date và có cảnh báo. Phần lẻ dưới một cổ phiếu bị bỏ; với vị thế nhiều lô (pyramiding > 1), cổ phiếu thưởng tính trên tổng số đang giữ rồi chia lại cho từng lô.
  • Quyền mua cổ phiếu (rights_issue): ratio=0.2 nghĩa là được mua thêm 20% số đang giữ, ở giá subscription_price (đồng). Mặc định (exercise=True) thư viện thực hiện quyền ngay ngày GDKHQ, trong phạm vi tiền mặt đang có; số cổ phiếu mua thêm thành một lô riêng, giá vốn bằng giá phát hành, bán được từ listing_date. Tiền mặt không đủ mua hết số quyền được hưởng thì mua được bấy nhiêu, ghi RIGHTS_CASH_LIMITED vào rejected_orders. Đặt exercise=False để coi như bỏ quyền: phần giá trị bị pha loãng mất theo, không mô phỏng việc bán lại quyền.
  • Mức dừng lỗ, chốt lời và đỉnh của cắt lỗ đuổi của các lô đang giữ dời theo đúng tỷ lệ pha loãng của từng sự kiện, nên khoảng giảm kỹ thuật ngày GDKHQ không bị tính là lỗ hay kích hoạt cắt lỗ oan.
  • Mỗi sự kiện có thể thêm symbol. Backtest một mã thì không cần; chạy danh mục nhiều mã (run_portfolio) thì bắt buộc khai symbol để sự kiện áp đúng mã.

Kịch bản sốc tự khai

Thư viện không kèm kịch bản nào. Bạn tự khai một khoảng ngày để thử chiến lược trong điều kiện thanh khoản xấu hơn dữ liệu cho thấy:

Python
res = bt.run(
    df, sigs, symbol="AAA",
    shock_events=[{
        "event_id": "stress_test_1",
        "symbol": "AAA",
        "start_date": "2026-07-01",
        "end_date": "2026-07-10",
        "enforce_floor_lock": True,      # phiên mở và đóng ở giá sàn được tính là khóa sàn
        "extra_slippage_pct": 0.01,      # trượt giá thêm 1% cho mọi lệnh khớp trong khoảng này
        "headline": "Giả định thanh khoản cạn",
    }],
)

Bắt buộc: event_id, symbol, start_date, end_date. Kịch bản chỉ áp khi bạn truyền vào, và chỉ áp cho đúng symbol. Đây là giả định của bạn, không phải dữ liệu thị trường.