Luật thị trường, phí và sự kiện
Mục lục
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 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ơ.
profile | Thị trường | Lô | Biên độ | Thanh toán | Thuế bán | Bán khống |
|---|---|---|---|---|---|---|
vn_hose | Cổ phiếu HOSE | 100 | ±7% | T+2.5 | 0,1% | không |
vn_hnx | Cổ phiếu HNX | 100 | ±10% | T+2.5 | 0,1% | không |
vn_upcom | Cổ phiếu UPCoM | 100 | ±15% | T+2.5 | 0,1% | không |
vn_futures | Hợp đồng tương lai VN30F | 1 hợp đồng, hệ số 100.000 | ±7% | T+0 | 0 | có, ký quỹ ban đầu 17% |
crypto_spot | Crypto giao ngay | 0,0001 | không | T+0 | 0 | không |
crypto_perp | Crypto hợp đồng vĩnh cửu | 0,001 | không | T+0 | 0 | có, ký quỹ ban đầu 10% |
global_equity | Cổ phiếu quốc tế | 1 | không | T+0 | 0 | có |
forex | Ngoại hối | 0,01 lot, hệ số 100.000 | không | T+0 | 0 | có, ký quỹ 2% |
frictionless | Không luật thị trường, dùng để đối chứng | 1 | không | T+0 | 0 | khô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:
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àn | Giá dưới 10.000 đ | 10.000 đ đến dưới 50.000 đ | Từ 50.000 đ |
|---|---|---|---|
| HOSE | 10 đ | 50 đ | 100 đ |
| HNX, UPCoM | 100 đ | 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_pricecủ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:
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_unit | Khi 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í:
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ảobt.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:
id | Phí mỗi chiều | verified | as_of |
|---|---|---|---|
vps | 0,13% | có | 2026 |
ssi | 0,15% | có | 2026 |
vnd | 0,15% | có | 2026 |
tcbs | 0,03% | chưa | 2025 |
vci, hsc, mbs, kis, mas, vpbanks, fpts | 0,15% | chưa | 2026 |
dnse | 0,03% | chưa | 2026 |
binance_spot | 0,10% | chưa | 2026 |
binance_perp | 0,05% | chưa | 2026 |
zero | 0 | có | — |
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:
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:
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ântax_rate(mặc định 5%). Thiếupayment_datethì ghi nhận ởex_datevà có cảnh báo.amount_per_sharetí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.15là thêm 15%. Cổ phiếu mới bán được từlisting_date. Thiếu ngày này thì dùngex_datevà 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.2nghĩ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, ghiRIGHTS_CASH_LIMITEDvàorejected_orders. Đặtexercise=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 khaisymbolđể 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:
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.
