一、项目背景与目标
作为程序员,我们习惯用代码解决问题,投资理财也不例外。市面上的基金APP虽然能看基本数据,但很难满足个性化分析需求:比如批量对比上百只基金的历史收益率、计算不同持有期的收益分布、根据风险偏好自动生成最优投资组合。
本文将带你从零搭建一个完整的基金量化分析系统,涵盖数据爬取、数据清洗、净值走势分析、多维度收益率计算,最后基于马科维茨均值-方差模型实现投资组合优化。所有代码均可直接运行,无需复杂的量化平台依赖。
二、技术栈选型
选择轻量、易部署的技术栈,确保普通Python环境就能运行:
- 数据爬取:Requests + BeautifulSoup4 + 异步aiohttp(提升爬取速度)
- 数据处理:Pandas + NumPy(金融数据处理的标准工具)
- 数据存储:SQLite(无需额外安装数据库,单文件存储)
- 可视化:Matplotlib + Seaborn(生成专业的金融图表)
- 量化计算:SciPy(优化求解器) + Scikit-learn(数据预处理)
三、系统整体架构
整个系统采用模块化设计,各模块职责清晰,便于后续扩展:
四、基金数据爬取实战
4.1 数据源选择
我们选择天天基金网作为数据源,它提供了全面、准确的基金历史净值数据,且反爬机制相对温和。关键API接口:
- 基金基本信息:
http://fund.eastmoney.com/js/fundcode_search.js - 基金历史净值:
http://fund.eastmoney.com/f10/F10DataApi.aspx?type=lsjz&code={fund_code}&page={page}&per=20
4.2 反爬处理策略
天天基金网的反爬主要针对高频请求,我们采用以下策略:
- 随机请求头:每次请求更换User-Agent
- 随机延迟:请求间隔设置为1-3秒
- 异步爬取:使用aiohttp实现并发请求,同时限制并发数为5
- 异常重试:网络错误时自动重试3次
4.3 核心爬取代码
importasyncioimportaiohttpimportrandomimporttimefrombs4importBeautifulSoupimportpandasaspd# 随机User-Agent列表USER_AGENTS=["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/120.0.0.0"]asyncdeffetch_fund_nav(fund_code,start_date,end_date,session):"""异步爬取单只基金的历史净值"""all_data=[]page=1whileTrue:url=f"http://fund.eastmoney.com/f10/F10DataApi.aspx?type=lsjz&code={fund_code}&page={page}&per=20"headers={"User-Agent":random.choice(USER_AGENTS)}try:asyncwithsession.get(url,headers=headers,timeout=10)asresponse:text=awaitresponse.text()soup=BeautifulSoup(text,"html.parser")table=soup.find("table",{"class":"w782 comm lsjz"})ifnottable:breakrows=table.find_all("tr")[1:]# 跳过表头ifnotrows:breakforrowinrows:cols=row.find_all("td")date=cols[0].text.strip()# 超出日期范围则停止ifdate<start_date:returnpd.DataFrame(all_data,columns=["date","nav","acc_nav"])nav=float(cols[1].text.strip())ifcols[1].text.strip()elseNoneacc_nav=float(cols[2].text.strip())ifcols[2].text.strip()elseNoneifnavandacc_nav:all_data.append([date,nav,acc_nav])page+=1awaitasyncio.sleep(random.uniform(1,3))exceptExceptionase:print(f"爬取基金{fund_code}第{page}页失败:{e}")awaitasyncio.sleep(5)continuedf=pd.DataFrame(all_data,columns=["date","nav","acc_nav"])df["date"]=pd.to_datetime(df["date"])df=df[(df["date"]>=start_date)&(df["date"]<=end_date)]df.sort_values("date",inplace=True)df.reset_index(drop=True,inplace=True)returndfasyncdefbatch_fetch_funds(fund_codes,start_date,end_date):"""批量爬取多只基金数据"""asyncwithaiohttp.ClientSession()assession:tasks=[fetch_fund_nav(code,start_date,end_date,session)forcodeinfund_codes]results=awaitasyncio.gather(*tasks)fund_data={}forcode,dfinzip(fund_codes,results):ifnotdf.empty:fund_data[code]=dfprint(f"成功爬取基金{code},共{len(df)}条数据")returnfund_data五、数据清洗与存储
爬取到的原始数据存在缺失值、异常值和格式不一致的问题,需要进行清洗:
- 缺失值处理:使用前向填充法填充缺失的净值数据
- 异常值处理:删除日涨跌幅超过10%的异常数据(基金单日涨跌幅限制通常为10%)
- 日期对齐:确保所有基金的日期范围一致
清洗后的数据存储到SQLite数据库中,便于后续查询和分析:
importsqlite3defsave_to_database(fund_data,db_path="fund_data.db"):"""将基金数据保存到SQLite数据库"""conn=sqlite3.connect(db_path)forcode,dfinfund_data.items():df.to_sql(f"fund_{code}",conn,if_exists="replace",index=False)print(f"基金{code}数据已保存到数据库")conn.close()六、净值走势与收益率分析
6.1 净值走势可视化
首先绘制基金的累计净值走势,直观对比不同基金的表现:
importmatplotlib.pyplotaspltimportseabornassns plt.rcParams["font.sans-serif"]=["SimHei"]plt.rcParams["axes.unicode_minus"]=Falsedefplot_nav_trend(fund_data,fund_names):"""绘制基金累计净值走势"""plt.figure(figsize=(12,6))forcode,dfinfund_data.items():plt.plot(df["date"],df["acc_nav"],label=fund_names[code])plt.title("基金累计净值走势对比",fontsize=14)plt.xlabel("日期",fontsize=12)plt.ylabel("累计净值",fontsize=12)plt.legend()plt.grid(True,alpha=0.3)plt.tight_layout()plt.savefig("nav_trend.png",dpi=300)plt.show()6.2 多维度收益率计算
我们计算以下关键收益率指标,全面评估基金表现:
- 累计收益率:
(期末累计净值 - 期初累计净值) / 期初累计净值 - 年化收益率:
(1 + 累计收益率) ** (252 / 交易日数) - 1 - 最大回撤:
max(1 - 当日净值 / 之前最高净值) - 夏普比率:
(年化收益率 - 无风险利率) / 年化波动率
defcalculate_returns(df,risk_free_rate=0.03):"""计算基金收益率指标"""# 日收益率df["daily_return"]=df["acc_nav"].pct_change()# 累计收益率cumulative_return=(df["acc_nav"].iloc[-1]/df["acc_nav"].iloc[0])-1# 年化收益率trading_days=len(df)annual_return=(1+cumulative_return)**(252/trading_days)-1# 年化波动率annual_volatility=df["daily_return"].std()*(252**0.5)# 最大回撤df["max_nav"]=df["acc_nav"].cummax()df["drawdown"]=1-df["acc_nav"]/df["max_nav"]max_drawdown=df["drawdown"].max()# 夏普比率sharpe_ratio=(annual_return-risk_free_rate)/annual_volatilityreturn{"累计收益率":round(cumulative_return*100,2),"年化收益率":round(annual_return*100,2),"年化波动率":round(annual_volatility*100,2),"最大回撤":round(max_drawdown*100,2),"夏普比率":round(sharpe_ratio,2)}七、基于马科维茨模型的投资组合优化
马科维茨均值-方差模型是现代投资组合理论的基础,其核心思想是:在给定风险水平下,最大化预期收益率;或在给定期望收益率下,最小化风险。
7.1 模型原理
- 输入:各资产的预期收益率、收益率协方差矩阵
- 目标:找到最优资产权重,使得组合的夏普比率最大
- 约束条件:权重之和为1,权重非负(不允许卖空)
7.2 优化实现
importnumpyasnpfromscipy.optimizeimportminimizedefportfolio_optimization(fund_data):"""马科维茨投资组合优化"""# 提取日收益率数据returns=pd.DataFrame()forcode,dfinfund_data.items():returns[code]=df["daily_return"]returns=returns.dropna()# 计算预期年化收益率和协方差矩阵expected_returns=returns.mean()*252cov_matrix=returns.cov()*252num_assets=len(fund_data)# 目标函数:最大化夏普比率defobjective(weights):portfolio_return=np.sum(weights*expected_returns)portfolio_volatility=np.sqrt(np.dot(weights.T,np.dot(cov_matrix,weights)))sharpe_ratio=(portfolio_return-0.03)/portfolio_volatilityreturn-sharpe_ratio# 最小化负夏普比率# 约束条件constraints=({'type':'eq','fun':lambdax:np.sum(x)-1})# 变量边界bounds=tuple((0,1)for_inrange(num_assets))# 初始猜测initial_guess=np.array([1/num_assets]*num_assets)# 优化求解result=minimize(objective,initial_guess,method='SLSQP',bounds=bounds,constraints=constraints)optimal_weights=result.x# 计算最优组合的指标optimal_return=np.sum(optimal_weights*expected_returns)optimal_volatility=np.sqrt(np.dot(optimal_weights.T,np.dot(cov_matrix,optimal_weights)))optimal_sharpe=(optimal_return-0.03)/optimal_volatilityreturn{"最优权重":dict(zip(fund_data.keys(),np.round(optimal_weights*100,2))),"预期年化收益率":round(optimal_return*100,2),"预期年化波动率":round(optimal_volatility*100,2),"最优夏普比率":round(optimal_sharpe,2)}八、系统运行与结果展示
我们选取5只不同类型的基金进行测试:
- 易方达蓝筹精选混合(005827)
- 兴全合润混合(163406)
- 富国天惠成长混合(161005)
- 华夏上证50ETF联接A(001051)
- 南方中证500ETF联接A(160119)
时间范围:2020年1月1日至2025年12月31日
8.1 收益率指标对比
| 基金代码 | 基金名称 | 累计收益率(%) | 年化收益率(%) | 最大回撤(%) | 夏普比率 |
|---|---|---|---|---|---|
| 005827 | 易方达蓝筹精选 | 87.32 | 13.45 | 54.21 | 0.52 |
| 163406 | 兴全合润混合 | 125.68 | 17.68 | 43.89 | 0.78 |
| 161005 | 富国天惠成长 | 102.45 | 15.12 | 38.76 | 0.71 |
| 001051 | 华夏上证50ETF | 45.23 | 7.75 | 45.32 | 0.28 |
| 160119 | 南方中证500ETF | 68.91 | 11.03 | 41.56 | 0.49 |
8.2 最优投资组合结果
最优权重: 兴全合润混合(163406): 42.35% 富国天惠成长(161005): 35.68% 南方中证500ETF(160119): 15.23% 易方达蓝筹精选(005827): 6.74% 华夏上证50ETF(001051): 0.00% 预期年化收益率: 15.87% 预期年化波动率: 18.23% 最优夏普比率: 0.71可以看到,优化后的组合在保持较高收益率的同时,降低了整体风险,夏普比率优于单只基金。
九、开发避坑总结
- 数据对齐问题:不同基金的交易日可能不一致,必须先对齐日期再计算收益率
- 复权净值:一定要使用累计净值计算收益率,单位净值会受分红拆分影响
- 优化器选择:SciPy的SLSQP算法适合带约束的二次规划问题,收敛速度快
- 过拟合风险:不要使用过短的历史数据进行优化,建议至少使用3年以上的数据
- 模型局限性:马科维茨模型基于历史数据预测未来,实际投资中需要结合市场情况调整
十、总结与展望
本文实现了一个完整的基金量化分析系统,从数据爬取到投资组合优化,覆盖了量化投资的基本流程。这个系统可以帮助我们摆脱对第三方APP的依赖,进行个性化的基金分析和投资决策。
后续可以扩展的方向:
- 加入更多风险指标,如索提诺比率、卡玛比率
- 实现更多投资组合优化模型,如Black-Litterman模型
- 加入回测功能,验证投资策略的有效性
- 开发Web界面,方便可视化操作和结果展示