vnstock-js

vnstock-js

Tài LiệuVí DụBài ViếtTài Chính
k

© Copyright 2026

Trở về Bài Viết

Chủ Nhật, 24 tháng 5, 2026

vnstock-js v1.4: MCP Server cho cổ phiếu Việt

Đăng bởi

TR

Tran Tu Quang

@ttqteo

cover

vnstock-js v1.4: MCP Server cho cổ phiếu Việt

v1.4.0 thêm một MCP server vào vnstock-js. Nói ngắn gọn: Claude đọc được dữ liệu chứng khoán Việt Nam mà bạn không phải dán tay, cũng không phải tự dựng API.

Trước đó, muốn nhờ Claude phân tích VCB hay so sánh FPT với MBB thì chỉ có hai đường. Một là tự lấy dữ liệu rồi dán vào khung chat, làm lại mỗi lần hỏi. Hai là tự viết API riêng, kèm deploy và bảo trì.

Bài này ghi lại vì sao tôi chọn MCP, dùng được vào việc gì, và phần kỹ thuật bên dưới.

Vấn đề

Claude có giới hạn dữ liệu huấn luyện, nên không biết giá cổ phiếu hôm nay. Muốn nó biết thì phải có đường dẫn dữ liệu vào, và trước MCP thì đường đó thường là chính bạn:

Hỏi Claude, Claude bảo cần dữ liệu, bạn đi lấy, bạn dán vào, Claude phân tích

Cách này có mấy chỗ bất tiện. Mỗi câu hỏi mới lại phải lấy dữ liệu mới. Bạn không thực sự đang trò chuyện mà đang làm người vận chuyển. Dùng Claude Desktop ở nhà, Claude Code ở công ty, Cursor trong IDE thì mỗi nơi phải tích hợp riêng. Và tới lúc dán xong thì số liệu đã cũ vài phút.

MCP giải quyết thế nào

MCP (Model Context Protocol) là chuẩn kết nối do Anthropic đưa ra, cho phép mô hình AI gọi tới nguồn dữ liệu và công cụ bên ngoài mà không cần dựng riêng cho từng ứng dụng.

Với vnstock-js, điều đó có nghĩa là:

  • Claude Desktop nói chuyện với vnstock-js qua stdio, không mở cổng, không qua mạng
  • Claude Code và Cursor dùng chung một file config, viết một lần
  • Dữ liệu lấy trực tiếp lúc hỏi, không phải bản đã cũ
  • Chạy cục bộ trên máy bạn

Vì sao thị trường Việt Nam cần

Thị trường Việt Nam không có API mở chính thức. VCI là nguồn dữ liệu nhưng thiết kế cho web của họ, không phải cho AI đọc.

Trong khi đó dữ liệu cần dùng thì nhiều: báo giá, lịch sử, chỉ báo, thông tin doanh nghiệp. Giá lại thay đổi liên tục nên số liệu cũ gần như vô dụng.

Thay vì viết mười cái wrapper cho mười ứng dụng, tôi viết một MCP server chạy được ở mọi nơi Claude có mặt.

Dùng được vào việc gì

AI Portfolio Analyzer

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:

  1. Gọi tool quote → lấy giá hiện tại cho VCB, FPT, MBB
  2. Gọi tool aiContext → lấy trend, RSI, support/resistance cho mỗi cái
  3. Tính PnL của bạn: (currentPrice - costPrice) × quantity
  4. So sánh với market (FPT có outperform market không? VCB có overbought không?)
  5. Recommend: HOLD VCB, TRIM FPT (lên 25%, ở resistance), DCA vào MBB (trend bullish, dưới SMA50)

Output: Lời khuyên có cấu trúc với rationale.

Claude phân tích trên số liệu lấy được lúc đó, không phải trên trí nhớ.

Automated Trading Signals

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 phải viết API wrapper, MCP server lo phần lấy dữ liệu.

Research Assistant

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:

  1. Gọi listing → lấy tất cả ngân hàng trên HOSE
  2. Cho mỗi ngân hàng, gọi aiContext → filter theo trend + RSI
  3. Format kết quả với support/resistance
  4. Bạn nhận được bảng, không phải một bức tường text

Hoặ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.

