在A股市场,选股是每个投资者和量化开发者都绕不开的话题。有没有一种方式,能让我们用自然语言描述选股条件,然后自动获得一份精准的股票清单?答案是肯定的!
我们并不需要自己训练AI模型来理解“金叉”、“市值小于200亿”这些语义,偶然发现东财已经存在支持自然语言的智能选股工具。我们的工作,是用Go语言搭建一个MCP协议服务器,将东财的接口“包装”成AI助手(如Claude Code、VS Code插件等)可以直接调用的工具。
本文将带你用Go语言,基于MCP协议,打造一个“智能”选股工具,并深入剖析其技术原理。
找出MACD金叉且PE小于30的股票
我要赌狗票:前5天涨停过,今天没有涨停。
什么是MCP协议?
MCP(Model Context Protocol)是一种面向AI Agent与工具集成的开放协议。它的核心思想是:让AI助手不仅能“聊天”,还能通过标准化的“工具调用”完成实际任务——比如选股、查天气、执行脚本等。
MCP协议基于JSON-RPC 2.0标准,定义了工具的注册、参数描述、调用与响应格式,极大降低了AI前端(如Claude、Cursor、VSCode插件)与后端服务之间的集成门槛。
MCP协议的典型交互流程
MCP协议本质是标准化的JSON-RPC工具调用流程。我们以“智能选股工具”为例,说明其交互过程:
1. 初始化连接
客户端(如AI助手、VS Code插件)首先通过 initialize 方法与MCP服务端建立连接,声明自身能力:
{
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {
"roots": { "listChanged": true }
},
"clientInfo": {
"name": "test",
"version": "1.0.0"
}
},
"id": 1
}
服务端响应中会包含所支持的工具列表、参数结构、协议版本等元信息。
2. 调用工具(以选股为例)
客户端通过 tools/call 方法,指定工具名和参数,发起选股请求:
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "query_stocks",
"arguments": {
"keyword": "MACD水上金叉且流通市值<200亿"
}
},
"id": 2
}
服务端收到后,会调用对应的Go处理器 queryStocksHandler,将自然语言条件转发给东方财富智能选股API,并将结构化结果返回。
3. 响应示例
服务端返回标准JSON-RPC格式的响应,此处我整理为Markdown表格,便于AI理解股票数据:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "## 筛选结果\n|代码|名称|最新价|涨跌幅|换手率|\n|----|----|------|--------|------|\n|601869|长飞光纤|115.28|-4.43%|7.85%|"
}
]
}
}
了解MCP协议后,我们聊下智能选股是如何落地的。
Go实现:工具注册与API对接
1. 工具注册与描述
我选择 github.com/mark3labs/mcp-go 这个包,通过mcp.NewTool注册了一个名为query_stocks的工具,并详细描述了它的功能和参数:
queryStocks := mcp.NewTool("query_stocks",
mcp.WithDescription("智能选股:描述你期望的股票(自然语言选股),返回股票的最新交易数据。"),
mcp.WithString("keyword", mcp.Description("选股条件,例:...")),
)
这里的keyword参数支持自然语言,比如:
- MACD金叉且PE小于30
- 最近几天涨停过但今天没有涨停的科技股;不要ST股及不要退市股;不要科创板;不要北交所
2. API接口解析
工具注册后,需要实现对应的处理器(Handler)。在queryStocksHandler中,我们将用户输入的自然语言条件封装为结构体,POST到东方财富接口:
requestBody, _ := json.Marshal(&StockSearchRequest{ ... })
req, _ := http.NewRequest("POST", "https://np-tjxg-g.eastmoney.com/api/smart-tag/stock/v3/pw/search-code", bytes.NewBuffer(requestBody))
...
resp, _ := client.Do(req)
body, _ := io.ReadAll(resp.Body)
返回的数据会用gjson解析,提取出股票列表、涨跌幅、换手率等关键信息,最终组装成Markdown表格返回。
3. MCP协议的运行机制
整个流程可概括为:
- 注册:开发者注册工具,描述其功能与参数。
- 调用:AI助手或用户通过MCP协议发起工具调用(带参数)。
- 处理:后端Handler收到请求,执行业务逻辑(如选股),返回结构化结果。
- 响应:MCP协议统一封装响应,便于AI理解和二次处理。
在本项目中,server.ServeStdio(mcpServer)负责监听MCP协议的请求,并分发到对应的工具Handler。
好的,我知道你们想看代码哈哈!文末有,麻烦点赞谢谢:)
如何本地运行和调试?
- 安装依赖:
go mod tidy
- 启动服务:
go run main.go
- 参考以下配置接入VSCode(Claude Code等都可以)。VSCode配置示例:
{
"servers": {
"mcp-server-ai-stock": {
"command": "go",
"args": ["run", "main.go"],
"cwd": "${workspaceFolder}",
"type": "stdio"
}
},
"inputs": []
}
推荐接入“秘塔搜索MCP”,可以对符合条件的股票进行二次筛选,如盈利情况、财务情况等,更多玩法大家可以评论区交流!
- 在AI助手对话框中输入你想要的股票,AI助手会理解你的需求,请求MCP服务器并整理股票数据后返回给你。
总结
MCP协议让AI不再只是“聊天机器人”,而是能真正“动手做事”的智能助手。
用Go语言实现MCP协议的选股工具,可以让AI与金融、数据、自动化等场景深度融合。无论你是AI开发者、量化工程师,还是对智能工具感兴趣的程序员,都值得一试!
本文完整代码如下,欢迎留言交流!
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/spf13/cast"
"github.com/tidwall/gjson"
)
type StockSearchRequest struct {
KeyWord string `json:"keyWord"`
PageSize int `json:"pageSize"`
PageNo int `json:"pageNo"`
Fingerprint string `json:"fingerprint"`
MatchWord string `json:"matchWord"`
ShareToGuba bool `json:"shareToGuba"`
Timestamp string `json:"timestamp"`
RequestID string `json:"requestId"`
RemovedConditionIDList []interface{} `json:"removedConditionIdList"`
OwnSelectAll bool `json:"ownSelectAll"`
NeedCorrect bool `json:"needCorrect"`
Client string `json:"client"`
DxInfo []interface{} `json:"dxInfo"`
Biz string `json:"biz"`
Gids []interface{} `json:"gids"`
}
func main() {
mcpServer := server.NewMCPServer("mcp-server-ai-stock", "1.0.0", server.WithToolCapabilities(false))
queryStocks := mcp.NewTool("query_stocks",
mcp.WithDescription("智能选股:描述你期望的股票(自然语言选股),返回股票的最新交易数据。"),
mcp.WithString("keyword",
mcp.Description("选股条件,例:\n"+
"1. 股票或板块名称:宁德时代、比亚迪、新能源、AI、白酒\n"+
"2. 技术指标:MACD、5日均线、RSI、KDJ\n"+
"3. 财务条件:ROE>15%、净利润增长率>30%\n"+
"4. 复合条件:MACD金叉且成交量放大;股价在5日均线上方;不要ST股及不要退市股;不要科创板;不要北交所"),
),
)
mcpServer.AddTool(queryStocks, queryStocksHandler)
if err := server.ServeStdio(mcpServer); err != nil {
panic(err)
}
}
func queryStocksHandler(ctx context.Context, request mcp.CallToolRequest) (res *mcp.CallToolResult, err error) {
keyword := cast.ToString(cast.ToStringMap(request.Params.Arguments)["keyword"])
// _, _ = fmt.Fprintln(os.Stderr, ">> "+keyword)
requestBody, err := json.Marshal(&StockSearchRequest{
PageNo: 1,
PageSize: 50,
KeyWord: keyword,
Fingerprint: "6d09ac0e7326d9299bb77ac2090dadd9",
Gids: []interface{}{},
Timestamp: cast.ToString(time.Now().UnixMilli()),
RequestID: "jVZgmtMhiPYtiDb7g2yferYT9aCrY1TA1758537136613",
NeedCorrect: true,
RemovedConditionIDList: []interface{}{},
DxInfo: []interface{}{},
Biz: "web_ai_select_stocks",
Client: "web",
})
if err != nil {
return &mcp.CallToolResult{Content: []mcp.Content{mcp.NewTextContent(fmt.Sprintf("序列化请求失败: %v", err))}}, nil
}
client := &http.Client{Timeout: 30 * time.Second}
req, err := http.NewRequestWithContext(ctx, "POST", "https://np-tjxg-g.eastmoney.com/api/smart-tag/stock/v3/pw/search-code", bytes.NewBuffer(requestBody))
if err != nil {
return &mcp.CallToolResult{Content: []mcp.Content{mcp.NewTextContent(fmt.Sprintf("创建请求失败: %v", err))}}, nil
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Origin", "https://xuangu.eastmoney.com")
req.Header.Set("Referer", "https://xuangu.eastmoney.com/")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0")
resp, err := client.Do(req)
if err != nil {
return &mcp.CallToolResult{Content: []mcp.Content{mcp.NewTextContent(fmt.Sprintf("请求失败: %v", err))}}, nil
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body)
if err != nil {
return &mcp.CallToolResult{Content: []mcp.Content{mcp.NewTextContent(fmt.Sprintf("读取响应失败: %v", err))}}, nil
}
g := gjson.ParseBytes(body)
if g.Get("code").Int() != 100 {
return &mcp.CallToolResult{Content: []mcp.Content{mcp.NewTextContent(fmt.Sprintf("响应非100: %s", string(body)))}}, nil
}
if g.Get("data.result.total").Int() <= 0 {
return &mcp.CallToolResult{Content: []mcp.Content{mcp.NewTextContent("未找到符合条件的股票")}}, nil
}
content := parseSearchCodeResponse(g)
// _, _ = fmt.Fprintln(os.Stderr, content)
return &mcp.CallToolResult{Content: []mcp.Content{mcp.NewTextContent(content)}}, nil
}
func parseSearchCodeResponse(g gjson.Result) string {
var (
columns []string
b strings.Builder
)
_, _ = b.WriteString("## 筛选到的股票数据\n|")
g.Get("data.result.columns").ForEach(func(_, o gjson.Result) bool {
columns = append(columns, o.Get("key").String()) // 英文字段
_, _ = b.WriteString(o.Get("title").String() + o.Get("unit").String() + "|") // 中文表头
return true
})
g.Get("data.result.dataList").ForEach(func(_, o gjson.Result) bool {
_, _ = b.WriteString("\n|")
for _, column := range columns {
_, _ = b.WriteString(strings.ReplaceAll(o.Get(column).String(), "|", ",") + "|")
}
return true
})
return b.String()
}