Bài trước mô tả cách CW Dashboard theo dõi lãi lỗ sổ chứng quyền có bảo đảm (CW). Bài này đi vào phần làm: bộ code gồm những file nào, cần chuẩn bị dữ liệu gì, chạy theo thứ tự nào, và engine tính ra từng con số ra sao. Làm theo từ đầu đến cuối, bạn có một dashboard chạy trên máy mình với dữ liệu mẫu trong vài phút, rồi thay bằng dữ liệu thật.
Mục lục
1. Bộ code gồm những gì
Bộ code tách làm hai chương trình độc lập, chạy nối tiếp nhau:
| Chương trình | Chạy khi nào | Đầu vào | Đầu ra |
|---|---|---|---|
Engine (cwpnl/engine.py) | Mỗi ngày sau phiên, khi có giao dịch và giá mới | Các file CSV trong data/input/ | File SQLite data/pnl.db: số luỹ kế theo ngày × mã |
Dashboard (cwpnl/server.py + static/index.html) | Bất cứ lúc nào cần xem | data/pnl.db + điều khoản CW | Trang web tại http://127.0.0.1:8060, 5 tab |
Tách như vậy để logic P&L chỉ nằm ở một chỗ (engine). Dashboard chỉ đọc, cộng gộp theo bộ lọc và tính Greeks; mở bao nhiêu người cùng lúc cũng không làm thay đổi số liệu.
Hai cách đo P&L được tính song song:
- Mark-to-Market: CW đang lưu hành định giá theo giá đóng cửa trên sàn. Khớp với số liệu kế toán.
- Mark-to-Model: CW đang lưu hành định giá theo Black-Scholes với IV định giá nội bộ. Phản ánh hiệu quả kinh tế của sổ phòng hộ, không bị nhiễu bởi giá CW trên sàn lệch khỏi giá trị lý thuyết.
2. Luồng dữ liệu
| Bước | Việc làm | File |
|---|---|---|
| 1 | Xuất giao dịch CW và cổ phiếu phòng hộ, giá đóng cửa, điều khoản CW, IV nội bộ ra CSV | data/input/*.csv |
| 2 | Engine đọc CSV, xử lý từng phiên theo thứ tự thời gian: khớp lệnh, cổ tức, đáo hạn; ghi trạng thái luỹ kế từng mã | run_engine.bat → data/pnl.db |
| 3 | Server đọc pnl.db ở chế độ chỉ đọc; tự đọc lại khi file đổi | run_dashboard.bat |
| 4 | Trình duyệt gọi API theo bộ lọc, vẽ biểu đồ và bảng | cwpnl/static/index.html |
pnl.db, nên có thể chạy engine trong lúc dashboard đang mở. Sau khi engine xong, chỉ cần bấm F5 trên trình duyệt.3. Cây thư mục
cw_pnl_dashboard/
├── run_demo.bat tạo dữ liệu mẫu -> engine -> dashboard (chạy thử)
├── run_engine.bat chạy engine
├── run_dashboard.bat mở dashboard
├── run.sh bản cho macOS / Linux: ./run.sh demo | engine | dashboard | test
├── config.json tham số + định nghĩa Lô phát hành
├── requirements.txt pandas
├── README.md hướng dẫn chi tiết
├── cwpnl/
│ ├── pricing.py Black-Scholes, Greeks, giải IV (chỉ dùng thư viện chuẩn)
│ ├── common.py đọc cấu hình và các file đầu vào
│ ├── engine.py tính P&L luỹ kế -> data/pnl.db
│ ├── server.py API JSON + phục vụ giao diện
│ └── static/
│ ├── index.html giao diện 5 tab
│ └── vendor/ Chart.js 4.4.4 (giấy phép MIT)
├── tools/
│ ├── make_sample_data.py sinh dữ liệu giả lập để chạy thử
│ ├── self_test.py kiểm tra công thức bằng ví dụ tính tay
│ └── find_python.bat tìm Python, tự cài thư viện còn thiếu
└── data/
├── input/ dữ liệu đầu vào (CSV)
└── pnl.db engine tạo ra4. Dữ liệu cần chuẩn bị
Tất cả là file CSV đặt trong data/input/, dòng đầu là tên cột, mã hoá UTF-8. Ngày dạng YYYY-MM-DD. Giá và tiền tính bằng đồng (không phải nghìn đồng). Ba file đầu là bắt buộc.
4.1 trades.csv — giao dịch của sổ (bắt buộc)
| Cột | Ý nghĩa | Ví dụ |
|---|---|---|
date | Ngày giao dịch | 2026-07-15 |
leg | CW = chứng quyền; UL = cổ phiếu phòng hộ | CW |
symbol | Mã CW hoặc mã cổ phiếu | CGHI2701 |
side | SELL hoặc BUY. Với CW: SELL = bán cho nhà đầu tư, BUY = mua lại | SELL |
qty | Khối lượng (số dương) | 50000 |
price | Giá khớp (đồng) | 1250 |
fee | Phí + thuế của lệnh (đồng); 0 nếu không có | 19 |
subaccount | Tiểu khoản phòng hộ (bắt buộc với UL, để trống với CW) | HEDGE02 |
date,leg,symbol,side,qty,price,fee,subaccount 2026-07-15,CW,CGHI2701,SELL,50000,1250,19, 2026-07-15,UL,GHI,BUY,8000,31200,374,HEDGE02 2026-07-20,CW,CGHI2701,BUY,10000,1180,4,
Mỗi dòng có thể là một lệnh khớp hoặc tổng khớp trong ngày theo mã và chiều. Kết quả như nhau vì engine dùng giá bình quân gia quyền.
4.2 prices.csv — giá đóng cửa (bắt buộc)
| Cột | Ý nghĩa |
|---|---|
date | Ngày |
symbol | Mã CW, mã cổ phiếu cơ sở, và chỉ số tham chiếu (mặc định VN30) |
close | Giá đóng cửa (đồng; chỉ số tính theo điểm) |
4.3 cw_metadata.csv — điều khoản CW (bắt buộc)
| Cột | Ý nghĩa | Ví dụ |
|---|---|---|
cw | Mã CW | CGHI2701 |
underlying | Mã cổ phiếu cơ sở | GHI |
issuer | Tổ chức phát hành | K |
strike | Giá thực hiện ban đầu (đồng) | 35700 |
ratio | Tỷ lệ chuyển đổi (số CW đổi 1 cổ phiếu) | 3 |
maturity | Ngày đáo hạn | 2027-01-18 |
qtty | Khối lượng phát hành | 3000000 |
is_call | 1 = CW mua (mặc định), 0 = CW bán | 1 |
4.4 cw_iv.csv — IV định giá nội bộ (nên có)
Ba cột date, cw, iv: IV áp dụng từ ngày date, viết dạng thập phân (0.42 = 42%). Mỗi lần bộ phận định giá đổi IV thì thêm một dòng. CW chưa có IV: engine dùng default_iv; dashboard tính Greeks bằng IV giải từ giá thị trường, không được thì IV mặc định và gắn nhãn cảnh báo.
4.5 File tuỳ chọn
| File | Cột | Dùng khi |
|---|---|---|
cw_adjustments.csv | cw, ex_date, strike_before, ratio_before, strike_after, ratio_after | Cổ phiếu cơ sở có sự kiện quyền làm điều chỉnh giá thực hiện, tỷ lệ chuyển đổi |
dividends.csv | symbol, ex_date, cash | Cổ tức tiền mặt trên cổ phiếu phòng hộ (đồng/cổ phiếu) |
owner.csv | cw, owner | Gán trader phụ trách để lọc theo người |
5. Cấu hình — config.json
{
"input_dir": "data/input",
"db_path": "data/pnl.db",
"risk_free_rate": 0.08,
"cof_rate": 0.08,
"default_iv": 0.40,
"index_symbol": "VN30",
"start_date": "2025-10-16",
"end_date": null,
"port": 8050,
"host": "127.0.0.1",
"batches": {
"Lô 1": { "start_date": "2025-10-16", "subaccounts": ["HEDGE01"],
"cws": ["CABC2601", "CABC2602", "CDEF2601", "CDEF2602"] },
"Lô 2": { "start_date": "2026-07-01", "subaccounts": ["HEDGE02"],
"cws": ["CGHI2701", "CGHI2702", "CJKL2701", "CJKL2702"] }
}
}| Khoá | Ý nghĩa |
|---|---|
risk_free_rate | Lãi suất phi rủi ro trong Black-Scholes |
cof_rate | Lãi suất vốn/năm để tính COF trên giá vốn cổ phiếu phòng hộ |
default_iv | IV dùng khi CW chưa có IV nội bộ |
start_date, end_date | Khoảng ngày engine tính; null = đến ngày giá cuối cùng |
host, port | Địa chỉ server. Giữ 127.0.0.1 để chỉ máy mình truy cập được |
batches | Mỗi Lô phát hành: ngày bắt đầu, tiểu khoản phòng hộ, danh sách CW. Mọi CW và tiểu khoản phải thuộc một Lô |
6. Các bước chạy
6.1 Lần đầu
- Cài Python 3.10+ từ python.org. Trên Windows, tick Add python.exe to PATH khi cài.
- Giải nén bộ code vào một thư mục bất kỳ, ví dụ
D:\cw_pnl_dashboard. - Bấm đúp
run_demo.bat(macOS/Linux:./run.sh demo). Lần đầu script tự càipandas, sinh dữ liệu giả lập (4 mã cơ sở, 8 CW, 2 Lô, khoảng một năm), chạy engine rồi mở trình duyệt. - Kiểm tra công thức:
python tools/self_test.pyphải in TẤT CẢ ĐỀU ĐÚNG.
6.2 Chuyển sang dữ liệu thật
- Xoá các file mẫu trong
data/input/. - Đặt 3 file bắt buộc
trades.csv,prices.csv,cw_metadata.csv(mục 4), thêmcw_iv.csvvà các file tuỳ chọn nếu có. - Sửa
config.json: ngày bắt đầu, lãi suất, danh sách Lô với đúng mã CW và tiểu khoản phòng hộ. - Bấm đúp
run_engine.bat. Đọc log: số phiên, số giao dịch, các dòng CẢNH BÁO. - Bấm đúp
run_dashboard.bat.
6.3 Hằng ngày
| Bước | Việc làm |
|---|---|
| 1 | Sau phiên: thêm giao dịch mới vào trades.csv, giá đóng cửa mới vào prices.csv; thêm dòng vào cw_iv.csv nếu đổi IV; thêm CW mới vào cw_metadata.csv và config.json nếu có đợt phát hành mới. |
| 2 | Chạy run_engine.bat (vài giây). |
| 3 | Mở hoặc F5 dashboard. Góc trên ghi “Số liệu tới <ngày>” phải là phiên vừa đóng. |
7. Engine tính gì: ví dụ tính tay
Engine xử lý từng phiên theo thứ tự thời gian, giữ trạng thái cho mỗi CW và mỗi cặp (cổ phiếu, tiểu khoản):
| Sự kiện | Cách ghi nhận |
|---|---|
| Bán CW | Cập nhật giá bán bình quân gia quyền; KL lưu hành tăng; tiền thu ròng tăng |
| Mua lại CW | Realized += (giá bán BQ − giá mua) × KL; KL lưu hành giảm |
| Mua cổ phiếu | Cập nhật giá vốn bình quân |
| Bán cổ phiếu | Realized += (giá bán − giá vốn BQ) × KL |
| Cổ tức tiền mặt | Realized phòng hộ += cổ tức × KL nắm giữ cuối phiên trước |
| Đáo hạn | Thanh toán tiền mặt theo S5 = bình quân giá đóng cửa cơ sở 5 phiên liền trước ngày đáo hạn; mỗi CW nhận max(S5 − K, 0) / CR |
| Cuối mỗi phiên | Unrealized Mark-to-Market = (giá bán BQ − giá đóng cửa CW) × KL lưu hành; Unrealized Mark-to-Model = (giá bán BQ − giá Black-Scholes) × KL lưu hành; Unrealized cổ phiếu = (giá − giá vốn BQ) × KL |
| Mỗi ngày lịch | COF của Lô = cof_rate / 365 × giá vốn cổ phiếu phòng hộ của Lô |
Ví dụ dưới đây chính là bộ kiểm tra trong tools/self_test.py: CW giá thực hiện 10.000 đ, tỷ lệ 2:1, đáo hạn 12/01/2026; cof_rate 3,65%/năm.
| Ngày | Sự kiện | Kết quả |
|---|---|---|
| 05/01 | Bán 1.000 CW giá 300; mua 500 cổ phiếu giá 10.000 | Giá bán BQ 300; giá vốn 5.000.000 đ; COF ngày = 3,65% / 365 × 5.000.000 = 500 đ |
| 06/01 | Bán thêm 1.000 CW giá 500 | Giá bán BQ = (300 × 1.000 + 500 × 1.000) / 2.000 = 400 |
| 07/01 | Mua lại 500 CW giá 450 | Realized CW = (400 − 450) × 500 = −25.000 đ; còn 1.500 CW |
| 08/01 | Bán 200 cổ phiếu giá 10.600; CW đóng cửa 450 | Realized cổ phiếu = (10.600 − 10.000) × 200 = 120.000 đ; Unrealized CW = (400 − 450) × 1.500 = −75.000 đ |
| 12/01 | Đáo hạn. Giá cơ sở 5 phiên trước: 10.000; 10.200; 10.400; 10.600; 10.800 | S5 = 10.400; thanh toán (10.400 − 10.000) / 2 = 200 đ/CW; Realized CW luỹ kế = −25.000 + (400 − 200) × 1.500 = 275.000 đ |
Dashboard cộng các số luỹ kế này theo ngày rồi tính:
Khi lọc theo mã cơ sở hoặc trader, COF của Lô được chia theo tỷ trọng giá vốn phòng hộ của nhóm đang lọc. Cộng các nhóm lại luôn bằng tổng: đã kiểm tra trên dữ liệu mẫu, tổng theo Lô, theo trader, theo mã cơ sở và theo hai nửa kỳ đều khớp với P&L toàn sổ.
8. Cơ sở dữ liệu đầu ra — data/pnl.db
| Bảng | Cột chính | Nội dung |
|---|---|---|
fp_symbol_daily | date, leg, sym, sub, batch, qty, qtty, px, spot, strike, ratio, iv, iv_src, bs_px, avg_px, realized, unreal, unreal_pricing, fee, net_cash, net_tx, fund | Mỗi dòng = 1 mã × 1 phiên. realized, fee, net_cash là luỹ kế; unreal, unreal_pricing, fund, qty là trạng thái cuối ngày; net_tx là giao dịch ròng trong ngày |
fp_cof_calendar | cal_date, COF <Lô> (VND) | COF từng ngày lịch của mỗi Lô |
fp_vn30 | date, close | Chỉ số tham chiếu |
fp_settlements | cw, maturity_date, s5, strike, ratio, payoff_per_cw, qty, realized | CW đã tất toán |
fp_cw_adjustments | như file đầu vào | Lịch điều chỉnh quyền |
fp_meta | key, value | Định nghĩa Lô, giờ chạy engine, mô tả COF (dạng JSON) |
File này mở được bằng DB Browser for SQLite, Python (pandas.read_sql) hoặc Excel qua Power Query, nên có thể dùng làm nguồn cho các báo cáo khác.
9. Dashboard và API
| Tab | Nội dung | Bộ lọc |
|---|---|---|
| Tổng quan | 6 thẻ KPI (PBT, Mark-to-Model, Delta exposure, Gamma 1%, Theta, Sổ phòng hộ) · P&L luỹ kế hai cách đo · Sổ phòng hộ so với VN30 · Báo cáo P&L theo kỳ với cột YTD từng năm và Toàn kỳ | Ngày / Tuần / Tháng |
| Phân tích P&L | Waterfall đóng góp · P&L luỹ kế · ROI · Phân rã theo mã cơ sở và từng CW | Kỳ, Lô, mã cơ sở, trader, trạng thái, cách đo |
| Vị thế | Tỷ lệ phân phối theo thời gian · Delta exposure theo mã · Bảng vị thế: moneyness, ngày còn lại, IV (gắn nhãn nếu không phải IV nội bộ), OI, Greeks, giao dịch ròng | Ngày, Lô, mã cơ sở, trader |
| IV | Smile và term structure (IV nội bộ so với IV thị trường) · IV lịch sử · bảng IV | Mã cơ sở |
| Tất toán | CW đã đáo hạn: giá S5, thanh toán mỗi CW, realized tại đáo hạn | — |
Giao diện lấy dữ liệu qua 6 API JSON (đều là GET, tham số trên URL), có thể gọi từ Excel hoặc script khác:
| API | Tham số | Trả về |
|---|---|---|
/api/meta | — | Danh sách Lô, mã cơ sở, trader, ngày dữ liệu |
/api/overview | range = D/W/M | KPI, xu hướng, bảng P&L theo kỳ |
/api/pnl | start, end, batch, underlying, owner, status, method (model/market), range | Waterfall, xu hướng, ROI, phân rã |
/api/positions | date, batch, underlying, owner | Vị thế, Greeks, tỷ lệ phân phối |
/api/iv | underlying | Điểm IV hiện tại và lịch sử |
/api/settlements | — | CW đã tất toán |
10. Kiểm tra và lỗi thường gặp
| Hiện tượng | Cách xử lý |
|---|---|
Thiếu data/input/...csv | Đặt đủ 3 file bắt buộc |
CW có giao dịch nhưng thiếu trong cw_metadata.csv | Bổ sung điều khoản cho mã được báo |
mua lại ... > KL đang lưu hành hoặc bán ... > KL nắm giữ | Giao dịch thiếu hoặc sai chiều; kiểm tra trades.csv của mã và ngày được báo |
chưa gán Lô | Thêm mã CW hoặc tiểu khoản vào batches trong config.json |
| Dashboard báo “Chưa có dữ liệu” | Chưa chạy engine, hoặc db_path trỏ sai |
| Cổng 8050 đang bận | Đổi port trong config.json và URL trong run_dashboard.bat |
| Nhãn TT / MĐ cạnh IV | CW chưa có IV nội bộ: Greeks đang dùng IV thị trường (TT) hoặc IV mặc định (MĐ) |
| Không tìm thấy Python | Cài lại Python và tick Add to PATH, hoặc đặt bản Python nhúng vào thư mục python_runtime\ cạnh bộ code |
11. Giới hạn và hướng mở rộng
- Cửa sổ S5: trong 5 phiên trước đáo hạn, nghĩa vụ CW đã bị chốt dần theo giá bình quân, nhưng Mark-to-Model vẫn định giá bằng Black-Scholes trên giá hiện tại. P&L có thể dao động trong vài phiên này rồi trở về đúng ở ngày tất toán. Có thể cải tiến bằng cách định giá theo phần S5 đã biết cộng phần còn lại.
- Một lãi suất phi rủi ro cho mọi CW; mỗi CW một IV (không nội suy bề mặt IV).
- Đầu vào là CSV xuất tay. Bước tiếp theo hợp lý là đọc thẳng từ cơ sở dữ liệu giao dịch, hoặc dùng Power Query gộp file xuất hằng ngày thành
trades.csv. - Server không có đăng nhập. Không đổi
hostthành0.0.0.0nếu không muốn máy khác trong mạng xem được số liệu. - Có thể lên lịch chạy engine tự động bằng Task Scheduler (Windows) hoặc cron (macOS/Linux) sau giờ đóng cửa.
12. Bản quyền và miễn trừ
- Bộ code và bài viết do tác giả tự viết. Dữ liệu mẫu (mã ABC, DEF, GHI, JKL, các CW và tiểu khoản) hoàn toàn giả lập, không phản ánh vị thế hay kết quả của bất kỳ tổ chức nào.
- Mô hình Black-Scholes là kiến thức công khai (Black, F. & Scholes, M., 1973, “The Pricing of Options and Corporate Liabilities”, Journal of Political Economy).
- Thư viện dùng kèm: Chart.js 4.4.4 (MIT, file giấy phép đi kèm trong
static/vendor/), pandas (BSD), SQLite (public domain), Python (PSF License). - VN30, HOSE là tên gọi thuộc chủ sở hữu tương ứng, chỉ dùng để mô tả.
- Bài viết là chia sẻ kỹ thuật, không phải khuyến nghị đầu tư. Kiểm tra kỹ số liệu trước khi dùng cho báo cáo chính thức.


