Skip to content

Commit 8074a87

Browse files
author
FutuAPI Developer
committed
feat: add MCP server, formatters, and updated docs
- Add formatters.py with markdown formatters for AI consumption - Add mcp_server.py with FastMCP server exposing 7 tools - Update pyproject.toml with [mcp] optional dependency and hhxg-mcp entry point - Update README.md with MCP Server configuration guide - Add test_formatters.py and test_mcp_server.py (48 tests total)
1 parent e6afdee commit 8074a87

7 files changed

Lines changed: 545 additions & 1 deletion

File tree

README.md

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,72 @@ snapshot = client.get_snapshot(force=True) # 跳过内存缓存
128128

129129
---
130130

131+
## MCP Server — AI 工具直接调用
132+
133+
安装 MCP 依赖:
134+
135+
```bash
136+
pip install hhxg[mcp]
137+
```
138+
139+
### Claude Desktop
140+
141+
编辑 `~/Library/Application Support/Claude/claude_desktop_config.json`
142+
143+
```json
144+
{
145+
"mcpServers": {
146+
"hhxg": {
147+
"command": "hhxg-mcp"
148+
}
149+
}
150+
}
151+
```
152+
153+
### Cursor
154+
155+
在 Cursor Settings > MCP 中添加:
156+
157+
```json
158+
{
159+
"mcpServers": {
160+
"hhxg": {
161+
"command": "hhxg-mcp"
162+
}
163+
}
164+
}
165+
```
166+
167+
### Claude Code
168+
169+
```bash
170+
claude mcp add hhxg hhxg-mcp
171+
```
172+
173+
### 可用 MCP 工具
174+
175+
| 工具 | 说明 |
176+
|------|------|
177+
| `get_snapshot` | 完整日报快照(最全面) |
178+
| `get_market` | 市场赚钱效应、涨跌分布 |
179+
| `get_hot_themes` | 热门题材及龙头股 |
180+
| `get_sectors` | 行业/板块资金流向 |
181+
| `get_ladder` | 连板天梯 |
182+
| `get_hotmoney` | 游资龙虎榜 |
183+
| `get_news` | 焦点新闻 |
184+
185+
配置完成后,直接对 AI 说「今天 A 股怎么样」「哪些题材最热」即可。
186+
187+
---
188+
131189
## AI 生态联动
132190

133191
hhxg 是[恢恢量化](https://hhxg.top) AI 生态的一部分:
134192

135193
| 接入方式 | 适用场景 | 状态 |
136194
|----------|----------|------|
137195
| **Python SDK**(本项目) | 量化研究、数据分析 | ✅ 可用 |
138-
| **MCP Server** | Claude Desktop / Cursor | 🚧 开发中 |
196+
| **MCP Server**(本项目) | Claude Desktop / Cursor / Claude Code | ✅ 可用 |
139197
| **GPT Action** | ChatGPT 自定义 GPT | 🚧 开发中 |
140198

141199
---

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ dependencies = [
4545
"pydantic>=2.0.0",
4646
]
4747

48+
[project.optional-dependencies]
49+
mcp = ["mcp>=1.0.0"]
50+
51+
[project.scripts]
52+
hhxg-mcp = "hhxg.mcp_server:main"
53+
4854
[project.urls]
4955
Homepage = "https://hhxg.top"
5056
Repository = "https://github.com/hhxg-top/hhxg-python"

src/hhxg/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ def get_news() -> list[NewsItem]:
124124
"HhxgError",
125125
"NetworkError",
126126
"SchemaError",
127+
# formatters
128+
"formatters",
127129
# version
128130
"__version__",
129131
]

