vndirect-mcp
# vndirect-mcp
MCP server cá nhân cung cấp dữ liệu chứng khoán Việt Nam từ API công khai của VNDIRECT
(không cần đăng nhập/API key), để dùng cho bot phân tích kỹ thuật trong VISSOFT AI Team
Manager (hoặc bất kỳ Claude Code / MCP client nào khác).
## Tool cung cấp
### `get_ohlc(symbol, resolution, count)`
Lấy dữ liệu nến OHLCV lịch sử.
- `symbol`: mã cổ phiếu, ví dụ `HPG`, `VNM`, `FPT`
- `resolution`: `"1H"` | `"4H"` | `"1D"`
- `count`: số nến muốn lấy (1–500, mặc định 100)
**Đã kiểm chứng hoạt động thật** (test trực tiếp qua stdio JSON-RPC lúc build) cho cả 3
khung thời gian. Lưu ý:
- `1H` và `1D` gọi thẳng API `dchart-api.vndirect.com.vn` (resolution `60` / `D`).
- `4H` **không có sẵn** ở VNDIRECT — server tự gộp mỗi 4 nến 1H liên tiếp thành 1 nến 4H
(gộp theo thứ tự nến trả về, không theo mốc giờ tường minh, vì phiên giao dịch VN chỉ
~4.25h/ngày và ngắt quãng nghỉ trưa nên gộp theo giờ cố định dễ lệch hơn gộp tuần tự).
### `get_fundamentals(symbol)`
Lấy chỉ số/thông tin cơ bản qua `api-finfo.vndirect.com.vn` (lưu ý: đúng là
`api-finfo`, KHÔNG PHẢI `finfo-api` — dễ gõ nhầm thứ tự, bản đầu tiên của tool này bị
đúng lỗi này).
**Đã kiểm chứng hoạt động thật với bộ 6 chỉ số curated** (P/E, P/B, EPS 4 quý gần nhất,
ROE, ROA, tỷ suất cổ tức) — test với HPG ra số hợp lý (P/E=8.02, P/B=1.32, EPS=2750đ,
ROE=17.5%, ROA=9%).
**Lịch sử debug endpoint** (đáng đọc nếu endpoint VNDIRECT đổi lần nữa):
- `/v4/ratios/latest` (bản đầu tiên dùng) hoạt động như 1 feed "bản ghi cập nhật gần
nhất" — luôn trả đúng 1 dòng NGẪU NHIÊN theo `order`, bỏ qua hoàn toàn filter
`ratioCode` — KHÔNG dùng được để lấy 1 chỉ số cụ thể.
- Endpoint ĐÚNG là `/v4/ratios` (không có `/latest`), query param `q` (không phải
`filter`), sort param `sort=reportDate:desc` (không phải `order=reportDate`) — mỗi
ratioCode phải gọi RIÊNG 1 request (`size=1&sort=reportDate:desc`), vì danh sách nhiều
`ratioCode` cách nhau dấu phẩy trong 1 lần gọi bị trộn không đáng tin (chỉ số cập nhật
hàng ngày như P/E lấn át chỉ số cập nhật theo quý như ROAE trong cùng 1 trang kết quả).
- Mã ratioCode xác nhận đúng: `PRICE_TO_EARNINGS`, `PRICE_TO_BOOK`, `EPS_TR` (EPS 4 quý
liền kề — không phải `EARNING_PER_SHARE`/`BASIC_EPS_TR` như đoán ban đầu),
`ROAE_TR_AVG5Q`, `ROAA_TR_AVG5Q`, `DIVIDEND_YIELD`.
**Cảnh báo dữ liệu**: `dividendYield` trả về giá trị cực nhỏ bất thường cho HPG (`~1.8e-7`,
gần như 0) — có thể do công ty trả cổ tức chủ yếu bằng cổ phiếu (không phải tiền mặt) nên
tỷ lệ tiền mặt/thị giá gần 0 là hợp lý thật, hoặc do cách VNDIRECT tính/đơn vị của field
này khác kỳ vọng — **CHƯA xác minh được nguyên nhân chính xác**, dùng số này thận trọng,
đối chiếu nguồn khác nếu user hỏi cụ thể về cổ tức.
### `render_chart(symbol, resolution, count, outputPath, smaPeriods?, showVolume?, showMacd?)`
Vẽ biểu đồ nến kỹ thuật ra file PNG: candlestick + đường SMA chồng lên giá, panel MACD
(12,26,9) với histogram + đường tín hiệu, panel khối lượng. Tự vẽ bằng
`@napi-rs/canvas` (Canvas 2D API thô, không phụ thuộc Chart.js hay `canvas`
gốc — xem "Ghi chú kỹ thuật" bên dưới).
- `outputPath`: đường dẫn file `.png` sẽ ghi ra. Muốn bot gửi ảnh này qua Telegram
trong lượt trả lời chat trực tiếp, path phải nằm trong thư mục `scratch/` của bot
(`userData/bots/<botId>/scratch/`) và bot phải in marker
`__BOT_SEND_FILE__{"kind":"photo","path":"..."}__BOT_SEND_FILE_END__` trong câu trả
lời (cơ chế có sẵn của app, xem TASK-123). **Cronjob tự động (cảnh báo watchlist)
hiện CHƯA gửi được ảnh** — đây là giới hạn ở tầng `CronjobEngine`/`DeliveryService`
của app, đang track ở TASK-128 (`docs/tasks/TASK-128` trong workspace-ai-team-central).
- `smaPeriods`: mảng chu kỳ SMA muốn vẽ, tối đa 4 đường, mặc định `[20, 50]`.
- `showVolume` / `showMacd`: bật/tắt từng panel, mặc định `true`.
**Đã kiểm chứng hoạt động thật** — render thành công cả khung `1D` và `1H` với dữ liệu
HPG/VNM thật, ảnh PNG hợp lệ, không lỗi. Layout 3-panel (giá/MACD/volume) tham khảo từ
`bot-trading-agent/scheduler/chart.py` (dự án cũ của user, xem Notes).
**Ghi chú kỹ thuật:** ban đầu định dùng `chart.js` + `chartjs-node-canvas` +
`chartjs-chart-financial`, nhưng package `canvas` (native, dependency của
`chartjs-node-canvas`) không có prebuilt binary cho Node 24 trên Windows và build từ
source cần Visual Studio Build Tools (không có sẵn trên máy). Đã chuyển sang
`@napi-rs/canvas` (có prebuilt binary Windows x64) + tự vẽ chart thủ công bằng Canvas 2D
API (candlestick = wick line + fillRect body, không dùng chart lib nào).
### `detect_macd_divergence(symbol, resolution, count)`
Port TOÀN BỘ từ hệ thống alert engine cá nhân của user (`D:\Projects\bot-trading\
bot-trading-agent`, Python — `indicators/extrema.py`'s `find_lobe_extrema()` +
`alerts/{divergence,confirmation,engine,config}.py`), đã backtest + tune trên dữ liệu
VNDIRECT thật (HPG/MBB). Đây là method THUẦN TÍNH TOÁN, không có bước "tự kết luận" mơ hồ
như Wyckoff/VSA/SMC — nên port TOÀN BỘ (không tách lớp AI-diễn-giải), giữ nguyên mọi
ngưỡng đã tune (`alerts/config.py`): cửa sổ xác nhận 7 phiên, thân nến ≥60% biên độ,
volume ≥1.5x TB 10 phiên, tối thiểu 1 phiên "đi đúng hướng" để phân biệt weak-confirmation
vs inconclusive.
**Khác bản gốc**: bản gốc là state machine PERSISTENT (lưu DB qua nhiều lần scheduler
chạy). MCP tool không có state lưu trữ giữa các lần gọi — `detect_macd_divergence()`
REPLAY lại toàn bộ lịch sử trong 1 lần gọi (walk-forward candle-by-candle, y hệt cách
production xử lý từng nến mới) để tự tái tạo trạng thái hiện tại. Trả về: `currentState`
(NEUTRAL/PENDING_CONFIRMATION), `freshAlert` (tín hiệu phát sinh ĐÚNG tại nến cuối — dùng
để quyết định có cần cảnh báo ngay không), `allAlerts` (lịch sử đầy đủ trong khoảng dữ
liệu, phục vụ ngữ cảnh).
**Đã kiểm chứng hoạt động thật** — test với HPG/MBB (đúng 2 mã bản gốc backtest), chuỗi
early_divergence → failed_divergence/successful_reversal hợp lý, ngưỡng bucket đúng thiết
kế (weak_confirmation_sessions chỉ xuất hiện khi pending_sessions đạt 7).
`count` mặc định 250, tối thiểu 60 — cần đủ lớn để thuật toán tìm được đủ lobe MACD trước
đó (khuyến nghị giữ >= 200).
### `compute_bar_metrics(symbol, resolution, count)` + `find_trading_range(symbol, resolution, count)` + `analyze_smc_structure(symbol, resolution, count)`
3 tool này port phần **KHÁCH QUAN** của Wyckoff/VSA/SMC từ 2 dự án cũ của user
(`D:\Projects\ai-factory\apps\wyckoffviet`, Python — `vsa_analysis.py`,
`wyckoff_phase.py`, `smc_analysis_1h.py`). **Quyết định kiến trúc đã chốt**: KHÔNG port
bước "tự động gán nhãn kết luận" (vd code cũ: `if volume≥2.5x avg và body≥50% range và
close gần low: return "Selling Climax"` — đây là DIỄN GIẢI theo phương pháp luận, tại
sao 2.5x cụ thể chứ không phải 2.3x/2.7x là quyết định tuỳ ý của tác giả, không phải sự
thật khách quan). Ranh giới áp dụng nhất quán cho cả 3 tool:
- **Port** (sự thật hình học/số học, không có ngưỡng phương pháp luận tự đặt): swing
high/low (pivot), trading range (vùng đi ngang), volume/spread ratio so với median,
close position trong nến, BOS/CHoCH (giá phá đỉnh/đáy cũ — nhị phân, không % tự đặt),
Fair Value Gap (định nghĩa hình học 3 nến, không tham số), Liquidity Sweep (wick xuyên
rồi close quay lại — nhị phân).
- **KHÔNG port** (diễn giải theo phương pháp luận, có ngưỡng tự đặt): gán nhãn signal VSA
cụ thể (No Demand/Climax/Up-thrust/...), gán nhãn event Wyckoff cụ thể (SC/AR/ST/
Spring/SOS/LPS), phase classification (A→E), Order Block (ngưỡng `body×1.5` tự đặt),
bias/score tổng hợp, và đề xuất entry/SL/TP (tư vấn giao dịch cụ thể — phải do AI cân
nhắc theo phong cách/khẩu vị rủi ro của user, không hard-code công thức chung).
AI (Claude) đọc dữ kiện khách quan từ 3 tool này + ảnh chart (`render_chart`) + skill mô
tả phương pháp (xem `skills/`, đã viết), rồi tự diễn giải/kết luận — phần "phân tích" thật
sự nằm ở đó.
**Đã kiểm chứng hoạt động thật** — test với HPG, cả 3 tool trả kết quả hợp lý (trading
range 30 nến 16.23% biên độ, 10 swing highs/lows xen kẽ hợp lý, 4 FVG active, bar metrics
với close_position/body_ratio đúng khoảng [0,1]).
**Bug fix sau khi user báo lỗi thật (TCB)**: ban đầu `find_trading_range` trả `null` bất
cứ khi nào KHÔNG có vùng nào đạt cả 2 ngưỡng (18% biên độ / 6% std) — với TCB (biên độ
thật 21.79%, chỉ nhỉnh hơn ngưỡng chút), bot chỉ nói được "không tìm thấy", không có gì để
phân tích tiếp dù dữ liệu thực ra khá gần đạt. Đã sửa: giờ LUÔN trả về 1 object (trừ khi
<30 nến) — nếu không có vùng nào đạt cả 2 ngưỡng, trả về ứng viên GẦN NHẤT kèm
`qualifies: false` + `rangePct`/`stdPct` thật để bot vẫn giải thích được "gần giống đi
ngang nhưng chưa đủ chặt" thay vì im lặng. Verify lại: TCB → `qualifies: false,
rangePct: 21.79`; HPG → vẫn `qualifies: true` như cũ (không regression).
`compute_bar_metrics` yêu cầu `count >= 70` (median rolling 50 nến cần warm-up).
### `list_covered_warrants(underlyingSymbol, limit?)` + `analyze_covered_warrant(cwSymbol)`
Tìm/so sánh chứng quyền có bảo đảm (covered warrant) cho 1 mã cơ sở — VNDIRECT chỉ niêm
yết CW mua (call), chưa có CW bán.
- `list_covered_warrants`: liệt kê CW còn niêm yết cho 1 mã cơ sở (mã CW, tổ chức phát
hành, giá thực hiện, tỷ lệ chuyển đổi, ngày đáo hạn, số ngày còn lại) — dữ kiện TĨNH,
chưa có giá thị trường. Endpoint `/v4/derivatives?q=underlyingAsset:X~status:LISTED`
(endpoint `/v4/covered_warrants` cũ dùng trong `wyckoffviet` đã KHÔNG còn tồn tại — trả
404 khi test — phải tìm endpoint thay thế).
- `analyze_covered_warrant`: phân tích 1 mã CW cụ thể — lấy giá CW + giá mã cơ sở hiện tại
(tái dùng `fetchCandles`/dchart, hoạt động luôn cho mã CW vì CW cũng là 1 mã niêm yết
giao dịch như cổ phiếu thường), tính breakeven/premium%/moneyness%/gearing lý thuyết —
công thức tài chính chuẩn, đầy đủ, KHÔNG có ngưỡng "kết luận tốt/xấu" tự đặt nên port
toàn bộ (giống `detect_macd_divergence`), không cần tách lớp AI-diễn-giải cho phần TÍNH
TOÁN — nhưng việc chọn CW nào phù hợp vẫn cần AI cân nhắc khẩu vị rủi ro user (xem skill
`covered-warrant-method`).
**Đã kiểm chứng hoạt động thật** — test với HPG (69 CW đang niêm yết), số liệu breakeven/
premium/moneyness/gearing đối chiếu tay đúng công thức.
## Skill cho bot
`skills/` chứa 6 skill (đúng format `SKILL.md` app dùng — `id`/`name`/`description`/
`source: user`/`createdAt` + nội dung markdown) — copy nội dung vào bot qua UI "Kỹ năng"
(tạo skill riêng, dán nội dung), hoặc copy thẳng cả thư mục vào
`userData/bots/<botId>/.claude/skills/<id>/`:
| Skill | Nội dung |
|---|---|
| `stock-toolkit` | Định hướng chung — tool nào dùng khi nào, quy trình phân tích, cách gửi ảnh chart qua Telegram |
| `wyckoff-method` | Định nghĩa SC/AR/ST/Spring/SOS/LPS + cách suy ra phase A-E từ `find_trading_range`/`compute_bar_metrics` |
| `vsa-method` | Định nghĩa các tín hiệu VSA (No Demand, Climax, Up-thrust...) + cảnh báo đọc trong strong trend |
| `smc-method` | Cách đọc `analyze_smc_structure` + tự nhận diện Order Block + cân nhắc entry/SL/TP (không công thức cứng) |
| `macd-price-action` | MA/MACD cổ điển, cách dùng `detect_macd_divergence`, Price Action cơ bản |
| `covered-warrant-method` | Cách lọc/so sánh chứng quyền qua `list_covered_warrants`/`analyze_covered_warrant` — đọc premium/moneyness/gearing, không áp đặt 1 lựa chọn |
Nên bật cả 6 nếu muốn bot phân tích đầy đủ theo yêu cầu ban đầu (MA, MACD, Wyckoff, VSA,
SMC, Price Action, chứng quyền) — hoặc chỉ bật vài skill nếu chỉ quan tâm 1-2 phương pháp.
## Cài đặt & build
```bash
npm install
npm run build
```
Output nằm ở `dist/index.js`.
## Cách wire vào app VISSOFT AI Team Manager
Đã publish lên npm — không cần clone/build thủ công nữa. Trong app, vào bot bạn muốn dùng
(hoặc 📂 Tài nguyên của tôi > tab mcp), thêm 1 MCP server mới (loại `command`):
- **Command**: `npx`
- **Args**: `["-y", "vndirect-mcp"]`
(`-y` để npx tự tải bản mới nhất không hỏi xác nhận — lần chạy đầu sẽ chậm hơn vài giây vì
phải tải về, các lần sau dùng cache.)
Sau đó vào tab "Công cụ hỗ trợ (MCP)" của bot, tick chọn server này.
**Chạy từ source (dev/debug):** nếu muốn sửa code rồi test ngay không qua npm, dùng cấu hình
cũ trỏ thẳng vào file build local:
- **Command**: `node`
- **Args**: `["<đường-dẫn-tuyệt-đối-tới-repo>/dist/index.js"]`
## Test thủ công (không qua app)
```bash
node dist/index.js
```
Server chạy stdio, chờ JSON-RPC trên stdin — dùng để debug tay hoặc tích hợp vào MCP
client khác ngoài app này.
TDQS
Scored across 9 tools
Each tool serves a clearly distinct purpose: raw data retrieval, fundamentals, charting, technical signal detection, bars metrics, range detection, SMC analysis, and warrant listing/analysis. The only mild overlap is between compute_bar_metrics and find_trading_range, but one provides per-candle metrics while the other identifies ranges, so they are not confusable.
All tool names follow a consistent verb_noun snake_case pattern (get_, render_, detect_, compute_, find_, analyze_, list_). The verbs are descriptive and match the action, making the API predictable and easy to navigate.
9 tools is well-scoped for a stock analysis MCP server covering historical data, fundamentals, technical analysis, charting, and covered warrants. Every tool addresses a distinct need without unnecessary bloat.
The tool surface covers the core needs for Vietnam stock analysis: OHLC data, key fundamentals, technical analysis tools (MACD, VSA/Wyckoff, SMC), charting, and covered warrant analysis. Minor gaps exist, such as no direct real-time quote tool or a stock screener, but these are not critical for the server's evident purpose.