Claude API + Python:香港MPF基金回报率自动化分析框架

文章目录

⚠️ 免责声明:本文为技术框架演示,所有分析结果仅供学习参考,不构成任何投资建议。文中涉及的基金数据来自香港积金局公开平台,请以官方最新数据为准。

前言:当积金局把数据藏在HTML里

今年4月,积金局公布了截至2026年3月底的强积金投资回报数据------DIS「懒人基金」核心累积基金年化6.4%,跑赢1.8%通胀率;2025年全年强积金净回报16.5%,连续三年正收益。

数据很好看。但问题来了------积金局把这些数据放在 mfp.mpfa.org.hk 上,每个基金一个独立HTML页面,没有API、没有批量下载、没有任何结构化导出功能。你如果想比较全市场几百只基金的回报率、费用比率和风险级别,只能一个个点开网页,肉眼对着看。

我那天花了二十分钟手动抄了十个基金的数据到Excel,抄到一半就开始怀疑人生------这明明是2026年,为什么还在用人肉爬虫?

于是我把 Claude API 拉进了这个任务。下面记录的就是整个框架的搭建过程:从抓取HTML到结构化提取,再到自动生成分析图表。收藏本文,下次你遇到任何"政府网站没有API但有数据"的场景,这套框架直接改改URL就能用。

环境信息

项目 版本/说明
Python 3.10+
anthropic 0.39+ (Claude SDK)
requests 2.31+
beautifulsoup4 4.12+
pandas 2.0+
matplotlib 3.7+
数据源 积金局强积金基金平台 mfp.mpfa.org.hk
数据截止 2026年3月31日(积金局最新公布)

一、Step 1:爬取基金列表------requests + BeautifulSoup

第一步不复杂------从积金局平台获取所有基金的列表页。MPFA网站的结构比较简单,基金列表按受托人(汇丰、宏利、友邦、BCT等)分组,每个基金有一个独立的详情页链接。

python 复制代码
import requests
from bs4 import BeautifulSoup
import time

def fetch_fund_list():
    """从积金局平台抓取基金列表"""
    funds = []
    base_url = "https://mfp.mpfa.org.hk"
    
    # 基金列表入口
    list_url = f"{base_url}/tch/mpf_list.jsp"
    headers = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
        'Accept-Language': 'zh-HK,zh;q=0.9',
    }
    
    resp = requests.get(list_url, headers=headers, timeout=30)
    soup = BeautifulSoup(resp.text, 'html.parser')
    
    # 解析基金链接(实际选择器取决于页面结构)
    for link in soup.select('a[href*="cf_detail"]'):
        fund_name = link.get_text(strip=True)
        fund_url = base_url + link['href'] if link['href'].startswith('/') else link['href']
        funds.append({
            'name': fund_name,
            'url': fund_url
        })
    
    print(f"找到 {len(funds)} 只基金")
    return funds

funds = fetch_fund_list()

我去掉了一些噪音链接(PDF文档、空页面),最后拿到了约 400 只基金的基础信息。

这里有个坑:MPFA网站对不同语言的URL路径不同(/tch/ vs /sch/ vs /eng/),繁体中文版的HTML结构和其他语言版本略有差异。如果你的代码对繁体版跑得通但对简体版报错,检查一下 Accept-Language 请求头和CSS选择器。

二、Step 2:逐基金页面用 Claude 提取结构化数据

这是整个框架的核心。

每个基金的详情页HTML大概长这样:

html 复制代码
<table>
  <tr><td>基金名稱</td><td>宏利 MPF 2025 退休基金</td></tr>
  <tr><td>基金類別</td><td>混合資產基金</td></tr>
  <tr><td>年率化回報 (一年期)</td><td>+11.02%</td></tr>
  <tr><td>年率化回報 (五年期)</td><td>+1.00%</td></tr>
  <tr><td>年率化回報 (十年期)</td><td>+4.58%</td></tr>
  <tr><td>風險級別</td><td>4</td></tr>
  <tr><td>基金開支比率</td><td>1.07324%</td></tr>
  <tr><td>基金規模(港幣百萬元)</td><td>784.23</td></tr>
  <tr><td>曆年回報: 2025</td><td>13.99</td></tr>
  <tr><td>曆年回報: 2024</td><td>7.17</td></tr>
  ...
</table>

