搞基金套利的人,最怕的其实不是行情不涨,而是机会摆在眼前,你根本没看见。LOF基金溢价套利这个事,窗口期往往就几分钟,等人肉刷新页面看到溢价,价格早就被资金抹平了。我用Python做了一套自动化监控脚本,开盘期间自动轮询几十只LOF的场内价格和盘中估算净值,实时计算溢价率,一旦超过阈值就通过微信推消息给我。全天不用盯盘,手机接收预警,看到消息再去券商App确认下单,比我以前手动刷盘要靠谱太多。
这篇文章不卖源码,也不讲那种“一键躺赚”的夸张故事,我只把整套方案的原理、数据结构、关键代码逻辑以及实际运行中踩过的坑讲清楚。无论你是已经会Python想要做量化监控的人,还是第一次听说LOF这个词但对自动化监控感兴趣,都可以把这篇文章当作一份可直接复现的参考手册。
1. 溢价套利的本质:为什么盯盘盯不住机会
1.1 LOF基金为什么会出现溢价
LOF基金,中文全称是上市型开放式基金。它有两个交易渠道:一个是通过基金公司或代销平台按净值申购和赎回,另一个是在交易所内像股票一样按实时价格买卖。这两个渠道并存,理论上价格应该无限接近净值,但实际因为场内买盘的强弱、市场情绪、资金进出速度不一样,二级市场的交易价格经常和基金净值出现偏差。
当场内交易价格高于基金净值的时候,就叫溢价。反过来,场内价格低于净值,就叫折价。对套利者来说,溢价和折价都是机会。溢价出现时,可以在场外按净值申购LOF份额,到账后转入场内卖出,赚取价格和净值的差价;折价出现时,则在场内买入份额,再转场外赎回。思路不复杂,难点在于价格波动太快,净值又是实时估算的,靠人工盯住几十个品种几乎不可能。
1.2 人工盯盘的痛点和自动化的价值
我最早尝试过用行情软件加自选股列表盯LOF,结果很受挫。LOF的溢价不是一直挂在那里等你的,它往往在开盘后的半小时内突然冒出来,持续不到十分钟就被套利资金抹平。而且一只基金出现溢价,常常会带动同板块的其他LOF一起动,人工根本来不及逐个切换查看。
自动化监控解决的就是这个“覆盖范围”和“响应速度”的问题。脚本可以每60秒把整个自选池全部扫一遍,溢价率超过设定阈值的品种直接推送微信。我在服务器上跑这套脚本,运行时间甚至超过了我的睡眠时间。它不管你是吃饭、开会还是睡觉,只要触发条件就发消息,这才是机器盯盘的意义。
1.3 完整监控链路看起来是什么样
整套监控系统的数据流并不复杂,大致是四个环节:数据采集、指标计算、逻辑判断、消息推送。数据采集从公开接口获取LOF的盘中估算净值和场内实时价格;指标计算负责把两者转成溢价率;逻辑判断决定是否触发预警;消息推送把结果通过微信发到手机上。
这四步单独拆开来看都不难,难点在于把它们稳定地串联起来长期运行。数据接口偶尔失效、网络偶尔抖动、行情偶尔跳变,这些都是实际运行中一定会遇到的问题。所以下面我会把每一步涉及的技术选型和关键判断讲透,并附上能跑的Python参考实现。
2. 动手前的关键判断:数据源、指标口径、预警通道
2.1 三种数据要分清:收盘净值、盘中估值、场内价格
很多刚接触LOF套利的人会被各种概念绕晕,我建议先把三种数据彻底分清楚。
第一种是收盘净值,也就是基金公司每天晚上公布的单位净值,它是前一日的结算结果,精确但滞后。第二种是盘中估值,是第三方平台根据基金最新披露的持仓组合、用实时股价估算出来的净值,它在交易时间内持续变化,但只是估算值,和当天晚上的真实净值会有偏差。第三种是场内实时交易价格,就是LOF在交易所里买卖的成交价。
溢价套利要盯的是“盘中估值”和“场内价格”,不是收盘净值。用收盘净值算,数据已经过期,没有任何参考价值。这个口径问题一定要搞清楚,否则后面整个计算逻辑都会偏。
2.2 溢价率公式和阈值怎么定
溢价率的计算公式不复杂:
溢价率(%) = (场内价格 - 盘中估算净值) / 盘中估算净值 × 100如果算出来是正数,代表溢价;负数代表折价。这个值越大,套利空间越大,但真正能落袋的收益还要扣除申购费、赎回费、场内卖出佣金等成本。
基于常见费率,我建议把预警阈值分成三档:第一档是1.5%,属于“开始关注”级别,通常覆盖不了交易成本,但可以记录观察;第二档是2.5%,属于“可以考虑”级别,对于部分低费率渠道已经具备操作价值;第三档是3.5%,属于“重要预警”级别,这类机会相对稀少,往往伴随明显的市场情绪推动,值得立即处理。
需要注意的是,估算净值和真实净值之间存在误差,这个误差可能在0.5%甚至更高。所以我更倾向于把阈值稍微定高一些,宁可少收到几条消息,也不要频繁收到假信号。我最终在自己的脚本里长期运行的阈值就是2.5%,并在3.5%设置了更醒目的推送。
2.3 微信推送选Server酱还是PushPlus
微信预警通道,我测试过Server酱、PushPlus和企业微信群机器人三种方式,各有取舍。
Server酱的使用门槛最低,只需要在网站上绑定微信,拿到一个SendKey,然后调用HTTP接口就能把消息推到微信。个人使用完全免费,消息里有富文本内容可以展示表格。PushPlus也类似,同样是HTTP接口,页面稍微复杂一点。企业微信群机器人的好处是不依赖第三方平台,通过Webhook直接推到企业微信群里,适合团队共同盯盘,但对网络环境有要求,而且邀请机器人进个人微信群目前不太方便。
我最后选择了Server酱作为主力推送通道。原因很简单:免费、稳定、接口简洁,一条GET或POST请求就能触发推送,非常适合脚本调用。如果你已经有企业微信,也可以使用群机器人方式,毕竟少一道第三方依赖,链路更可控。
2.4 调度方案:定时任务优于死循环
写Python监控脚本,常见的做法有两个:一是用while True加time.sleep让脚本自己循环,二是用调度库在指定时间触发任务。刚开始写监控脚本时我也喜欢用死循环,觉得这样代码简单,但实际跑了几天就发现问题:死循环里只要一个请求卡住,后面所有数据都会跟着延迟,而且脚本崩了之后不会自动恢复。
后来我改用schedule库做固定间隔触发,加上APScheduler做更精细的交易时段控制。每个监控周期之间相互独立,就算某一只基金的数据请求超时,也不会拖垮整个调度。用APScheduler的CronTrigger可以精确控制在交易时段内运行,比如周一到周五的9:30到11:30、13:00到15:00,非交易时段直接跳过,省得收盘后还收到无效信息。
3. Python实现:从零搭一个能跑的监控脚本
3.1 准备工作:Python环境与依赖
我用的是Python 3.9以上版本,主要依赖库包括requests、pandas、schedule和apscheduler。requests负责请求数据和推送消息,pandas用来做数据处理和记录,schedule负责分钟级触发,apscheduler做交易日定时控制。
安装依赖很简单,直接用pip批量安装即可:
pip install requests pandas schedule apscheduler如果是在本地Windows上跑,安装完以后可以直接在终端运行脚本;如果在服务器上跑,建议配合systemd或者Docker做进程守护,避免SSH断开后脚本被终止。我在稳定运行阶段选择了服务器加systemd的部署方式,比本地电脑挂机要可靠得多。
3.2 第一步:抓取基金盘中估值
盘中估值数据,我用的是天天基金公开的估值接口。接口地址格式是固定的,把基金代码拼进去即可:
http://fundgz.1234567.com.cn/js/{基金代码}.js接口返回的是JSONP格式,例如:
jsonpgz({"fundcode":"161005","name":"富国天惠成长混合(LOF)A","jzrq":"2025-04-15","dwjz":"2.9051","gsz":"2.8974","gszzl":"-0.26","gztime":"2025-04-15 15:00"})字段含义分别是:jzrq是净值日期,dwjz是前一日单位净值,gsz是当前估算净值,gztime是估值时间。在Python里,用正则把JSON部分提取出来,转换成字典就行。
import requests import re import json def fetch_estimate(code: str) -> dict: url = f"http://fundgz.1234567.com.cn/js/{code}.js" resp = requests.get(url, timeout=5) resp.encoding = "utf-8" match = re.search(r"jsonpgz\((.*?)\)", resp.text) if not match: return {} data = json.loads(match.group(1)) return { "estimate_nav": float(data["gsz"]), "estimate_time": data["gztime"], "nav_date": data["jzrq"], "name": data["name"], }要注意,这个接口在非交易时间也会返回数据,但估算净值是最后一天收盘时的状态,盘中判断时一定要先看gztime是否属于当前交易日,避免拿过期数据算溢价。
3.3 第二步:获取场内实时价格
场内价格我走的是东方财富的行情接口。LOF基金在交易所的代码有规律,上海市场的LOF一般以50、51、52开头,深圳市场的LOF一般以16开头。在东方财富接口里,secid参数区分市场,上海市场用1作为前缀,深圳市场用0作为前缀。
def get_market_price(code: str) -> float: market = "1" if code.startswith(("50", "51", "52")) else "0" secid = f"{market}.{code}" fields = "f43" url = "https://push2.eastmoney.com/api/qt/stock/get" params = {"secid": secid, "fields": fields} resp = requests.get(url, params=params, timeout=5) j = resp.json() if j.get("data") is None: return 0.0 price = j["data"]["f43"] / 100 return pricef43字段在接口里返回的是“价格乘以100”的整数,所以需要除以100转换成正常的价格单位。不同接口的字段含义可能随版本变动,刚接入时最好先手工打印一次完整响应,确认字段位置和数量级再写逻辑。
3.4 第三步:计算溢价并加“冷静期”
有了估算净值和场内价格,溢价率就很好算了。但我建议把溢价率计算封装成一个独立函数,同时记录时间和基金名称,方便后面的消息推送和日志归档。
def calc_premium(price: float, estimate_nav: float) -> float: if price <= 0 or estimate_nav <= 0: return 0.0 return (price - estimate_nav) / estimate_nav * 100直接发预警还有一个问题:某个LOF可能在几分钟内连续触发多次,如果每次都推送,手机通知会变成灾难。我的做法是给每只基金设置一个“冷静期”,比如15分钟内只推一次,之后再触发才重新提醒。
last_alert = {} COOLDOWN_SECONDS = 15 * 60 def should_alert(code: str) -> bool: now = time.time() last = last_alert.get(code, 0) if now - last < COOLDOWN_SECONDS: return False last_alert[code] = now return True冷静期时间不宜太长,否则机会都走完了才提醒,毫无意义;也不宜太短,否则连续震荡会刷爆手机。我测试下来,10到20分钟是比较合理的区间。
3.5 第四步:微信预警推送
我使用Server酱作为推送通道。首先需要在Server酱官网绑定微信并获取SendKey,然后调用接口发送消息。标题部分用基金代码和预警类型,内容部分把溢价率、场内价格、估算净值、触发时间全部带上,方便手机上直接判断。
def send_wechat(title: str, content: str) -> bool: sendkey = "你的SENDKEY" url = f"https://sctapi.ftqq.com/{sendkey}.send" data = {"title": title, "desp": content} resp = requests.post(url, data=data, timeout=10) result = resp.json() return result.get("code") == 0第一次测试时建议先发一条测试消息,确认微信能收到,再接入监控循环。推送失败时,脚本要记录日志,不能静默吞掉异常。
3.6 第五步:整合成定时监控任务
主监控函数需要按顺序执行:遍历监控列表、取估值、取价格、算溢价、判断阈值、推送预警。每个品种的请求异常都要单独捕获,不能让一只基金的数据问题拖垮整个监控进程。
import schedule import time WATCH_CODES = ["161005", "501018", "163415", "160119", "501078"] ALERT_LEVEL1 = 2.5 ALERT_LEVEL2 = 3.5 def monitor_job(): for code in WATCH_CODES: try: estimate = fetch_estimate(code) if not estimate: continue price = get_market_price(code) premium = calc_premium(price, estimate["estimate_nav"]) if premium >= ALERT_LEVEL2 and should_alert(code): content = ( f"价格: {price:.3f}\n" f"估算净值: {estimate['estimate_nav']:.3f}\n" f"溢价率: {premium:.2f}%\n" f"估值时间: {estimate['estimate_time']}" ) send_wechat(f"【重要】{code} {estimate['name']} 溢价 {premium:.2f}%", content) elif premium >= ALERT_LEVEL1 and should_alert(code): content = ( f"价格: {price:.3f}\n" f"估算净值: {estimate['estimate_nav']:.3f}\n" f"溢价率: {premium:.2f}%" ) send_wechat(f"【关注】{code} {estimate['name']} 溢价 {premium:.2f}%", content) except Exception as exc: print(f"{code} 处理异常: {exc}") continue schedule.every(1).minutes.do(monitor_job) while True: schedule.run_pending() time.sleep(1)这个脚本是最小可运行版本,大约六十行代码,已经能把溢价机会实时推送到手机上。接下来要做的,就是把它丢到服务器上长期运行。
4. 跑在生产环境:稳定性、日志和那些坑
4.1 数据源不稳定怎么办
第三方接口不保证全年无休,我实际跑下来的确遇到过几次接口超时、返回空数据、甚至是返回昨天旧数据的情况。应对策略有三层。
第一层是超时控制。requests请求全部加上timeout参数,默认5秒,宁可放弃这次采样,也不能让脚本卡死。第二层是异常重试。对于偶发性的网络抖动,连续重试两次往往就能拿到数据,但单只基金重试次数不要超过三次,否则会拖慢整个监控周期。第三层是数据新鲜度校验。拿到估值数据后一定要检查gztime是不是当前时间,如果发现返回的是非交易时段数据,直接跳过,不要写进日志里当有效记录。
另外,不要把接口返回的字段当成永久稳定的。接口升级、字段调整都是有可能的,定期手工抽查一次数据,确认解析逻辑仍然正确,这是运维监控脚本的基本习惯。
4.2 别让重复预警把手机塞满
很多人把监控脚本跑起来之后遇到的第一个问题不是没信号,而是信号太多。盘中某一刻估值跳一下,溢价率突然超过阈值,但几分钟后价格回到正常区间,脚本又会推送一次“恢复”或者“再次触发”的消息。如果不做冷却处理,一个交易日下来手机能收到几十条无效通知。
我的做法是双保险:冷静期加上状态标记。冷静期保证同一只基金最少间隔15分钟才能再次推送;状态标记记录每只基金上一轮的溢价状态,只有状态从“未触发”变成“触发”时才推送。这样一天下来,真正收到的消息数量非常可控,每一条背后都对应一个值得看的机会。
还有一个小细节:推送内容要包含完整的计算字段,而不仅仅是溢价率。因为在手机上看到消息时,往往已经过了几十秒,行情又变了,如果只有溢价率而没有价格、净值的对比,就没法快速判断现在的真实空间。
4.3 日志要留,复盘要勤
监控脚本不是部署完就不管的,我也走过一段“只顾跑、不看结果”的弯路。头一个月脚本天天推消息,我也跟着跑了几次操作,但盈亏结果并不理想,后来一查日志才发现,问题不在监控,而在我没有把推送和后续成交数据关联起来复盘。
建议脚本至少记录两类日志:第一类是采样日志,每轮监控完成后,把每个品种的价格、估值、溢价率、时间追加到CSV文件或数据库;第二类是预警日志,记录每次推送触发的具体原因和阈值档位。采样的数据积累一段时间后,可以用来统计不同基金、不同时段的溢价频率和幅度,找到真正值得监控的品种。
我现在的做法是每天收盘以后自动生成一个当日汇总文件,包含所有监控品种的溢价次数、最大溢价、持续时间。这个文件既是复盘依据,也是调整阈值的参考。没有数据积累就去调整预警参数,那基本是拍脑袋。
4.4 安全边界:估值不等于真实净值,留足成本余量
最后还是要泼一盆冷水。盘中估算净值是基于基金季报持仓推算出来的,基金经理实际持仓和季报公布的组合之间往往有一段时间差,所以估算误差是所有LOF套利监控都无法回避的问题。盘中看到3%的溢价,扣除估算误差和交易成本后,实际能落袋的利润可能只剩下一个零头。这也是我不建议把预警阈值放到1.5%以下的原因,信号太多,操作价值却不高。
成本测算也要提前做好。场外申购LOF有申购费,场内卖出有佣金,赎回拿钱还要等待到账时间,资金占用本身也有成本。把这些全部算完以后,如果溢价空间仍然可观,才值得出手。监控脚本的价值是帮你发现“可能存在机会”的品种,而不是代替你完成交易决策。
我个人在实际操作里的体会是:这套Python自动化监控脚本最大的价值不是让你抓住每一个套利机会,而是让你从“被动盯盘”变成“被动收消息”。把监控跑稳了之后,我每天花在看盘上的时间大大减少,反而更有精力去研究真正重要的东西。如果你也想搭一套类似的系统,建议先从小范围开始,选三五只自己熟悉的LOF,跑两周纯日志模式,不急着接微信推送,等摸清了数据波动规律再打开预警。稳定压倒一切,消息越少,说明你的公式越可靠,真正来消息的时候,才值得你放下手里的事情去看一眼。