src/hhxg/formatters.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
"""Human-readable formatters for AI tool output.
2+
3+
Each function takes a typed model and returns a markdown string
4+
optimized for LLM consumption.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from .models import (
10+
Hotmoney,
11+
LadderDetail,
12+
Market,
13+
HotTheme,
14+
NewsItem,
15+
SectorGroup,
16+
Snapshot,
17+
)
18+
19+
20+
def format_snapshot(snap: Snapshot) -> str:
21+
"""Format the full snapshot as a concise AI-readable summary."""
22+
parts = [f"# A 股日报快照 — {snap.date or '未知日期'}"]
23+
parts.append(f"\n> {snap.disclaimer}")
24+
25+
s = snap.ai_summary
26+
parts.append(f"\n## 市场概况\n- {s.market_state}")
27+
parts.append(f"- {s.focus_direction}")
28+
parts.append(f"- {s.theme_focus}")
29+
parts.append(f"- {s.hotmoney_state}")
30+
parts.append(f"- {s.news_highlight}")
31+
32+
if snap.market:
33+
parts.append(f"\n{format_market(snap.market)}")
34+
if snap.hot_themes:
35+
parts.append(f"\n{format_hot_themes(snap.hot_themes)}")
36+
if snap.sectors:
37+
parts.append(f"\n{format_sectors(snap.sectors)}")
38+
if snap.ladder_detail:
39+
parts.append(f"\n{format_ladder(snap.ladder_detail)}")
40+
if snap.hotmoney:
41+
parts.append(f"\n{format_hotmoney(snap.hotmoney)}")
42+
if snap.focus_news:
43+
parts.append(f"\n{format_news(snap.focus_news)}")
44+
45+
parts.append(f"\n---\n数据来源: [恢恢量化](https://hhxg.top)")
46+
return "\n".join(parts)
47+
48+
49+
def format_market(m: Market) -> str:
50+
"""Format market overview."""
51+
lines = [
52+
"## 市场赚钱效应",
53+
f"- 日期: {m.date}",
54+
f"- 赚钱效应: {m.sentiment_index}% ({m.sentiment_label})",
55+
]
56+
if m.total:
57+
lines.append(f"- 个股总数: {m.total}")
58+
if m.struct_diff is not None:
59+
lines.append(f"- 结构差值: {m.struct_diff}")
60+
if m.limit_up is not None:
61+
lines.append(f"- 涨停: {m.limit_up} 炸板: {m.fried} 跌停: {m.limit_down}")
62+
if m.promotion_rate:
63+
lines.append(f"- 晋级率: {m.promotion_rate}")
64+
65+
if m.buckets:
66+
lines.append("\n| 分类 | 今日 | 昨日 | 方向 |")
67+
lines.append("|------|------|------|------|")
68+
for b in m.buckets:
69+
lines.append(f"| {b.name} | {b.count} | {b.prev} | {b.dir} |")
70+
71+
return "\n".join(lines)
72+
73+
74+
def format_hot_themes(themes: list[HotTheme]) -> str:
75+
"""Format hot themes table."""
76+
lines = [
77+
"## 热门题材",
78+
"| 题材 | 涨停数 | 净流入(亿) | 龙头股 |",
79+
"|------|--------|-----------|--------|",
80+
]
81+
for t in themes:
82+
leaders = ", ".join(s.name for s in t.top_stocks[:2])
83+
net = f"{t.net_yi:.2f}" if t.net_yi is not None else "-"
84+
lines.append(f"| {t.name} | {t.limitup_count} | {net} | {leaders} |")
85+
return "\n".join(lines)
86+
87+
88+
def format_sectors(groups: list[SectorGroup]) -> str:
89+
"""Format sector fund-flow data."""
90+
lines = ["## 行业/板块资金流向"]
91+
for g in groups:
92+
lines.append(f"\n### {g.label}")
93+
if g.strong:
94+
lines.append("\n**强势:**")
95+
lines.append("| 名称 | 净流入(亿) | 领涨 | 偏离% |")
96+
lines.append("|------|-----------|------|-------|")
97+
for s in g.strong:
98+
net = f"{s.net_yi:.1f}" if s.net_yi is not None else "-"
99+
bias = f"{s.bias_pct:.1f}" if s.bias_pct is not None else "-"
100+
lines.append(f"| {s.name} | {net} | {s.leader or '-'} | {bias} |")
101+
if g.weak:
102+
lines.append("\n**弱势:**")
103+
lines.append("| 名称 | 净流入(亿) | 领涨 | 偏离% |")
104+
lines.append("|------|-----------|------|-------|")
105+
for s in g.weak:
106+
net = f"{s.net_yi:.1f}" if s.net_yi is not None else "-"
107+
bias = f"{s.bias_pct:.1f}" if s.bias_pct is not None else "-"
108+
lines.append(f"| {s.name} | {net} | {s.leader or '-'} | {bias} |")
109+
return "\n".join(lines)
110+
111+
112+
def format_ladder(detail: LadderDetail) -> str:
113+
"""Format the consecutive limit-up ladder."""
114+
lines = ["## 连板天梯"]
115+
116+
if detail.lb_rates_map:
117+
rates = ", ".join(f"{k}板→{v}" for k, v in sorted(detail.lb_rates_map.items()))
118+
lines.append(f"晋级率: {rates}")
119+
120+
for level in detail.levels:
121+
names = ", ".join(
122+
f"{s.name}({s.code})" if s.code else s.name
123+
for s in level.stocks[:5]
124+
)
125+
suffix = f" +{level.count - 5}只" if level.count > 5 else ""
126+
lines.append(f"- **{level.boards}板** ({level.count}只): {names}{suffix}")
127+
128+
if detail.area_counts:
129+
top_areas = sorted(detail.area_counts.items(), key=lambda x: -x[1])[:5]
130+
lines.append(f"\n地域分布: {', '.join(f'{k}({v})' for k, v in top_areas)}")
131+
if detail.concept_counts:
132+
top_concepts = sorted(detail.concept_counts.items(), key=lambda x: -x[1])[:5]
133+
lines.append(f"概念分布: {', '.join(f'{k}({v})' for k, v in top_concepts)}")
134+
135+
return "\n".join(lines)
136+
137+
138+
def format_hotmoney(hm: Hotmoney) -> str:
139+
"""Format hotmoney / Dragon-Tiger board data."""
140+
lines = [
141+
"## 游资龙虎榜",
142+
f"- 日期: {hm.date}",
143+
]
144+
if hm.total_net_yi is not None:
145+
lines.append(f"- 合计净买入: {hm.total_net_yi:.2f} 亿")
146+
147+
if hm.top_net_buy:
148+
lines.append("\n**净买入 TOP:**")
149+
lines.append("| 股票 | 净买入(亿) | 占比% |")
150+
lines.append("|------|-----------|-------|")
151+
for b in hm.top_net_buy:
152+
ratio = f"{b.ratio_pct:.1f}" if b.ratio_pct is not None else "-"
153+
lines.append(f"| {b.name} | {b.net_yi:.2f} | {ratio} |")
154+
155+
if hm.seats:
156+
lines.append("\n**知名游资席位:**")
157+
for seat in hm.seats:
158+
stocks_str = ", ".join(
159+
f"{s.name}({'+' if s.net_yi >= 0 else ''}{s.net_yi:.2f}亿)"
160+
for s in seat.stocks[:5]
161+
)
162+
lines.append(f"- **{seat.name}**: {stocks_str}")
163+
164+
return "\n".join(lines)
165+
166+
167+
def format_news(items: list[NewsItem]) -> str:
168+
"""Format news items."""
169+
lines = ["## 焦点新闻"]
170+
for n in items:
171+
lines.append(f"- [{n.cat}] {n.title}")
172+
return "\n".join(lines)

0 commit comments

Comments
 (0)