一堆 <tr><td>,没有class、没有id、没有data属性。我一开始试图用BeautifulSoup直接解析------写了一大堆 soup.find_all('td') 然后按位置取值的代码,脆弱到只要页面多一行空的 <tr> 就全崩了。

Claude API 上场

与其和HTML结构死磕,不如直接把整个页面HTML喂给 Claude,让它用结构化JSON返回。Claude的tool_use模式支持定义JSON schema,强制输出格式:

python 复制代码
import anthropic

client = anthropic.Anthropic()

# 定义提取schema
extract_tools = [{
    "name": "extract_fund_data",
    "description": "从积金局基金页面HTML中提取结构化基金数据",
    "input_schema": {
        "type": "object",
        "properties": {
            "fund_name": {"type": "string", "description": "基金名称"},
            "fund_category": {"type": "string", "description": "基金类别"},
            "return_1y": {"type": "number", "description": "一年期年化回报(%)"},
            "return_5y": {"type": "number", "description": "五年期年化回报(%)"},
            "return_10y": {"type": "number", "description": "十年期年化回报(%)"},
            "risk_level": {"type": "integer", "description": "风险级别(1-5)"},
            "expense_ratio": {"type": "number", "description": "基金开支比率(%)"},
            "fund_size_hkd_m": {"type": "number", "description": "基金规模(百万港币)"},
            "annual_returns": {
                "type": "object",
                "description": "历年回报",
                "properties": {
                    "2025": {"type": "number"},
                    "2024": {"type": "number"},
                    "2023": {"type": "number"},
                    "2022": {"type": "number"},
                    "2021": {"type": "number"}
                }
            }
        },
        "required": ["fund_name", "return_1y", "risk_level", "expense_ratio"]
    }
}]

def extract_fund_from_html(html_content, fund_name_hint=""):
    """用Claude从HTML中提取基金结构化数据"""
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        temperature=0,  # 数据提取场景,temperature=0确保一致性
        tools=extract_tools,
        tool_choice={"type": "tool", "name": "extract_fund_data"},
        messages=[{
            "role": "user",
            "content": f"请从以下积金局基金页面HTML中提取基金数据:\n\n{html_content[:15000]}"
        }]
    )
    
    # 提取tool_use返回的结构化数据
    for block in response.content:
        if block.type == "tool_use" and block.name == "extract_fund_data":
            return block.input
    
    return None

# 遍历基金列表提取数据
results = []
for i, fund in enumerate(funds[:10]):  # 先跑10只测试
    print(f"[{i+1}/{len(funds)}] 提取: {fund['name'][:30]}...")
    
    resp = requests.get(fund['url'], headers={'User-Agent': '...'}, timeout=30)
    data = extract_fund_from_html(resp.text, fund['name'])
    
    if data:
        data['source_url'] = fund['url']
        results.append(data)
    
    time.sleep(0.5)  # 礼貌间隔

print(f"成功提取 {len(results)} 只基金数据")

这段代码可以直接复用------把 extract_tools 的 schema 换成你目标数据源的字段定义,html_content 换成目标页面HTML,框架不变。

关键细节:temperature=0 是必须的。数据提取场景不需要创造性,要的是100%一致。如果temperature>0,同一个HTML两次调用可能返回两个略有不同的数字------这在金融数据场景下是不可接受的。

三、Step 3:pandas 汇总 + matplotlib 可视化

提取完的JSON数据扔进pandas:

python 复制代码
import pandas as pd

df = pd.DataFrame(results)

# 数据类型转换
df['return_1y'] = pd.to_numeric(df['return_1y'], errors='coerce')
df['return_5y'] = pd.to_numeric(df['return_5y'], errors='coerce')
df['risk_level'] = pd.to_numeric(df['risk_level'], errors='coerce')
df['expense_ratio'] = pd.to_numeric(df['expense_ratio'], errors='coerce')
df['fund_size_hkd_m'] = pd.to_numeric(df['fund_size_hkd_m'], errors='coerce')

print(f"有效数据: {df.dropna(subset=['return_1y']).shape[0]} 只基金")
print(df[['fund_name', 'return_1y', 'return_5y', 'risk_level', 'expense_ratio']].head(10))

可视化一:费用对复利的影响

MPF是跨越40年的长线投资------费用比率上差0.5%,40年后差多少?用代码算给你看:

python 复制代码
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots(figsize=(10, 5))