Integration với Cursor

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:

  1. Fetch history cho nhiều stocks qua MCP
  2. Tính RSI
  3. Detect crossover
  4. Viết optimized code
  5. Suggest improvements dựa trên dữ liệu thực

Gợi ý của Claude dựa trên số liệu thật chứ không phải phỏng đoán.

Bên dưới nó hoạt động thế nào

Vì sao dùng stdio

Có ba lựa chọn:

  1. HTTP API: Standard, widely understood, heavy
  2. WebSocket: Real-time, persistent, overkill cho request-response
  3. stdio: Lightweight, secure by default, không expose port

Tôi chọn stdio vì:

  • Security by default: MCP server chạy trên máy của bạn, nói chuyện với Claude trên máy của bạn. Không expose port, không network roundtrip.
  • Claude Desktop native: Claude Desktop được design để launch subprocesses và communicate qua stdin/stdout.
  • Zero configuration: Không cần specify ports, IPs, hay lo collisions.
  • Low latency: Direct parent-child process communication vs HTTP handshake.

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
  }
}

Giao thức đơn giản, không giữ trạng thái giữa các lần gọi.

Khai báo tool

Ở v1.4, MCP server khai báo 11 tool cho Claude. Con số này đã tăng ở các bản sau. 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:

  • Input nào valid
  • Tool trả về gì
  • Khi nào phù hợp để gọi

Claude đọc schema và quyết định khi nào gọi tools dựa trên prompt của bạn.

Adapter Pattern

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.

Hỗ Trợ Bilingual

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:

  • English prompt: "Get the price of VCB" → Gọi quote ✓
  • Vietnamese prompt: "Lấy giá VCB" → Gọi quote ✓

Error Handling & Resilience

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.

Bộ công cụ

v1.4 có 11 tool, chia hai nhóm:

Nhóm dữ liệu (8 tool), gọi thẳng API VCI:

  • quote: giá hiện tại, change, volume
  • history: OHLCV history cho charting
  • company: company info, industry, exchange
  • trading: bid/ask, average price
  • listing: tất cả stocks trên exchange
  • topMovers: gainers + losers
  • quickQuote: multiple quotes trong một call
  • watchlist: CRUD cho watchlists

Nhóm phân tích (3 tool), tính toán tại chỗ:

  • indicators: MACD, Bollinger, ATR
  • aiContext: trend, RSI, support/resistance, volume
  • compareSymbols: side-by-side comparison

Tại sao separate? Vì Claude cần cả hai:

  • Raw data cho exploration ("Cho tôi MACD cho VCB")
  • Pre-computed analysis cho decision-making ("VCB có overbought không?")

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ẻ.

Kiểm thử

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.

Performance Profile

Chúng tôi benchmark mỗi tool:

ToolP50P95P99
quote120ms280ms450ms
history200ms350ms600ms
company100ms200ms350ms
aiContext250ms400ms800ms
indicators300ms500ms1000ms

Claude có patience để chờ 1s cho analysis, nhưng sẽ frustrated với 5s latency.

Chúng tôi optimize:

  • Caching: 30s cache cho quote, 60s cho aiContext
  • Parallel requests: compareSymbols fetch multiple symbols in parallel
  • Lazy init: symbols list chỉ load trên first listing call

Result: Hầu hết calls finish dưới 300ms ngay cả slow network.

Vài tình huống dùng thật

Investment Decision

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.

Portfolio Rebalancing

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.

Automated Watchlist

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.

Thay đổi thực tế

Trước v1.4, muốn Claude tra cứu dữ liệu chứng khoán thì bạn phải tự viết API wrapper, tự host, tự giữ cho nó chạy, tự lo bảo mật. Đó là việc vận hành, và nó đứng chắn giữa bạn với thứ bạn thực sự muốn làm.

MCP biến việc đó thành một file cấu hình. vnstock-js lo phần dữ liệu, MCP lo phần kết nối, còn bạn chỉ cần đặt câu hỏi.

Sắp tới

Bản kế tiếp tập trung vào dữ liệu mức thị trường: độ rộng, thanh khoản, khối ngoại, và bối cảnh thị trường cho AI đọc.

Bắt Đầu

Đọ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ì?