Thứ Bảy, 16 tháng 11, 1929
Đăng bởi

Ba tuần trước, tôi ship vnstock-js v1.4.0. Đó không phải chỉ là một version bump. Đó là khoảnh khắc khi AI trở thành native với thị trường chứng khoán Việt Nam.
Cho đến bây giờ, nếu bạn muốn Claude phân tích VCB hay so sánh FPT vs MBB, bạn có hai lựa chọn:
Với v1.4, có một lựa chọn thứ ba: MCP Server. Claude giờ có truy cập trực tiếp vào dữ liệu chứng khoán Việt mà không cần bất kỳ sự phức tạp nào. Một file config. 11 tools. Dữ liệu real-time.
Bài post này đi sâu vào tại sao chúng tôi xây dựng nó, cái gì bạn có thể xây dựng giờ, và cách chúng tôi làm nó từ kỹ thuật.
Claude không chỉ là chatbot nữa. Nó đang trở thành hệ điều hành cho AI workflows. Mọi người đang dùng Claude để:
Nhưng Claude không thể thấy dữ liệu thực tế. Nó có knowledge cutoff. Nó không thể fetch portfolio của bạn. Nó không thể check giá chứng khoán hôm nay. Cho đến bây giờ, nếu bạn muốn Claude truy cập dữ liệu real-time, bạn phải xây dựng một cây cầu.
Cây cầu đó thường trông như:
Bạn → Prompt Claude → Claude nói "Tôi cần dữ liệu" → Bạn fetch dữ liệu → Bạn paste dữ liệu → Claude phân tích
Điều này tệ vì:
MCP (Model Context Protocol) là câu trả lời của Anthropic. Nó là một protocol nói:
"Đây là cách AI models kết nối với nguồn dữ liệu và tools mà không cần bất kỳ setup đặc biệt nào."
Với vnstock-js, MCP có nghĩa:
Thị trường chứng khoán Việt Nam là độc nhất:
Chúng tôi nhận ra: thay vì xây dựng 10 cái API wrapper khác nhau, chúng ta nên xây dựng một MCP server hoạt động ở mọi nơi Claude đi.
Bạn: "Phân tích portfolio của tôi. Tôi có VCB 100 cổ phiếu @ giá vốn 50k, FPT 50 cổ phiếu @ 80k, MBB 200 cổ phiếu @ 28k."
Claude:
quote → lấy giá hiện tại cho VCB, FPT, MBBaiContext → lấy trend, RSI, support/resistance cho mỗi cái(currentPrice - costPrice) × quantityOutput: Lời khuyên có cấu trúc với rationale.
Đây là phân tích thực, không hallucination. Claude có dữ liệu, Claude reason trên dữ liệu.
Claude Code có thể chạy hàng ngày và email cho bạn trading opportunities:
// Claude Code script, chạy hàng ngày lúc 4pm
const vnstock = require('vnstock-js');
async function dailyAnalysis() {
const symbols = ['VCB', 'FPT', 'MBB', 'TCB', 'HPG', 'VNM'];
const signals = [];
for (const symbol of symbols) {
const context = await vnstock.stock(symbol).aiContext();
// Bullish divergence: trend up, RSI < 70
if (context.trend === 'bullish' && context.rsi < 70) {
signals.push(`MUA: ${symbol} (RSI=${context.rsi}, trend bullish)`);
}
// Bearish reversal: trend down, volume spike
if (context.trend === 'bearish' && context.volumeSignal === 'strong') {
signals.push(`BÁN: ${symbol} (volume spike, bearish trend)`);
}
}
if (signals.length > 0) {
sendEmail('Trading Signals', signals.join('\n'));
}
}
dailyAnalysis();
Không cần API wrapper. MCP server handle tất cả data fetching.
Bạn: "Tìm cho tôi 3 ngân hàng có trend bullish và RSI < 60. Hiển thị level support/resistance của chúng."
Claude:
listing → lấy tất cả ngân hàng trên HOSEaiContext → filter theo trend + RSIHoặc: "Track watchlist của tôi hàng ngày và alert tôi nếu bất kỳ cái nào hit support hoặc break resistance."
Claude lưu watchlist trong ~/.vnstock-js/watchlist.json, check hàng ngày, gửi alerts.
Bạn đang viết trading bot trong Cursor. Bạn nói với Claude:
"Viết một function tìm stocks với RSI crossover dưới 30 trong 5 ngày qua, signal oversold conditions."
Claude:
Claude có dữ liệu thực, vậy suggestions của nó grounded.
Chúng tôi có các lựa chọn:
Chúng tôi chọn stdio vì:
Protocol là JSON-RPC 2.0:
// Claude → MCP: "Lấy quote cho VCB"
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "quote",
"arguments": { "symbol": "VCB" }
}
}
// MCP → Claude: "Đây là dữ liệu"
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"symbol": "VCB",
"matchingPrice": 105.2,
"change": 1.2,
"changePercent": 1.15,
"volume": 15234500,
"ceiling": 107.3,
"floor": 103.1,
"reference": 104.0
}
}
Simple. Efficient. Stateless.
vnstock-js MCP server cần declare 11 tools cho Claude. Mỗi tool là một schema + handler:
interface Tool {
name: string // "quote", "history", "aiContext"
description: string // Nó làm gì
inputSchema: JSONSchema // Nó nhận input gì
handler: (args) => Promise<any> // Implementation
}
const tools: Tool[] = [
{
name: 'quote',
description: 'Lấy giá cổ phiếu hiện tại, volume, ceiling/floor',
inputSchema: {
type: 'object',
properties: {
symbol: { type: 'string', description: 'Mã cổ phiếu (VCB, FPT, etc)' }
},
required: ['symbol']
},
handler: async (args) => {
const quote = await adapter.getQuote(args.symbol)
return {
symbol: quote.symbol,
matchingPrice: quote.matchingPrice,
change: quote.change,
changePercent: quote.changePercent,
volume: quote.volume,
ceiling: quote.ceiling,
floor: quote.floor,
reference: quote.reference
}
}
},
// ... 10 tools khác
]
Schema nói cho Claude:
Claude đọc schema và quyết định khi nào gọi tools dựa trên prompt của bạn.
vnstock-js có adapter pattern từ v1.0:
interface StockDataAdapter {
getQuote(symbol: string): Promise<Quote>
getHistory(symbol: string, options): Promise<HistoryData>
getCompanyInfo(symbol: string): Promise<CompanyInfo>
// ... 8 methods khác
}
class VciAdapter implements StockDataAdapter {
async getQuote(symbol: string) {
const data = await this.fetchVci('/v1/stock/quote', { symbol })
return this.transformQuoteResponse(data)
}
// ...
}
MCP server không cần adapter mới. Nó chỉ gọi adapter hiện tại:
const adapter = new VciAdapter()
const tools = [
{
name: 'quote',
handler: (args) => adapter.getQuote(args.symbol)
},
// ... 10 tools khác
]
Đây là sức mạnh của architecture: Transport layer (HTTP, MCP, CLI) riêng biệt từ business logic. Một implementation, nhiều frontends.
Developers Việt phải có thể dùng tools bằng tiếng Việt. Developers nói tiếng Anh phải dùng bằng tiếng Anh.
Chúng tôi implement bilingual descriptions:
const tools = [
{
name: 'quote',
description: 'Get current stock price | Lấy giá cổ phiếu hiện tại',
inputSchema: {
properties: {
symbol: {
type: 'string',
description: 'Stock symbol (e.g. VCB) | Mã cổ phiếu (ví dụ VCB)'
}
}
},
handler: (args) => adapter.getQuote(args.symbol)
}
]
Claude's language understanding mạnh đủ để parse bilingual descriptions và pick đúng tool.
Test:
quote ✓quote ✓VCI API không luôn nhanh. Network có thể chậm. Chúng tôi cần MCP server resilient:
1. Graceful degradation trên slow responses:
const handler = async (args) => {
const startTime = Date.now();
try {
const result = await adapter.getQuote(args.symbol);
// Nếu mất > 2s, log warning cho monitoring
if (Date.now() - startTime > 2000) {
logger.warn(`Slow query: quote(${args.symbol}) took ${Date.now() - startTime}ms`);
}
return result;
} catch (err) {
// Không crash. Trả về error với context.
return {
error: err.message,
symbol: args.symbol,
retry: true
};
}
};
2. Session caching để tránh rate limits:
const cache = new Map();
async function getCachedQuote(symbol: string) {
const key = `quote:${symbol}`;
if (cache.has(key)) {
const { data, timestamp } = cache.get(key);
// Nếu cached < 30s, trả về từ cache (VCI update mỗi 30s anyway)
if (Date.now() - timestamp < 30000) {
return data;
}
}
const data = await adapter.getQuote(symbol);
cache.set(key, { data, timestamp: Date.now() });
return data;
}
VCI có rate limit ~100 req/min. Session caching (30-60s) đảm bảo Claude có thể spam tools mà không hit limit.
3. Auto-reconnect trên connection loss:
MCP protocol handle reconnection ở transport layer. Nếu stdout break, Claude Desktop tự động restart subprocess.
Chúng tôi shipped 11 tools. Hãy group chúng:
Data Tools (8) — direct VCI API passthrough:
quote — giá hiện tại, change, volumehistory — OHLCV history cho chartingcompany — company info, industry, exchangetrading — bid/ask, average pricelisting — tất cả stocks trên exchangetopMovers — gainers + losersquickQuote — multiple quotes trong một callwatchlist — CRUD cho watchlistsAnalysis Tools (3) — computation layer:
indicators — MACD, Bollinger, ATRaiContext — trend, RSI, support/resistance, volumecompareSymbols — side-by-side comparisonTại sao separate? Vì Claude cần cả hai:
aiContext tool là secret sauce. Nó trả về structured analysis:
{
"symbol": "VCB",
"trend": "bullish",
"indicators": {
"rsi": 65.2,
"sma20": 104.5,
"sma50": 103.2,
"sma200": 102.1
},
"support": 104.0,
"resistance": 106.5,
"volumeSignal": "strong",
"priceChange": { "1d": 1.15, "7d": 2.5 }
}
Claude lấy trong một call cái mà cần 5 queries riêng lẻ.
Chúng tôi thêm 27 tests cho MCP server:
describe('MCP Server', () => {
describe('tools/quote', () => {
it('returns price data for valid symbol', async () => {
const result = await server.call('quote', { symbol: 'VCB' })
expect(result).toHaveProperty('matchingPrice')
expect(result).toHaveProperty('change')
})
it('handles invalid symbol gracefully', async () => {
const result = await server.call('quote', { symbol: 'INVALID' })
expect(result.error).toBeDefined()
})
})
describe('tools/aiContext', () => {
it('returns all required fields', async () => {
const result = await server.call('aiContext', { symbol: 'VCB' })
expect(result).toHaveProperty('trend')
expect(result).toHaveProperty('indicators')
expect(result).toHaveProperty('support')
expect(result).toHaveProperty('resistance')
})
})
// ... 25 tests khác covering:
// - Edge cases (symbols at limits, empty watchlist)
// - Error scenarios (network timeouts, invalid schemas)
// - Bilingual descriptions
// - Cache behavior
// - Performance (all calls < 2s)
})
Tests hit real VCI API (không mock) để đảm bảo MCP server works với dữ liệu thực, không imaginary responses.
Chúng tôi benchmark mỗi tool:
| Tool | P50 | P95 | P99 |
|---|---|---|---|
| quote | 120ms | 280ms | 450ms |
| history | 200ms | 350ms | 600ms |
| company | 100ms | 200ms | 350ms |
| aiContext | 250ms | 400ms | 800ms |
| indicators | 300ms | 500ms | 1000ms |
Claude có patience để chờ 1s cho analysis, nhưng sẽ frustrated với 5s latency.
Chúng tôi optimize:
compareSymbols fetch multiple symbols in parallellisting callResult: Hầu hết calls finish dưới 300ms ngay cả slow network.
Bạn: Đang cân nhắc mua VCB. Nên không?
Claude:
1. Gọi aiContext(VCB)
2. Lấy: trend=bullish, RSI=65 (neutral/overbought), support=104, resistance=106
3. Checks: "Bullish trend nhưng approaching resistance"
4. Recommend: "Mua sau pullback đến 104-105, hoặc chờ breakout rõ ràng trên 106"
Đây là information, không phải advice. Claude là research assistant, không phải broker.
Bạn: Đây là portfolio của tôi.
VCB 100 @ 50k
FPT 50 @ 80k
MBB 200 @ 28k
Total NAV: 100M
Nên rebalance không?
Claude:
1. Fetch quotes cho cả 3
2. Tính current NAV per position
3. Tính % của portfolio
4. Compare với sector benchmarks
5. Recommend: VCB & FPT overweight (27% + 22% = 49% trong 2 stocks)
"Cân nhắc trim một position và rotate sang underweight sectors"
Lại là suggestion, không prescription.
Bạn: Monitor watchlist của tôi hàng ngày lúc 4pm.
Alert nếu bất kỳ stock nào break trên 20-day high
hoặc drop dưới 20-day low.
Claude Code (scheduled):
1. Load watchlist từ ~/.vnstock-js/watchlist.json
2. Mỗi symbol, fetch history(symbol, limit=20)
3. Tính high/low
4. Fetch current quote
5. Nếu breached, send email/Telegram/Slack
Đây là automation. MCP server supply data, Claude Code supply logic.
Trước v1.4, nếu bạn muốn Claude làm research trading cho bạn, bạn phải:
Đó là DevOps problem. Chúng tôi giải quyết nó với protocol problem — MCP.
Giờ:
Barrier to entry drop từ tuần xuống phút.
v1.5 (Power Workflow) sẽ focus trên:
Bây giờ, v1.4 là foundation. Bạn có dữ liệu real-time. Bạn có analysis. Bạn có protocol hoạt động ở mọi nơi Claude đi.
Next wave của AI trading tools sẽ không là closed platforms. Chúng sẽ là Claude + MCP + dữ liệu của bạn. Open. Composable. Yours.
Đọc full MCP Server documentation →
Hoặc nếu prefer hands-on:
npm install vnstock-js
# Thêm vnstock-js MCP vào Claude Desktop config
# Bắt đầu hỏi câu hỏi
Claude giờ biết thị trường chứng khoán Việt. Bạn sẽ xây dựng gì?