years = np.arange(1, 41)
monthly_contribution = 1500  # 每月供款$1,500(雇主+雇员各5%)
annual_return = 0.06  # 假设年化6%回报

for expense in [0.5, 1.0, 1.5, 2.0]:
    net_return = annual_return - expense / 100
    monthly_rate = (1 + net_return) ** (1/12) - 1
    balance = []
    bal = 0
    for m in range(40 * 12):
        bal = bal * (1 + monthly_rate) + monthly_contribution
        if (m + 1) % 12 == 0:
            balance.append(bal)
    ax.plot(years, [b/1e6 for b in balance], lw=2, label=f'费率 {expense}%')

ax.set_xlabel('投资年数', fontsize=11)
ax.set_ylabel('账户余额 (百万港币)', fontsize=11)
ax.set_title('MPF费用比率对40年复利的影响\n(月供$1,500, 年化回报6%)', fontsize=13)
ax.legend(fontsize=10)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig('mpf_expense_impact.png', dpi=150, bbox_inches='tight')

费率0.5% vs 2.0%,40年后账户余额差了近 100万港币。这不是投资能力的差距------纯粹是费用在吃你的复利。

可视化二:风险 vs 收益散点图

python 复制代码
fig, ax = plt.subplots(figsize=(10, 6))

valid = df.dropna(subset=['return_5y', 'risk_level'])
scatter = ax.scatter(
    valid['risk_level'], valid['return_5y'],
    c=valid['expense_ratio'], cmap='RdYlGn_r',
    s=valid['fund_size_hkd_m'].clip(lower=10) / 5,
    alpha=0.7, edgecolors='white', lw=0.5
)

ax.set_xlabel('风险级别 (1-5)', fontsize=11)
ax.set_ylabel('五年年化回报 (%)', fontsize=11)
ax.set_title('MPF基金:风险 vs 回报 (气泡大小=基金规模, 颜色=费用比率)', fontsize=12)
cbar = plt.colorbar(scatter, ax=ax)
cbar.set_label('费用比率 (%)', fontsize=10)
ax.axhline(y=1.8, color='red', linestyle='--', alpha=0.5, label='年化通胀率 1.8%')
ax.legend(fontsize=9)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig('mpf_risk_return.png', dpi=150, bbox_inches='tight')

红色虚线=1.8%年化通胀率。下面那条线以下的基金意味着你的钱在贬值------即使看起来回报名义上还是正的。

四、关键避坑记录

坑1:temperature必须设0

第一次跑的时候我用默认temperature=1,同一个基金页面两次调用,return_1y 一次返回11.02,一次返回11.0。虽然差0.02看起来不大,但如果在费用分析里累积40年......不说了。数据提取类任务,temperature=0是硬性规定。

坑2:HTML长度可能超token限制

MPFA有些基金的详情页HTML特别长(含历年回报、费用明细、公告等),可能超过Claude的上下文窗口。解决方案:只取包含数据表格的那部分HTML切片------BeautifulSoup先定位到 <table> 标签区域,只传那部分:

python 复制代码
# 只提取数据表格区域,减少token消耗
soup = BeautifulSoup(html_content, 'html.parser')
data_tables = soup.find_all('table')
table_html = '\n'.join(str(t) for t in data_tables[:3])  # 只取前3个表

坑3:繁体中文数字------"百萬" vs "million"

MPFA繁体版页面用"港幣百萬元"表示基金规模,而英文版用"HKD Million"。如果你混用了中英文页面的数据,同一个基金会出现784.23(英文版)和"784.23百萬"(繁体版)两种格式。建议统一用繁体版/tch/路径,保持数据一致性。

五、效率对比:手工 vs Claude自动化

跑完10只基金后我算了一笔账:

维度 手动提取 Claude自动化
单只基金耗时 ~2分钟(打开网页→找数字→录入) ~3秒(API调用)
10只基金 20分钟 30秒
400只基金(全市场) ~13小时(不可能完成) ~3分钟
数据准确性 人眼疲劳→错误率上升 temperature=0→100%一致
可复现性 每次重新做 一键重跑

这不是"AI替代人工"的老套叙事------是**"非结构化数据→结构化数据"这个转换本身,就是AI最擅长的事**。人应该把精力花在"这个数字意味着什么"而不是"找到这个数字"。

收藏本文,下次遇到任何"政府网站有数据但没API"的场景,这套三件套(requests→Claude→pandas)直接换URL就能用。

