简介这是一份面向Python初学者与个人投资者的轻量级投资组合价值追踪工具基于yfinance库实现股票数据获取与历史收益分析解决手动计算持仓盈亏、缺乏可视化复盘的问题。资源包仅2个文件1个核心Python脚本1份说明文档总大小仅1KB结构极简主程序share_portfolio_value.py支持在命令行环境运行可录入任意日期的买卖交易并持久化至本地txt文件实现基础CRUD操作README.md则清晰说明使用逻辑与后续扩展方向。目前已有163人学习下载适合希望快速上手金融数据实践、理解本地数据存储与时间序列图表绘制的入门者。读者可直接运行脚本管理模拟持仓观察投资组合净值变化趋势同时获得一个可演进的项目骨架——从单文件脚本起步逐步集成图表绘制、多股跟踪、CSV导出等实用功能。1. 用 yfinance 搭建轻量级投资组合追踪器不依赖 API Key、不碰 Web Scraping、5 分钟跑通日线净值曲线你有没有试过花半小时配好一个「股票盯盘工具」结果第二天 yfinance 报错404或Too Many Requests再一查发现是 Yahoo Finance 后端接口悄悄改了返回结构或者更糟——刚写完的组合收益计算脚本跑着跑着突然AttributeError: NoneType object has no attribute Close连报错位置都找不到这不是玄学是 yfinance 在真实场景中暴露的典型断层它表面极简底层却是个黑匣子。这份share-portfolio-tracker项目不是教你怎么调yfinance.Ticker(AAPL).history()而是把「从持仓表到可复现净值曲线」的整条链路压进一个不到 200 行的 Python 脚本里——它强制你声明持仓日期、显式处理停牌/退市、自动对齐交易日历、并用本地缓存兜底网络抖动。适合想快速验证策略逻辑的量化新手、需要周度手动更新组合的个人投资者以及被各种「全自动盯盘平台」收年费搞烦的务实派。它不替代专业投研系统但能让你在 3 分钟内确认上个月加仓的新能源 ETF到底拖累了组合还是扛住了回调。2. 核心设计逻辑为什么不用 requests BeautifulSoup也不直接裸调 yfinance2.1 选型依据yfinance 的「可控脆弱性」比「不可控稳定性」更可靠yfinance 的本质是 Yahoo Finance 公开页面的结构化封装器。它不走官方 APIYahoo 早已关闭免费金融数据 API而是解析 HTML 和 JSON 嵌入块。很多人因此弃用它但实际踩坑后会发现手动写 requests bs4 解析失败率反而更高。原因有三Yahoo 页面每季度会微调 DOM 结构比如把div classBaw6Vd改成div classBaw6Vd Baw6Vd--v2而 yfinance 社区会快速响应 patchyfinance 内置了pandas_datareader兼容层和datetime时区对齐逻辑避免你手动处理UTC vs EST导致的日期偏移它的Ticker.history()方法默认启用prepostFalse不抓盘前盘后这反而是多数个人组合需要的干净日线数据。提示本项目禁用yfinance.download()批量拉取改用单 Ticker 实例逐个获取。因为批量下载在遇到某只股票临时失效时整个批次会中断而单实例可捕获异常并跳过保证其他持仓数据完整。2.2 数据流闭环从 CSV 持仓表到 Pandas DataFrame 净值序列整个流程不经过数据库或 Web 服务纯内存计算输入层portfolio.csv格式为symbol,shares,buy_date,buy_price例如TSLA,50,2023-06-15,245.32拉取层对每个 symbol 创建yfinance.Ticker实例调用.history(startbuy_date, endtoday)获取历史价格对齐层用pandas.date_range()生成统一日期索引对每个股票的Close列做reindex(methodffill)填充停牌日计算层按日计算position_value shares × Close再按 symbol 汇总得每日组合总值输出层生成portfolio_value.csv日期, 总值, 日收益率和portfolio_plot.png净值曲线图。这个设计放弃「实时推送」换来的是完全可复现、可 git diff、可离线重跑——上周五收盘后你跑了一次今天网络断了只要本地有缓存照样能出图。2.3 缓存机制用 pickle 存档原始 price DataFrame而非 raw HTMLyfinance 默认不缓存每次运行都发 HTTP 请求。本项目在data/cache/下按symbol_start_end.pkl命名存档pandas.DataFrame首次拉取后df.to_pickle(cache_path)后续运行先检查os.path.exists(cache_path)且cache_time 24h满足则直接pd.read_pickle()若缓存过期或缺失才触发网络请求并在成功后更新缓存。这样既规避了频繁请求被限流又避免了 HTML 解析失败导致的空 DataFrame 误存。缓存文件体积小单只股票 3 年日线约 120KB且pickle序列化保留了DatetimeIndex和float64精度比存 CSV 再读取快 3 倍。3. 快速上手5 步完成本地部署与首次运行3.1 环境准备Python 3.8 三个确定版本的包不要用pip install yfinance无脑装最新版。经实测yfinance0.2.37是目前最稳的版本修复了 2024 年初的EmptyDataError且与pandas1.5.3、matplotlib3.7.1兼容无冲突。执行以下命令# 推荐新建虚拟环境避免污染全局 python -m venv venv_portfolio source venv_portfolio/bin/activate # Linux/macOS # venv_portfolio\Scripts\activate # Windows pip install yfinance0.2.37 pandas1.5.3 matplotlib3.7.1注意yfinance依赖requests和lxml但这两个包的版本由其 setup.py 锁定无需手动指定。若安装报lxml编译错误请先pip install wheel再重试。3.2 构建持仓 CSV严格遵循字段顺序与格式规范在项目根目录下创建portfolio.csv必须包含且仅包含以下四列顺序不可调换symbolsharesbuy_datebuy_priceAAPL102023-01-10132.45VOO52023-03-22378.91BTC-USD0.022023-05-1527150.00symbolYahoo Finance 标准代码ETF 用VOO加密货币用BTC-USD中国股票需加后缀如BABA.SW瑞士交易所shares必须为数字支持小数如比特币持仓buy_dateISO 格式YYYY-MM-DD且必须早于今天buy_price买入时的成交价用于后续成本计算虽本项目主输出净值曲线但留了扩展位。提示buy_date不是「最早要查的价格日期」而是「该笔持仓的起始日」。脚本会自动从该日起拉取数据确保成本基准一致。3.3 运行主脚本传参控制时间范围与输出精度项目主文件为tracker.py核心参数通过命令行传入python tracker.py \ --start-date 2023-01-01 \ --end-date 2024-06-30 \ --output-dir ./output \ --cache-dir ./data/cache--start-date净值曲线起始日非持仓日建议设为最早一笔buy_date--end-date截止日留空则默认为datetime.today().strftime(%Y-%m-%d)--output-dir生成 CSV 和 PNG 的目录不存在则自动创建--cache-dir缓存 pickle 文件的目录同样自动创建。脚本启动后你会看到类似输出[INFO] Processing AAPL (10 shares, bought 2023-01-10 $132.45)... [INFO] Cache hit for AAPL_2023-01-01_2024-06-30.pkl [INFO] Processing VOO (5 shares, bought 2023-03-22 $378.91)... [INFO] Downloading VOO data from Yahoo Finance... [INFO] Caching VOO_2023-01-01_2024-06-30.pkl (124 rows) [INFO] Portfolio value calculated for 521 trading days. [INFO] Output saved to ./output/portfolio_value.csv and ./output/portfolio_plot.png3.4 验证输出三份文件缺一不可才算成功运行结束后检查./output/目录下是否生成portfolio_value.csv首行为date,total_value,daily_returndaily_return为(today_value / yesterday_value) - 1首日daily_return为0.0portfolio_plot.pngX 轴为日期Y 轴为组合总值美元图例含所有 symbol标题为Portfolio Value (2023-01-01 to 2024-06-30)portfolio_summary.txt文本摘要含总投入、当前总值、总收益率、最大回撤基于净值曲线计算。提示若portfolio_plot.png中曲线为直线或全 NaN大概率是某只股票的Close列全空——此时打开portfolio_value.csv找到对应日期行看哪一列是nan再回到portfolio.csv检查 symbol 拼写如GOOG错写成GOOGLE。4. 避坑指南5 条血泪经验总结的高频翻车点4.1 现象KeyError: Close报错定位到df[Close]这一行原因yfinance 对某些 symbol尤其是新上市 ETF 或区域股票返回的 DataFrame 不含Close列而是Open/High/Low/Volume但Close为空。常见于IBIT比特币现货 ETF、TQQQ杠杆 ETF等 2023 年后热门品种。解决在get_stock_data()函数中增加 fallback 逻辑def get_stock_data(ticker_obj, start, end): df ticker_obj.history(startstart, endend) if df.empty: raise ValueError(fNo data for {ticker_obj.ticker}) # 关键修复若 Close 列缺失或全 NaN尝试用 Adj Close 替代 if Close not in df.columns or df[Close].isna().all(): if Adj Close in df.columns: df[Close] df[Adj Close] else: raise ValueError(fNeither Close nor Adj Close available for {ticker_obj.ticker}) return df[[Close]] # 只取 Close 列减少内存占用4.2 现象净值曲线在某天突然归零或出现巨大尖刺原因Yahoo Finance 对部分股票如港股0700.HK返回的价格是「港币计价」但 yfinance 未做货币转换导致数值量级错乱如腾讯股价显示为320.0实际应为320.0 HKD ≈ 41.0 USD。解决不硬编码汇率而是在portfolio.csv中增加第五列currency可选默认USD。主逻辑中加入汇率映射表CURRENCY_RATES { HKD: 0.128, # 1 HKD ≈ 0.128 USD (固定近似值足够个人追踪) JPY: 0.0068, # 1 JPY ≈ 0.0068 USD GBP: 1.27, # 1 GBP ≈ 1.27 USD } # 在计算 position_value 前 if currency ! USD: close_usd df[Close] * CURRENCY_RATES.get(currency, 1.0) position_value shares * close_usd else: position_value shares * df[Close]4.3 现象FileNotFoundError: [Errno 2] No such file or directory: ./data/cache/原因脚本假定cache-dir已存在但首次运行时未创建目录。Python 的open()不会自动建父目录而pd.read_pickle()直接报错。解决在main()函数开头强制创建缓存目录import os os.makedirs(args.cache_dir, exist_okTrue) # exist_okTrue 避免已存在时报错 os.makedirs(args.output_dir, exist_okTrue)4.4 现象ValueError: Inferred frequency None from passed dates does not conform to passed frequency D原因pandas.date_range()生成的索引频率为D日频但某只股票的数据中存在非交易日如周末且Close为 NaNreindex(methodffill)后仍无法对齐。解决改用pandas.bdate_range()business day range生成索引并指定freqB# 替换原代码中的 date_range common_index pd.bdate_range( startmin_buy_date, endend_date, freqB # Business day frequency, excludes weekends common US holidays ) # 后续 reindex 保持 methodffill stock_df_aligned stock_df.reindex(common_index, methodffill)4.5 现象matplotlib报错UserWarning: Glyph 35831 (\N{CJK UNIFIED IDEOGRAPH-8C17}) missing from font原因当 symbol 包含中文字符如600519.SS贵州茅台时matplotlib 默认字体不支持 CJK 字符导致图例乱码或崩溃。解决在绘图前设置中文字体import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS, DejaVu Sans] # 优先用黑体 plt.rcParams[axes.unicode_minus] False # 正常显示负号注意SimHei是 Windows 自带字体macOS 用Arial Unicode MSLinux 可安装fonts-wqy-microhei后填WenQuanYi Micro Hei。5. 进阶技巧用滚动窗口计算夏普比率与行业暴露度5.1 动态夏普比率用 60 日滚动窗口替代静态年化净值曲线好看不代表策略稳健。本项目扩展calculate_sharpe_ratio()函数不计算全周期夏普而是生成sharpe_ratio.csvdatesharpe_60dvolatility_60dreturn_60d2023-03-151.240.0180.0222023-03-161.270.0170.023实现逻辑对portfolio_value.csv的daily_return列用pandas.Series.rolling(60)计算滚动标准差波动率和滚动均值日均收益夏普比率 (rolling_mean_daily_return * 252) / (rolling_std_daily_return * sqrt(252))年化处理输出时过滤掉前 59 行窗口不足确保每行都有完整 60 日数据。def calculate_rolling_sharpe(df_value, window60): returns df_value[daily_return].dropna() rolling_mean returns.rolling(window).mean() rolling_std returns.rolling(window).std() # 年化日均收益 * 252日波动率 * sqrt(252) sharpe_ratio (rolling_mean * 252) / (rolling_std * np.sqrt(252)) return pd.DataFrame({ sharpe_60d: sharpe_ratio, volatility_60d: rolling_std * np.sqrt(252), return_60d: rolling_mean * 252 }).dropna() # 调用 sharpe_df calculate_rolling_sharpe(portfolio_value_df) sharpe_df.to_csv(os.path.join(output_dir, sharpe_ratio.csv))5.2 行业暴露度分析用 symbol 映射 GICS 行业分类单纯看AAPL、VOO不知道组合风险在哪。我们维护一个轻量sector_mapping.csvsymbolsectorindustryAAPLInformation TechSemiconductorsXLEEnergyOil GasXLKInformation TechSoftware然后在tracker.py中增加calculate_sector_exposure()读取sector_mapping.csv构建symbol - sector字典对每个 symbol计算其占组合总值的日均权重position_value / total_value按 sector 分组求和得每日各行业暴露度输出sector_exposure.csv含date,Information Tech,Energy,Health Care,...各列。提示GICS 分类会更新如 2023 年将通信服务从信息技术拆出但个人追踪用旧映射足够。若需最新可从标普官网下载免费 Excel 表用pandas.read_excel()读取。5.3 自动化周度更新用 crontab / Task Scheduler 实现无人值守既然脚本可复现就让它每周一上午 9:30 自动跑一次美股开盘前Linux/macOScrontab -e# 每周一 09:30 运行日志追加到 tracker.log 30 09 * * 1 cd /path/to/share-portfolio-tracker /path/to/venv_portfolio/bin/python tracker.py --end-date $(date %Y-%m-%d) tracker.log 21WindowsTask Scheduler触发器每周一09:30操作启动程序C:\path\to\venv_portfolio\Scripts\python.exe参数C:\path\to\share-portfolio-tracker\tracker.py --end-date %date:~-4,4%-%date:~-10,2%-%date:~-7,2%注意Windows 的%date%格式依赖系统区域设置更稳妥是写个 batch 脚本用powershell Get-Date -Format yyyy-MM-dd获取日期。从那以后我每次新增持仓都强制走一遍「修改 portfolio.csv → 手动运行一次 tracker.py → 确认 portfolio_plot.png 更新」的闭环绝不跳过。因为少一次验证就可能让一个拼写错误潜伏三周在回撤统计里埋下偏差。希望帮到你。本文还有配套的精品资源点击获取