前端get到的新技能--手把手教你使用Python实现查询基金年度排名功能

背景

做前端好多年了, 在掘金前端热榜上,刷到的好多文章,都是旧饭新炒。这种情况说明, 前端技术已从蓝海变成红海,需要寻找新的蓝海,才能比较easy的获取到大量的新知识。那么该选择哪门编程语言作为下一个目标呢,个人感觉Python比较有前途,因为当下比较火热的AI大模型领域,编程语言用的就是Python。目标选定好了之后,我们写一个查询年度基金排名的入门级应用,练练手。废话不多说了,现在我们进入正题。

第一步 安装Python及项目依赖包

1. 安装Python

下载win最新版的Python,安装时记得勾选Add python.exe to PATH

2. 安装项目依赖的Python包

这里说两点:第一点,给pip配置永久镜像源,以提升下载速度,解决连接问题。第二点,要做环境隔离。Python项目安装依赖包时,建议设置环境隔离。

1.配置永久镜像源

常用国内镜像源有:

国内镜像源 地址
清华大学 pypi.tuna.tsinghua.edu.cn/simple
阿里云 mirrors.aliyun.com/pypi/simple
中国科技大学 pypi.mirrors.ustc.edu.cn/simple
华中理工大学 pypi.hustunique.com/simple
豆瓣 pypi.douban.com/simple

可以采用ping指令测试各个源的响应速度,例如

bash 复制代码
ping pypi.tuna.tsinghua.edu.cn
ping mirrors.aliyun.com
// ...

经过测试,发现阿里云比较快,所以将pip镜像源配置成阿里源。打开Windows: C:\Users\<你的用户名>\pip\pip.ini文件,如果没有,可以手动创建pip文件夹,并在此文件夹下创建一个pip.ini文件。填写如下内容:

bash 复制代码
[global]
index-url = https://mirrors.aliyun.com/pypi/simple

配置完之后,可以用下面的命令验证配置是否成功

bash 复制代码
pip config list

2.设置环境隔离

为什么需要环境隔离?不同项目可能依赖不同版本的库,比如: 项目A需要 pandas 版本1.3, 项目B需要 pandas 版本2.0; 如果你在系统全局安装包,没有隔离,会产生冲突,导致代码跑不起来。隔离后,每个项目的依赖包版本互不干扰,代码环境干净且可控。

如何做环境隔离呢,采用下面的命令:

bash 复制代码
cd 项目目录
python -m venv venv        # 创建虚拟环境
source venv/Scripts/activate   # win激活虚拟环境
pip install 依赖包         # 安装依赖
pip freeze > requirements.txt  # 记录依赖

# 下次打开项目
source venv/Scripts/activate
# 团队成员拉取新项目时
source venv/Scripts/activate
pip install -r requirements.txt

第二步 编写查询基金年度排名功能

1. 编写主体功能

整理思路就是:

  1. 从所有基金中筛选出可以进行T+0交易的基金。
  2. 对符合条件的基金进行排名,打印出排名前三的基金

对于T+0的判断逻辑,这里要说明一下:A股市场中,ETF的属性字段没有直接是标注"T+0"还是"T+1"交易。要判断一个ETF是支持T+0交易,主要是基于以下几个关键规则:

  • 跨境ETF:只要是投资境外市场(例如:港股、美股、日股、德股等)的ETF,并且在境内(沪深两市)上市,通常都可以进行T+0交易。这是因为这类ETF的底层资产在境外,与A股市场的交易时间存在时差,为了方便投资者交易,监管机构允许其当日买入后当日卖出。
  • 黄金ETF:在沪深交易所上市的黄金ETF,如华安黄金ETF(518880),也属于T+0交易品种。
  • 货币基金ETF:在交易所上市的货币市场基金,如华宝添益(511990),也支持T+0交易。
  • 商品期货ETF:跟踪国内商品期货指数的ETF,比如豆粕ETF、原油ETF等,也属于T+0交易。

通过关键词匹配的方式来筛选,是一个非常有效的、也是目前最主流的实现方式。