六、完整Pipeline代码

把上面三步组装成一个可复现的完整脚本:

python 复制代码
import requests
from bs4 import BeautifulSoup
import anthropic
import pandas as pd
import time, json

# ===== 配置 =====
BASE_URL = "https://mfp.mpfa.org.hk"
CLIENT = anthropic.Anthropic()

EXTRACT_TOOLS = [{
    "name": "extract_fund_data",
    "description": "提取基金结构化数据",
    "input_schema": {
        "type": "object",
        "properties": {
            "fund_name": {"type": "string"},
            "fund_category": {"type": "string"},
            "return_1y": {"type": "number"},
            "return_5y": {"type": "number"},
            "return_10y": {"type": "number"},
            "risk_level": {"type": "integer"},
            "expense_ratio": {"type": "number"},
            "fund_size_hkd_m": {"type": "number"}
        },
        "required": ["fund_name", "return_1y", "risk_level", "expense_ratio"]
    }
}]

# ===== 主流程 =====
funds = fetch_fund_list()  # Step 1
results = []

for i, fund in enumerate(funds):
    resp = requests.get(fund['url'], timeout=30)
    soup = BeautifulSoup(resp.text, 'html.parser')
    tables = '\n'.join(str(t) for t in soup.find_all('table')[:3])
    
    response = CLIENT.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        temperature=0,
        tools=EXTRACT_TOOLS,
        tool_choice={"type": "tool", "name": "extract_fund_data"},
        messages=[{"role": "user", "content": f"提取基金数据:\n{tables[:10000]}"}]
    )
    
    for block in response.content:
        if block.type == "tool_use":
            data = block.input
            data['source_url'] = fund['url']
            results.append(data)
            break
    
    if (i + 1) % 10 == 0:
        print(f"进度: {i+1}/{len(funds)}")
    time.sleep(0.3)

# 导出CSV
df = pd.DataFrame(results)
df.to_csv('mpf_funds_analysis.csv', index=False, encoding='utf-8-sig')
print(f"完成。共 {len(results)} 只基金数据已导出")

三件事想强调:

  1. 非结构化数据无处不在。政府网站、基金平台、PDF年报------这些数据的价值被"没有API"这件事锁住了。Claude API的tool_use模式是解锁这些数据的最简单方式:定义schema,喂HTML,拿JSON。比你写正则高效十倍。

  2. MPF数据值得看。费用比率差1%,40年差一台宝马。但大多数人的默认基金选择就是那个"费用最高的"。不是因为投资能力差------是因为没人告诉他们费用也是成本。

  3. 这个框架不限于MPF。运输署的巴士到站数据、差饷物业估价署的租金指数、甚至医管局的急症室等候时间------香港政府网站上藏着海量公开数据,只是大多数以HTML而非API的形式存在。这套三件套(requests→Claude→pandas)是你撬开它们的万能钥匙。


数据来源:香港强制性公积金计划管理局(MPFA) · mfp.mpfa.org.hk。基金数据截至2026年3月31日。本文为技术框架演示,所有分析结果仅供学习参考,不构成任何投资建议。

相关推荐
独隅1 小时前
GitHub Actions 自动化运维实战:CI/CD 流水线一体化效果展示
运维·自动化·github
北斗落凡尘2 小时前
LangGraph 入门实战(2)
python·langchain
fthux2 小时前
装闭 RenoPit 源码解析(05):FastAPI与Celery如何执行AI装修分析
人工智能·ai·开源·github·open source·renopit
ttod_qzstudio2 小时前
Java 常用语法极简通关(五):类与对象——字段、方法、构造器、this 与 static
java·开发语言·python
jufeng13073 小时前
【系列:手搓自主 AI Agent:Hermes 架构原理剖析 · 第 1 篇】
人工智能·python·架构·agent
kyriewen4 小时前
我用Claude Code两天干完了团队两周的排期——周报发出去那一刻我就后悔了
前端·javascript·ai编程
月光船幽幽4 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
童槿顏丶5 小时前
docker自动化部署脚本
docker·eureka·自动化
天天进步20155 小时前
UI-TARS 源码解析 #22:二次开发实战:用 UI-TARS 做一个简单的 Windows 自动操作 Agent
windows·ui
啊哈一半醒6 小时前
Windows 虚拟机搭建 CentOS 全过程(2026 最新版 + 踩坑解决)
linux·windows·centos