ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FVTracker 1.22:用Python自建基金估值跟踪体系的技术实践

FVTracker 1.22:用Python自建基金估值跟踪体系的技术实践 FVTracker这个项目我从第一版写到现在断断续续迭代了大半年1.22这个版本算是把之前积累的很多问题一次性理顺了。这中间踩过的坑不少尤其是数据源稳定性、估值模型误差分析、还有盘中实时跟踪那一整套逻辑都有不少值得复盘的地方。这篇就借1.22发布的机会把整个工具的思路、架构、关键实现和这次更新的具体内容都梳理一遍希望能给同样在做基金估值跟踪、或者想自己动手写量化辅助工具的朋友一些参考。1. 为什么要写FVTracker被估值偏差坑过之后的自救1.1 表面需求天天盯盘看估值越看越不对劲买过主动型基金或场内ETF的朋友应该都有感受盘中看平台给的实时估值买完之后收盘发现实际净值和盘中估值对不上有时候偏差还很大。短则半个点长则两三个点都见过。一开始我以为是平台数据延迟后来仔细对比才发现问题出在估值口径上——平台给的估值往往基于基金定期报告披露的重仓股但基金经理调仓之后重仓股早就变了估值自然失真。更麻烦的是天天基金、蛋卷这些平台的估值算法不透明你根本不知道它用的权重是什么、更新频率是多少只能被动接受。1.2 真实需求建立一套属于自己的估值跟踪体系我需要的是自己能完全控制数据来源、计算逻辑、误差统计的一套工具。它可以做到这几件最基本的事盘中按一定频率抓取指数行情结合基金历史持仓数据自己计算实时估值。收盘后拉取基金真实净值和盘中估值做对比自动累计误差数据。把误差数据沉淀下来分析哪些基金会系统性高估、哪些低估作为后续操作的重要参考。支持自选基金列表的批量跟踪而不是一只一只手动查。基于这几点需求FVTracker最早是在2024年初开始写的。当时就是纯粹自用所以代码结构上没想那么多能用就行。但迭代到现在1.22版本已经把数据抓取、估值计算、误差跟踪、通知推送这几块拆得比较清楚了后续再扩展功能也方便。1.3 适合谁来参考如果你属于下面这几类人这个项目的思路对你应该有一定参考价值投资基金但不想完全依赖平台估值想自己验证估值准确性的投资者。有Python基础想做基金数据分析但不知道从哪里入手的开发者。想做一个低配版量化跟踪系统训练自己数据采集、清洗、计算能力的数据爱好者。我后面写的内容会涉及比较具体的代码逻辑和数据结构但不会假设你已经很熟悉Python量化那一套东西。必要的背景概念我会用通俗的话解释清楚。没有代码基础的朋友也可以只关注整体思路和更新内容再回头补代码细节。2. 整体架构与设计思路2.1 为什么用Python而不是其他语言或现成平台先说选Python的理由。其实最早我考虑过直接用Excel抓数据VBA做但试了两天就放弃了——数据量一大Excel文档刷新慢还容易崩溃更别谈定时任务和通知推送。也考虑过Node.js但Python在数据处理和分析这块的生态太成熟了pandas、requests、schedule这些都是现成的写起来效率最高。而且做这种小工具Python的开发速度优势就是最大的优势性能上完全够用。用Python还有另一个隐性好处后续如果要加机器学习模型做估值预测可以直接在同一个项目里扩展不用换语言和工具链。2.2 整体数据链路FVTracker的数据链路说简单也简单就是采集行情→计算估值→对比净值→归档误差→通知结果。但每一步拆开都有细节。数据源(指数行情/基金净值) → 采集层(requests) → 清洗层(pandas) → 计算层(估值引擎) → 存储层(SQLite) → 展示层(CLI/Web) → 通知层(推送)这样设计的好处是每一层职责单一。采集层只管把数据拿回来清洗层处理缺失值、格式转换计算层拿清洗后的数据算估值存储层负责把所有历史数据归档。改任何一层都不影响其他层。我1.22版本新增的指数估值分位功能就是在计算层加了一个模块存储层加了一张表基本没有动其他部分的代码。2.3 数据存储为什么选SQLite而不是CSV最早一版FVTracker用的是CSV存数据每个交易日生成一个文件夹里面一堆CSV文件。用了一周就发现问题了第一增量写数据很麻烦每次都要全部重读再写第二误差统计要跨日期分析时加载多个CSV文件、做笛卡尔积式关联代码写起来非常啰嗦第三没有任何约束类型转换全得自己处理。换到SQLite之后这些问题基本都消失了。尤其SQLite是单文件数据库不需要额外跑服务放在自己的项目目录里备份就是拷贝一个文件对个人工具来说再合适不过。如果你的基金数量在几十只以内每天的数据量撑死在几万行以内SQLite完全扛得住。# 初始化数据库 sqlite3 fv_tracker.db CREATE TABLE IF NOT EXISTS daily_estimate ( fund_code TEXT, trade_date TEXT, estimate_time TEXT, estimate_value REAL, real_value REAL, deviation_percent REAL );如果你不想装sqlite3命令行工具直接在Python里用sqlite3模块初始化也是一样的import sqlite3 conn sqlite3.connect(fv_tracker.db) conn.execute( CREATE TABLE IF NOT EXISTS daily_estimate ( fund_code TEXT, trade_date TEXT, estimate_time TEXT, estimate_value REAL, real_value REAL, deviation_percent REAL ) ) conn.commit() conn.close()3. 1.22版本的更新内容全解析3.1 新增指数估值分位模块这是1.22最大的一块新功能。之前FVTracker只能看单只基金的实时估值但没法和历史估值水平做对比。这次加了一个指数估值分位模块可以基于沪深300、中证500、创业板指、中证医疗等主流指数的历史PE/PB数据计算出当前估值处于近5年、近10年的什么分位水平。这个功能解决什么痛点呢举个例子你盘中看到某只医疗主题基金估值涨了2%单看数字你可能觉得涨这么多了要不要卖一点。但如果同时告诉你当前板块整体PE处于近10年的15%分位也就是历史上85%的时候都比现在贵你的操作决策可能就不一样了。FVTracker现在可以在推送通知里同时带上指数估值分位信息辅助判断是高估区域还是低估区域。具体实现上分位计算用的是最简单的经验累积概率没有搞复杂的核密度估计。逻辑就是把历史PE/PB序列拿出来排序然后看当前值排在第百分之多少import numpy as np def historical_percentile(current_value, history_values): history_sorted np.sort(history_values) count_less np.searchsorted(history_sorted, current_value, sideright) return count_less / len(history_sorted) * 100用searchsorted而不是直接写循环主要是历史数据量多了之后性能有差异。沪深300从发布到现在日频PE数据有4000多条如果每天刷一遍所有跟踪指数的分位直接起循环还是能感觉到零点几秒的延迟换成searchsorted就是毫秒级。3.2 改进估值模型加入行业权重修正讲一下1.22在估值算法上的一个关键改进。之前的估值计算逻辑相对直接根据基金定期报告披露的十大重仓股把每只股票的盘中涨跌幅按持仓权重加权算出基金估值涨跌幅。这套逻辑的问题在于十大重仓股之外的股票完全没参与计算如果基金持仓集中度低比如只有30-40%估值结果就会非常不准。1.22版本引入了行业权重修正逻辑。简单说就是定期报告的持仓数据里除了个股明细还会披露行业配置比例。FVTracker会增加一个步骤十大重仓之外的权重差额按基金所属行业的指数涨跌幅做模拟填充。行业涨跌幅数据可以直接从申万一级行业指数或者中证全指行业指数获取。下面是核心计算逻辑的简化示意def estimate_fund_with_industry_correction(stock_weights, stock_changes, industry_changes, top10_weight): # 十大重仓部分正常加权 top10_contrib sum(w * c for w, c in zip(stock_weights, stock_changes)) # 剩余仓位用行业指数涨跌幅模拟 remaining_weights max(0, 1 - top10_weight) industry_contrib remaining_weights * industry_changes # 注意还要考虑仓位比例一般基金不会满仓操作 # 假设仓位系数 0.93留存现金不参与涨跌 return (top10_contrib industry_contrib) * 0.93这里有两个容易踩的坑权重比例的计算基准要统一。定期报告里披露的个股占净值比例是占基金资产净值比例不是占股票仓位比例。如果你直接拿来和股票仓位比例相乘会低估股票部分的涨跌贡献导致估值系统性偏小。行业配置数据和个股十大重仓数据在报告里的披露时间粒度不同要按对应的报告期分别取数不能混用。加上这个修正之后FVTracker的估值和实际净值偏差在持仓集中度低的产品上改善非常明显。最典型案例是某沪深300增强基金之前盘中估值动不动差1%以上修正后偏差基本控制在0.3%以内。3.3 改进通知推送渠道统一老版FVTracker的通知是用Python的smtplib发邮件每天收盘后把跟踪结果发一封汇总邮件。但邮件有个问题手机上看邮件还是不够快而且发多了容易被邮箱当垃圾邮件。1.22版本把通知这块重构成了统一消息接口现在支持两套渠道企业微信/钉钉机器人Webhook适合盘中实时提醒通过HTTP POST把消息推送到群机器人。邮件摘要适合收盘后的每日汇总报告。Webhook的代码实现其实很简洁难点主要在消息格式拼装和失败重试。以企业微信机器人为例import requests def send_wecom_webhook(webhook_url, content): payload { msgtype: text, text: {content: content} } try: resp requests.post(webhook_url, jsonpayload, timeout10) resp.raise_for_status() except Exception as e: # 失败就记录日志不完全中断主流程 print(f[ERROR] 企业微信推送失败: {e})提示一下企业微信和钉钉的webhook地址都带一些特殊字符如果你们把配置写在代码里建议用环境变量或独立的config.json管理不要直接提交到Git。个人项目无所谓但如果以后想开源分享这个习惯能避免很多麻烦。3.4 优化SQLite增量归档与数据一致性这一块用户肉眼看不出来但对我自己维护来说价值很大。早期版本每天收市后是全量重算一次再把结果覆盖写入数据库。1.22改成了增量模式——盘中每15分钟写入一条估值快照收盘后只更新对应交易日的真实净值字段不再覆盖整个交易日的数据行。增量模式的直接好处是可以做盘中估值走势曲线而不只有收盘一个点。比如你可以看今天这只基金的估值是上午冲高之后回落还是一路稳步走强这对判断日内操作时机有参考意义。数据表结构大概是这样CREATE TABLE IF NOT EXISTS fund_estimate_snapshot ( fund_code TEXT, trade_date TEXT, snapshot_time TEXT, estimate_nav REAL, estimate_pct REAL, real_nav REAL, real_pct REAL );每天收盘后由于净值尚未公布real_nav字段为空等到晚上净值出来再执行一次update填充UPDATE fund_estimate_snapshot SET real_nav ?, real_pct ? WHERE trade_date ? AND fund_code ? AND real_nav IS NULL用WHERE real_nav IS NULL而不是直接覆盖就是为了避免重复更新已经把数据覆盖掉的风险。4. 核心模块实现与实操细节4.1 数据采集层怎么稳定地拿指数和净值数据FVTracker的数据源包括两类一类是盘中实时指数数据另一类是盘后基金净值数据。盘中实时指数数据我目前用的是腾讯和新浪的行情接口。选这两个不是因为它们多稳定而是免费、无需注册而且返回格式简单。腾讯的接口长这样https://qt.gtimg.cn/qsh000300,sz399006返回的是一段以GBK编码的文本需要解析。新浪的接口格式不同https://hq.sinajs.cn/listsh000300,sz399006注意新浪这个接口需要带Referer头否则会返回403。这是我在绝境中查了好久才发现的不用加Cookie只需要一个合法的Referer比如https://finance.sina.com.cn。import requests headers { Referer: https://finance.sina.com.cn, User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) } url https://hq.sinajs.cn/listsh000300,sz399006 resp requests.get(url, headersheaders, timeout10) resp.encoding gbk print(resp.text)基金净值数据的获取相对简单天天基金有公开的净值接口返回JSON格式直接用requests处理即可https://fundgz.1234567.com.cn/js/{fund_code}.js这里返回的其实是一个JSONP格式的文本开头多了jsonpgz(后面是JSON数据。实际开发中需要做一次剥壳处理。这个接口同时会返回盘中估值正好可以和FVTracker自己的估值做对照。4.2 估值计算引擎何时用算术平均何时用几何平均FVTracker的核心计算引擎说直白点就是拿已知持仓去模拟基金今天的变化。但有一处细节值得提出来讲就是多日累计涨跌幅的计算方式。如果你只关心当日估值涨跌直接用单日加权平均就够了。但如果你想自己画一条自跟踪以来的累积估值走势直接用每日估值涨跌幅累加会引入复利误差——比如第一天涨1%第二天跌1%简单累加得到0%但实际净值变化是负的1.01 * 0.99 0.9999。所以FVTracker在画走势图时统一用几何累积def cumulative_return(daily_returns): cum 1.0 for r in daily_returns: cum * (1 r) return cum - 1用这个几何累积方法FVTracker把平台估算累计涨幅和自己计算累计涨幅放在同一个维度对比。经过三轮迭代下来我自己测的数据里大部分基金跟踪7个交易日以上时累积估值和真实累积净值的偏差已经能控制在0.8%以内少数集中度极高、基金经理换仓频繁的基金偏差会明显拉大。4.3 自选基金列表与持仓管理自选基金列表是FVTracker的地基。1.22版本把自选基金按策略类型分成三组核心仓、卫星仓、观察仓。每组对估值误差的容忍度不同——核心仓要求估值精度最高平时跟踪频率也最高观察仓可能一周才看一次估值变化。持仓管理这块实际功能很简单记录每只基金在持仓中的份额、成本净值、当前估值然后汇总计算出整个组合的实时收益曲线。这个功能本身不复杂但有一个细节需要注意场外基金和场内基金的份额确认规则不同。场外基金申购净值按收盘确认盘中你的实时收益只是参考场内基金ETF/LOF的价格是实时变化的要按现价算。FVTracker在这两个场景下计算参考盈亏用的是不同的数据链路否则会混淆。4.4 展示层从纯命令行到Web UIFVTracker最早是命令行工具运行一条命令输出一张表。后来发现命令行适合你自己在电脑前看但人不在电脑前时想在手机上快速看一眼今天的估值情况就没法方便地实现了。所以1.22版本加了一个极简的Web UI用Flask写只有几个页面首页自选基金列表实时估值汇总。详情页单只基金的盘中估值走势图、历史误差分析。设置页通知渠道配置、跟踪频率配置。写Web UI用了不少时间但实际核心业务逻辑没变都是在调数据库里的数据。Flask的好处是轻量一个app.py就能跑起来。如果只是想本地给自己用运行python app.py后浏览器打开http://127.0.0.1:5000就行。这里强调一个实操经验Flask自带的开发服务器不擅长并发如果你同时开了Web UI和定时任务建议用waitress替代Flask默认的app.run()。直接pip安装然后替换一行代码from waitress import serve serve(app, host127.0.0.1, port5000)别小看这一步。之前我用默认服务器跑了两周经常出现Web页面卡顿、定时任务和页面请求互相阻塞的情况。换了waitress之后再没出现过。5. 常见问题与排查技巧实录5.1 数据源偶发超时重试机制怎么设计做数据采集最烦的就是网络抖动。尤其盘中行情接口偶尔请求失败在所难免。FVTracker的解决方案是三层重试第一层接口请求失败后立即重试1次第二层重试仍失败则等待5秒后再试1次第三层连续三次失败才把这条记录标记为采集失败跳过当前采集周期并写入日志。def fetch_with_retry(url, headersNone, retries3, timeout10): for i in range(retries): try: resp requests.get(url, headersheaders, timeouttimeout) if resp.status_code 200: return resp except Exception as e: print(f[WARN] 第{i1}次请求失败: {e}) if i retries - 1: time.sleep(5) return None重试逻辑看着简单但有一个容易忽略的点超时时间不要太长10秒足够。如果行情接口10秒还没返回大概率是网络拥堵或者服务器异常继续等待只会拖慢整个采集循环。5.2 估值偏差过大先查四点再怀疑算法用了FVTracker一段时间后你会发现部分基金估值偏差始终很大。这里我总结了一套排查顺序供参考第一检查持仓报告期。如果基金已经过了定期报告披露期比如4月底一季度报披露后到7月中报披露前基金经理可能已经做了大幅调仓你的估值自然对不上。这种情况叫持仓陈旧误差不是算法能解决的只能通过降低权重修正来缓解。第二检查股票仓位比例。不同基金的仓位差异很大股票型基金通常在80%-95%之间灵活配置型可能在50%-80%之间。如果你用固定的0.93仓位系数套用在所有基金上偏差会很明显。1.22版本之后FVTracker支持按基金类型设置默认仓位系数可以在设置页手动覆盖。第三检查分红除权。基金分红时净值会下降但估值计算如果不考虑分红除权就会产生假跌。FVTracker在每年分红季会特别容易踩这个坑。解决办法是读取基金的每10份派现公告在估值计算中把分红转成等额净值增量加回去。第四检查行业修正的方向。行业权重修正适用于重仓集中度低的基金但如果基金经理风格非常极致——比如个股集中持有、前十大重仓占比80%以上——行业修正反而可能引入误差。这类基金建议关闭行业修正只使用十大重仓加权。5.3 盘中估值和净值确认时间不一致一个很容易忽视的问题是盘中估值用的是实时点位而基金净值结算用的是收盘点位但两者对应的截点不同。实际操作中指数价格在15:00前最后一笔交易后还会有变动部分算法会用收盘集合竞价价格计算而FVTracker的盘中快照用的是实时成交价两者在数据含义上存在细微差别。我给的建议是不要强求盘中估值和最终净值严格一致而是关注误差的统计分布。FVTracker的误差分析页会计算每个交易日估值与净值偏差的均值、标准差、最大偏离值。只要偏差均值在正负0.2%以内标准差不超过0.5%这个估值体系就是可用的没必要追求完美的单点预测。从统计学角度看盘中估值本质上就是一个实时预测预测允许有误差但误差的分布应该是稳定的。如果你某一天看到偏差突然大幅放大优先排查当日是否发生了突发事件比如指数大幅低开、行业板块巨震这类情况下的误差放大属于系统性问题第二天的估值偏差通常就会恢复正常。5.4 多通道数据不一致以谁为准FVTracker的数据源不止一个实际运行中经常出现不同数据源给出不同指数点位的情况。以沪深300为例腾讯行情、新浪行情、中证指数官网的实时点位都可能存在细微差异。我的处理原则是以追踪指数的官方源为准其他数据源只做备用。对沪深300、中证500这类指数中证指数官网公布的实时点位最权威但接口稳定性不如腾讯新浪。所以FVTracker默认优先用腾讯新浪但每天收盘后会从官方源拉一次收盘价校正盘中数据可能存在的偏差。这样设计主要是为了数据一致性。盘中你不需要百分百精确的点位但统计分析需要的是同一口径下的数据序列。如果在误差分析里今天用A源、明天用B源统计出来的误差分布完全是乱的没法用。6. 个人使用体验与实用建议6.1 这一个多月用下来我观察到什么FVTracker跑了一个多月跟踪了12只不同风格的基金我自己对这版本的核心感受是亮点不太在于准确预测净值——短期预测本身就有天花板——而在于现在终于可以建立一个统计可靠的偏差基线。过去靠平台估值做判断你根本说不清估值涨了1%是真实涨了还是算法漂移。现在通过累计误差数据分析对每只基金的估值可信度心里有数有的基金偏差标准差只有0.2%盘中看到估值大跌就能直接当参考有的基金偏差稳定在1%以上那就只能当模糊的温度计不能用来做精细决策。这个知道哪些可参考、哪些不可参考的能力我认为比单纯提高估值精度的价值还大。交易决策里最大的风险不是判断错误而是一种无意识的盲目信任——把误差当成真相。FVTracker做的事是把误差从潜意识里拉到台面上让数据本身告诉你这个信号到底可不可信。6.2 量化辅助工具的核心思维写这个工具过程中我最大的体会是金融数据的复杂性从来不在于计算而在于口径一致性和数据质量治理。你写一个加权平均函数很简单但你要搞清楚每一份数据的场景、定义和边界条件才是最耗时间和精力的。比如上文提到的占净值比例和占股票仓位比例的区别很多新手在写估值模型时会踩进去好几天才出来。所以如果你也想动手写类似的基金跟踪工具我的建议是先不要急着优化算法先把数据字典和口径搞清楚把每一条数据从哪来、怎么清洗、能回答什么问题都写清楚。这个工作不值钱但后面所有准确性和稳定性的基础都建立在上面。6.3 下一步想做什么后续迭代有几个方向。一是把机器学习模型融进来用历史误差、行业轮动、市场波动率等特征构建一个纠偏模型对纯规则估值做二次修正。二是把指数估值分位功能做得更细除了宽基指数增加更多细分主题指数的历史分位数据。三是移动端适配目前Web界面在手机上能看但体验一般考虑做一套PWA或直接用小程序承载。但这些功能都会等到FVTracker真正稳定运行一段时间再动。工具的价值在于持续积累的数据量而不在于功能多。数据积累得越久统计口径越一致后续能做的分析和决策支持就越靠谱。这是我从这个项目里得到的最大收获也是保持这个更新节奏的根本原因。
返回列表