初始版本是:

python 复制代码
import akshare as ak
import pandas as pd
from datetime import datetime

def get_qdii_etf_rank():
    try:
        df = ak.fund_etf_fund_daily_em()

        # 过滤含有海外、全球、港股、美股、纳指等关键词
        df = df[df["基金简称"].str.contains("全球|纳指|标普|恒生|港股|美股|道琼斯|国际|MSCI|海外", na=False)]

        # 提取并转换"近1年"列
        df["近1年"] = pd.to_numeric(df["近1年"].str.rstrip('%'), errors="coerce")

        # 排序取前10
        top10 = df.sort_values(by="近1年", ascending=False).head(10)

        print(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 全球ETF近一年涨幅前10:\n")
        print(top10[["基金代码", "基金简称", "基金类型", "近1年"]].to_string(index=False))
    except Exception as e:
        print(f"获取 ETF 数据失败:{e}")

if __name__ == "__main__":
    get_qdii_etf_rank()

这个Python脚本用到了两个外部依赖包,它们的功能如下:

模块导入名 pip 安装包名 用途说明
akshare akshare 提供财经数据接口库,可获取股票、基金、期货、宏观等金融数据
pandas pandas 提供强大的数据处理、表格处理能力,例如 DataFrame。适用于读写 CSV、Excel、数据分析等。

执行安装命令进行安装,如果安装成功,但运行报错,大概率因为Python版本与依赖包依赖的包版本不兼容,需要把Python升级到最新版本。

js 复制代码
# 安装依赖
pip install akshare pandas
# 验证安装是否成功
python -c "import akshare, pandas, requests; print('All packages are installed')"

2. 更换数据源

查询A股所有的ETF时,老是报错: Connection aborted., ConnectionResetError(10054, '远程主机强迫关闭了一个现有的连接, 意思说客户端成功地向数据服务器(远程主机)发起了连接请求,但服务器那边因为某种原因,在数据还没传输完的时候就单方面切断了这个连接。发现是ak.fund_etf_fund_info_em()引起的,ak.fund_etf_fund_info_em() 这个函数是从东方财富(em)获取数据的。akshare 库非常灵活,它常常提供来自不同数据源(如东方财富、新浪财经、腾讯财经)的接口。当一个源不稳定时,可以尝试更换另一个。可以用 akshare 中另一个获取ETF实时行情列表的函数 ak.fund_etf_spot_em() 来代替它。虽然这个函数主要用于获取实时价格,但它同样返回了一个包含代码和名称的ETF列表,足以满足筛选的需求。

3. 手动计算排名

更换数据源之后,获取ETF列表没有问题了,可是查询排名的时候又报错

js 复制代码
第三步:正在获取所有基金的业绩排名数据...

❌ 第 1 次尝试获取基金排名数据失败: ('Connection aborted.', ConnectionResetError(10054, '远程主机强迫关闭了一个现有的连接。', None, 10054, None))

   5秒后将进行重试...

❌ 第 2 次尝试获取基金排名数据失败: ('Connection aborted.', ConnectionResetError(10054, '远程主机强迫关闭了一个现有的连接。', None, 10054, None))

   5秒后将进行重试...

❌ 第 3 次尝试获取基金排名数据失败: ('Connection aborted.', ConnectionResetError(10054, '远程主机强迫关闭了一个现有的连接。', None, 10054, None))

原因是调用东方财富出在调用ak.fund_open_fund_rank_em() 上。这个函数和最初失败的函数一样,都是从东方财富(em)获取数据,因此也同样遭到了网络拦截。既然直接获取"基金排名"这个大表格的路被堵死了,就换一种思路:不拿现成的排名表了,而是自己动手,一个一个地去查询符合条件的ETF过去一年的历史价格,然后手动计算出它们的涨跌幅。

这个方法虽然慢一点,但有两大优势:

  • 查询单个ETF历史数据是小请求,不容易被防火墙拦截。
  • 即使中途有几个ETF查询失败,程序也不会崩溃,可以跳过它们继续运行,最终能得到一个有效的结果。

将基础版修改为:

Python 复制代码
import pandas as pd
import akshare as ak
import time
from datetime import datetime, timedelta

def find_top_t0_global_etfs_professional():
    # --- 第一步:获取ETF列表 ---
    print("第一步:正在获取所有A股ETF基金列表 (使用备用接口)...")
    try:
        etf_info_df = ak.fund_etf_spot_em()
        etf_info_df.rename(columns={'名称': '基金简称'}, inplace=True)
        print("✅ ETF列表获取成功!")
    except Exception as e:
        print(f"❌ 获取ETF列表失败: {e}")
        return

    # --- 第二步:筛选全球ETF (扩展关键词) ---
    print("\n第二步:正在筛选潜在的T+0全球ETF...")
    # 关键改动:在关键词列表中加入"香港"
    keywords = ['纳指', '标普', '恒生', 'H股', '中概', '日经', '德国', '法国', '美国', '港股', '全球', '亚太', '越南', '印度', '香港']
    pattern = '|'.join(keywords)
    global_etfs_df = etf_info_df[etf_info_df['基金简称'].str.contains(pattern, na=False)].copy()

    if global_etfs_df.empty:
        print("   未能找到符合条件的全球ETF。")
        return
    print(f"   已找到 {len(global_etfs_df)} 个潜在的T+0全球ETF。")
    
    # --- 第三步:手动计算近一年涨跌幅 (增加专业过滤器) ---
    print("\n第三步:正在逐个获取ETF历史数据并计算近一年涨跌幅...")
    
    end_date = datetime.now()
    start_date = end_date - timedelta(days=365)
    end_date_str = end_date.strftime('%Y%m%d')
    start_date_str = start_date.strftime('%Y%m%d')
    
    results = []
    total_etfs = len(global_etfs_df)


    for index, row in global_etfs_df.iterrows():
        etf_code = row['代码']
        etf_name = row['基金简称']
        print(f"   正在处理 ({index+1}/{total_etfs}): {etf_code} {etf_name}...")
        
        try:
            hist_df = ak.fund_etf_hist_em(symbol=etf_code, period="daily", start_date=start_date_str, end_date=end_date_str, adjust="qfq")
            

            if not hist_df.empty and len(hist_df) :
                start_price = hist_df.iloc[0]['收盘']
                end_price = hist_df.iloc[-1]['收盘']
                yearly_return = ((end_price / start_price) - 1) * 100
                
                results.append({
                    '代码': etf_code,
                    '基金简称': etf_name,
                    '近1年涨跌幅': yearly_return
                })
                print(f"     ✅ 计算成功: {yearly_return:.2f}%")
       
            
            time.sleep(0.5)

        except Exception as e:
            print(f"     ❌ 获取或计算失败,跳过。错误: {e}")
            continue

    if not results:
        print("\n未能成功计算任何ETF的涨跌幅。")
        return

    # --- 第四步:排序和展示结果 ---
    print("\n第四步:正在对结果进行排序...")
    final_df = pd.DataFrame(results)
    top_3_etfs = final_df.sort_values(by='近1年涨跌幅', ascending=False).head(3)

    print("\n==================== 查询结果 (专业版) ====================")
    print("近一年涨跌幅前三的可T+0交易全球ETF为:")
    print("---------------------------------------------------------")

    if top_3_etfs.empty:
        print("在上市的基金中,没有找到符合条件的ETF数据。")
    else:
        top_3_etfs.reset_index(drop=True, inplace=True)
        for index, row in top_3_etfs.iterrows():
            print(f"🏆 排名 {index + 1}:")
            print(f"   - 基金代码: {row['代码']}")
            print(f"   - 基金简称: {row['基金简称']}")
            print(f"   - 近一年涨跌幅: {row['近1年涨跌幅']:.2f}%")
            print("---------------------------------------------------------")

    print("\n⚠️ 免责声明: 数据仅供参考,不构成投资建议,市场有风险,投资需谨慎。")


if __name__ == '__main__':
    find_top_t0_global_etfs_professional()

执行python main.py, 得到的结果是:

js 复制代码
近一年涨跌幅前三的可T+0交易全球ETF为:
---------------------------------------------------------
🏆 排名 1:
   - 基金代码: 159570
   - 基金简称: 港股通创新药ETF
   - 近一年涨跌幅: 133.74%
---------------------------------------------------------
🏆 排名 2:
   - 基金代码: 513090
   - 基金简称: 香港证券ETF
   - 近一年涨跌幅: 133.37%
---------------------------------------------------------
🏆 排名 3:
   - 基金代码: 159567
   - 基金简称: 港股创新药ETF
   - 近一年涨跌幅: 128.97%
---------------------------------------------------------

和同花顺查询的结果对比了一下, 同花顺显示的结果是

js 复制代码
1. 513090 | 香港证券ETF   | 134.88%
2. 159570 | 港股创新药ETF | 127.33%
3. 159567 | 港股创新药ETF | 122.98%

4. 结果差异原因

结果有些差异,造成这些差异的原因有:

  • 近一年"的计算基准日不同

    • 代码start_date = datetime.now() - timedelta(days=365),这会精确地从今天向前推365天。
    • 同花顺 :同花顺的数据更新可能是每日收盘后,或者在某些特定时间点。它所定义的"近一年"可能是从今天(交易日)的收盘价与一年前同期交易日的收盘价进行对比。例如,如果一年前的今天是一个非交易日,同花顺可能会使用上一个交易日的数据。这种细微的时间差异就会导致计算结果不同。
  • 复权方式的细微差异

    • 代码ak.fund_etf_hist_em(..., adjust="qfq"),使用的是前复权
    • 同花顺 :同花顺也使用复权数据,但其具体复权算法(比如对分红、拆分等事件的处理细节)可能与akshare接口所采用的底层数据源略有不同。尽管绝大多数情况下差异很小,但在某些特定ETF上,累积起来的微小差异就可能影响最终的百分比。

总结来说,代码逻辑是完全正确的。出现差异是由于数据源、计算基准日和复权细节上的不同。这种差异在金融数据分析中很常见,只要逻辑严谨,代码计算结果就是可靠的。

最后

今天有点事,先写到这里,下一步打算把查询出来的消息,推送到企微或微信,代码片段先保存着,未完待续。

js 复制代码
def send_wechat(msg):
    # 你需要替换成自己的 SKey,注册 Server酱:https://sct.ftqq.com/
    server_key = 'YOUR_SERVER_CHAN_KEY'
    url = f'https://sctapi.ftqq.com/{server_key}.send'
    data = {
        'title': '全球ETF涨跌幅排名(近一年)',
        'desp': msg
    }
    try:
        requests.post(url, data=data, timeout=10)
    except Exception as e:
        print(f'发送微信失败:{e}')
        
        
send_wechat(f"```\n{msg}\n```")
相关推荐
CF14年老兵17 分钟前
✅ Next.js 渲染速查表
前端·react.js·next.js
七七软件开发17 分钟前
团购商城 app 系统架构分析
java·python·小程序·eclipse·系统架构·php
七七软件开发23 分钟前
打车小程序 app 系统架构分析
java·python·小程序·系统架构·交友
司宸25 分钟前
学习笔记八 —— 虚拟DOM diff算法 fiber原理
前端
阳树阳树26 分钟前
JSON.parse 与 JSON.stringify 可能引发的问题
前端
让辣条自由翱翔30 分钟前
总结一下Vue的组件通信
前端
dyb31 分钟前
开箱即用的Next.js SSR企业级开发模板
前端·react.js·next.js
前端的日常32 分钟前
Vite 如何处理静态资源?
前端
前端的日常33 分钟前
如何在 Vite 中配置路由?
前端
兮漫天34 分钟前
bun + vite7 的结合,孕育的 Robot Admin 靓仔出道(一)
前端