躺平挖 alpha,听起来像段子,但做投研相关工作的朋友应该都体会过:真正让人心累的往往不是“想不出策略”,而是一遍遍重复劳动把你的精力和耐心磨没了。这个系列叫“日常工作流优化”,前两篇分别讲了数据处理和回测框架,今天这篇是第三篇,主题落到自动化编排上——把数据更新、因子计算、批量回测、结果汇总全部串成一条可以无人值守的流水线,把每天动手的时间压缩到半小时以内。核心关键词就两个:alpha(我们想挖的超额收益)和工作流优化(我们为了挖到它而不得不解决的工程问题)。
散户和研究员最大的区别其实不在于信息渠道,而在于能否把研究过程做成一条稳定、可迭代、不依赖个人情绪的管道。手工操作越多,出错概率就越大,策略想法被淹没在琐事里的概率也越大。我自己的目标很直白:机器能跑完的事绝不用手点,每天需要我人工介入的动作越少越好。这篇文章就把我这半年调整之后的日常研究流水线完整拆开,包含每一步的设计思路、代码骨架和踩过的坑,适合正在搭自己研究框架、或者已经有一堆脚本但跑起来很费劲的人参考。
1. 先说清楚:我理解的“躺平挖alpha”不是不干活,而是不干重复活
很多人一听到“躺平挖alpha”,第一反应是搞个全自动机器人,挂上策略躺着收钱。我要先把这层幻想戳破:自动化的价值不在替你决策,而在替你把脏活累活接走。真正需要人来做判断的地方,一点都不能省。
1.1 手工流程里的时间黑洞:我的时间到底去哪了
在把这套流程自动化之前,我每天的节奏大致是这样的:早上先打开行情软件手动更新数据,导出来到本地 Excel 里,拼接表头、处理停牌标记,然后再打开回测脚本,改几个参数跑一遍候选因子,等结果的时候顺手写几行备注,结果出来之后截图、复制到周报表格里。每个步骤单看都不重,五到十分钟能搞定,但问题出在它们之间的依赖关系上——数据没导好,回测就没法跑;回测没跑完,就没法写备注;写备注的时候又可能要回头改参数,一改又得重跑。
我后来认真统计过一周的时间分布,结果是:真正花在“想策略”上的时间不到一半,剩下全在数据整理、等待回测、复制粘贴和任务切换上。心理学里有个概念叫注意力残留,从一个任务跳到另一个任务的时候,大脑并不会立刻进入状态,而是要花几分钟回忆“我这一步跑到哪了”。按每天十五次任务切换、每次三分钟来算,光是切换损耗就是四十五分钟。还没算上数据源报错、复权因子算错、某天忘记更新这种意外情况。
我常用一个做饭的类比:做三菜一汤,真正颠勺炒菜的时间其实没多少,大量时间耗在洗菜、切菜、刷锅、擦灶台,做完一顿饭腰酸背痛,一半力气都花在收拾上了。手工做研究就是这个状态。工作流优化的本质,就是把“洗锅擦台”的动作集中起来交给机器,让灶台是干净的,锅是热的,我只负责把菜下锅和决定火候。
1.2 自动化之后,我的研究节奏变成了什么样
把流程理顺之后,我现在的工作节奏更接近这样:
| 时段 | 自动化之前 | 自动化之后 |
|---|---|---|
| 收盘后 | 手工下载数据、拼接、检查 | 定时任务自动增量更新并体检 |
| 晚上 | 手动跑回测,盯着进度条 | 因子计算 + 批量回测自动完成 |
| 睡前 | 整理截图,写备注 | 收到一份日报,直接看结论 |
| 第二天早上 | 重新核对前一天的表格 | 只看告警项和候选因子池 |
至于省下来的时间,我并没有全部拿去“跑更多因子”。恰恰相反,我刻意压低了测试数量,把精力挪到阅读、复盘和验证逻辑上。自动化真正带给我的不是“多测了一百个因子”,而是让“持续做研究”这件事不需要靠意志力硬撑。一个可持续的日常流程,比某一周疯狂加班更有价值。
2. 把日常研究管线拆成四条可自动化的流水线
整个研究流程听起来很复杂,但拆开看就是四个环节:取数、算因子、回测、出报告。这四条流水线可以各自独立,也可以用统一的调度串起来。我建议所有人都先按这个思路拆分,因为只有模块边界清楚,自动化的时候才知道该在哪儿加检查、在哪儿做重试。
2.1 数据层:增量更新、复权因子和入库体检
数据层是整个管道的地基。我见过太多人把精力全放在回测模型上,结果数据源一个小错误就让整个研究白跑一周。这个层做三件事:增量更新、复权因子处理、数据体检。
增量更新的逻辑很简单:记录当前库里最大的交易日期,然后只抓取这个日期之后的数据。关键的一个细节是,不能只把新日期 append 进去,因为复权因子一变,历史价格全部要跟着改。所以在我的实现里,每次更新会先拉取最新的复权因子表,然后删除旧数据段,再整体重写。这样不仅保证价格一致,也保证了任务可重复执行——跑两遍和跑一遍结果完全相同。
def update_daily_bars(data_api, db_conn): latest = db_conn.query("SELECT MAX(trade_date) FROM daily_bars").scalar() if latest: new_data = data_api.fetch_bars(start_date=latest + timedelta(days=1)) else: new_data = data_api.fetch_bars(start_date=START_DATE) if new_data.empty: return "no_new_data" # 复权因子整体重算,避免历史价格不一致 adjust_factors = recompute_adjust_factor(new_data) # 先删除后插入,保证幂等 db_conn.execute("DELETE FROM daily_bars WHERE trade_date > ?", (latest,)) db_conn.insert_with_adjust(new_data, adjust_factors) return "updated"数据体检经常被忽略,但它才是真正决定“无人值守能不能放心睡觉”的部分。我现在的规则很简单:当天入库的股票数量不能低于全市场正常数量的 90%,最新日期不能落后于预期交易日超过一天,关键字段的缺失率超过阈值就视为失败。这几个规则一旦触发,直接中止后续管道并告警,而不是带着脏数据继续往下算。
2.2 因子层:让特征计算像自来水一样随开随用
因子层解决的是“怎么批量、快速地算出几十上百个特征”的问题。我见过很多刚起步的人,每个因子单独写一个脚本,数据读取、格式转换、缺失处理各自为政。这种做法最大的问题是复用成本高——新因子要复制一份模板改半天,而且很难保证所有因子用的是同一套数据口径。
我的做法是做一个统一的因子算子库,每个因子函数只做一件事:输入标准化的行情 DataFrame,输出一个因子列。输入输出格式提前约定好,因子与因子之间不互相调用,这样任何一个因子崩溃都不会影响其他因子。
顺手说一个后台经常有人搜的问题:python 中 alpha 是什么意思。在很多刚接触量化的人眼里,alpha 似乎是一种神秘的信号,其实在 Python 的回归模型里,alpha 就是回归截距项。比如你用statsmodels跑y = alpha + beta * x,模型输出的const就是 alpha,代表剔除掉因子暴露之后剩下的平均超额收益。我们平时说的挖 alpha,本质上就是在不断寻找那些能让这个截距项稳定为正、而且不太随市场环境切换而失效的特征组合。这两层含义是连通的:先理解回归里的 alpha 是什么,才能理解我们要挖的东西是什么。
def momentum_factor(df, lookback=20): return df["close"] / df["close"].shift(lookback) - 1 def volatility_factor(df, lookback=20): return df["close"].pct_change().rolling(lookback).std() FACTOR_REGISTRY = { "momentum_20": momentum_factor, "volatility_20": volatility_factor, }实际运行时,我会在配置文件里声明要计算哪些因子,管道遍历注册表,把所有结果拼成一张宽表。新因子诞生时只需要写一个算子函数、注册一次,之后就跟自来水一样随开随用。这个“接口约定”比任何花哨的回测框架都重要。
2.3 回测层:模板化回测和批量参数扫描
回测层最忌讳的是每个因子一套回测代码。我的建议是抽象成一个模板,所有因子都走同一个流程:拿因子值、生成信号、模拟交易、计算绩效。模板要做的是把固定流程固定下来,允许变的部分收敛为参数。
至于因子预测力的度量,我日常看两个核心指标。一个是 IC,也就是每个截面期上因子值与下一期收益的秩相关系数,衡量这个因子对横截面收益排序的预测能力。另一个是 ICIR,等于 IC 的均值除以标准差,衡量预测力的稳定性。打个比方,IC 像是在判断一个人投篮准不准,ICIR 则是在判断他是不是每场都这么准、还是偶尔爆发一次。
def run_backtest(factor_name, params): factor_data = load_factor(factor_name) signals = generate_signals(factor_data, params) returns = simulate_next_day_returns(signals) summary = compute_ic_and_icir(factor_data, returns) return summary批量参数扫描就是穷举几个候选参数组合,然后把每个组合的结果塞进回测结果表。这一步要注意先后次序:先拿一组默认参数跑通全流程,确认数据、信号、模拟三个环节都没问题,再放开参数组合批量跑。否则一旦数据源有问题,几百组参数全部白算。
2.4 报告层:让人 5 分钟看懂当天结果的摘要
最后一步是报告层,也是很多人自动化流程里最容易敷衍的一环。回测结果多了以后,人工根本看不过来,如果报告不聚焦,自动化反而变成新的信息噪音。
我现在的日报长这样:每个因子一行,包含因子名、参数组合、IC 均值、ICIR、年化、最大回撤、换手率,最后加一列自动标记。标记规则是提前写好的,比如 IC 小于 0.02 且 ICIR 小于 0.3 标记为“弱相关”,换手率太高标记为“高换手”,最大回撤超过阈值标记为“需关注”。
报告的目的不是把所有数字堆上去,而是让人在五分钟内给出三个判断:今天有没有值得进一步看的候选因子,有没有触发的告警,昨天关注的因子今天是否失效。如果看完报告想不起该干什么,这份报告就不合格。
3. 调度与失败恢复:真正决定“躺平”质量的部分
四条流水线写完之后,把它们串起来的过程才是真正决定“躺平”质量的部分。调度好不好用,失败能不能及时发现,重跑是不是安全,每个细节都直接影响你能不能安心睡个好觉。
3.1 为什么我从 cron 换到了 Python 调度器
最早我用的是 crontab,每天下午五点半触发一个 shell 脚本,按顺序跑四个环节。一开始没问题,但任务变多之后缺点就暴露了:任务之间的依赖关系在 shell 里表达得很别扭,某个环节失败也没有一个统一的通知机制,想重跑某个环节还得手动登录服务器敲命令。
后来我把调度逻辑收拢到一个 Python 文件里,用最轻量的方式管理任务依赖。没有上 Airflow、Luigi 这类重型工具,理由很简单:个人研究管道节点少、依赖固定、并发要求低,杀鸡用牛刀只会增加维护成本。
tasks = [ Task("fetch_data", update_daily_bars, depends_on=[]), Task("calc_factors", calculate_all_factors, depends_on=["fetch_data"]), Task("run_backtests", run_batch_backtests, depends_on=["calc_factors"]), Task("push_report", compose_and_push_report, depends_on=["run_backtests"]), ] for task in tasks: task.run()这个编排逻辑的核心就一句话:按依赖顺序顺序执行,前一个成功才进入下一个,失败就停下来发告警。顺序执行比并发执行更容易定位问题。每天这些任务加起来也就几十分钟,完全不需要并发加速。
3.2 结果通知与失败告警:无人值守不等于无人发现
自动化之后最怕的一件事,是任务跑挂了没人知道,第二天打开电脑才发现昨天的流程根本没跑完。这个问题靠定时检查是治标不治本,正确做法是让系统主动找人。
我现在用的是群机器人推送,企业微信和钉钉都行,原理都差不多,就是往 webhook 地址 POST 一条消息。正常日报推送一次,失败告警单走一条通道推送。关键经验是:告警通道必须独立于主流程存在,即便主流程把数据库写坏了、把消息队列堵了,告警也要能发出来。我甚至保留了一份 SMTP 邮件兜底,因为 webhook 偶尔也会因为网络原因挂掉。
def send_alarm(title, content): url = "https://your_webhook_url" data = {"msgtype": "text", "text": {"content": f"[{title}] {content}"}} requests.post(url, json=data)推送内容要有足够上下文,不能只说“任务失败”。要包含哪个任务失败、异常堆栈、日志文件路径、下一次重试时间。有一次我的数据拉取任务在凌晨失败,早上七点自动重试成功,我醒来只看到一条“已自动恢复”的消息,这种体验才是真正的省心。
3.3 失败重试、幂等性与一次空数据事故
网络接口偶尔抖一下是很正常的,所以对网络请求类的任务,我都会加重试机制。策略很简单:失败重试三次,每次等待时间翻倍,超过次数再进入告警流程。不要无限重试,否则某个接口持续故障时,你的脚本会反复撞墙到第二天。
“幂等性”是另一个容易被忽略的概念。简单说就是同一个任务执行两次和执行一次,最终结果必须一样。我处理数据写入时统一采用“先删除日期段,再插入新数据”,就是为了保证重跑不会产生重复记录。如果直接 append,重跑两次就会得到两份重复数据,后面所有因子和回测都会跟着错。
我踩过最典型的一次坑,是某个数据源返回了 HTTP 200,看起来一切正常,但内容其实是空列表。当时管道没做最小行数校验,把空数据当成“今天没有新数据”处理,结果因子表里全是 NaN,回测结果看起来就像一个神奇的市场崩盘日。我排查了半天才意识到是空数据问题。后来我在每个任务后面都加了“数据量合理性”检查,宁可多一次误报,也不能放过一次静默失败。
4. 别让自动化替你过度思考:挖 alpha 流程中最容易翻车的三件事
自动化拉高了“试错速度”,但没有拉高“判断质量”。管道跑得越快,翻车的影响面就越大,而且翻车的方式往往不是代码 bug,而是统计逻辑和数据理解上的问题。
4.1 多重检验陷阱:每天自动测试几百个因子,总有几个“假活”
假设你一天自动化测试五百个随机生成的因子,哪怕这些因子跟未来收益毫无关系,也会有一批因子的历史 IC 看起来还不错。这不是玄学,是概率:一千次独立试验里,p 值小于 0.05 的结果大约会出现五十次。自动化挖因子的过程,本质上就是把这上千次试验全部跑完了,所以“假活”因子一定存在,而且数量不会少。
我的应对思路有三个。第一,样本外验证,把历史数据按时间切分为训练、验证、测试三段,绝不在同一段数据上反复调参后直接当结论用。第二,滚动前推验证,每次只用当前时点之前的数据做拟合,然后评价之后的表现,模拟真实使用场景。第三,记录有效测试次数,测试次数越多,对显著性门槛的要求就应该越高。
自动化能帮你更快地把想法变成可验证的假设,但它不能帮你绕过统计学的基本约束。每当我看到一个 IC 特别漂亮的因子,第一反应不是惊喜,而是警惕:到底是真的发现了规律,还是只是把噪声翻来覆去筛了几百遍。
4.2 数据质量坑:复权、停牌、财报时间对齐
数据质量问题在自动化流程里会被放大,因为没人每天盯着每一行数据看了。我遇到的三类坑分别是:复权方式不统一、停牌处理粗糙、财报日期用错。
复权问题最隐蔽。除权除息当天,价格会出现一个不自然的跳变,如果因子计算时没处理好,一个纯粹的价格跳变就会被识别成“动量”或者“反转”信号,而且历史上会反复出现。我现在统一使用后复权价格做因子计算,同时在数据表里记录复权因子,方便随时校验。
停牌股票的处理同样重要。停牌期间成交量为零,股价没有真实变动,直接把它的日收益当作 0 会扭曲截面排序,尤其是流动性因子的计算。现在我的做法是:因子计算前剔除停牌股票,回测模拟时也强制禁止交易停牌股票。
财报数据还有一个新手最容易犯的前视偏差:上市公司财报有发布滞后,如果直接用报告期对齐会更漂亮,但那是看到了未来信息。正确做法是维护一个公告日期字段,所有因子计算都用公告日期作为时间戳。这个坑一踩就是好几周白干,值得反复提醒。
4.3 人工复核点:自动化管道里的安全阀
自动化程度应该分等级,而不是一步到位全自动。我自己的分法是这样的:L1 级只做数据整理和因子计算,结果全部人工查看;L2 级自动批量回测并生成报告,人工只看候选池;L3 级可以自动执行小仓位交易动作,但必须配合严格的风控和熔断机制。现阶段我稳定运行在 L2,L3 只做过模拟验证。
即便是 L2,每天也保留三个必看的信息:告警列表、候选因子列表、当天异常标记清单。不需要看全量数据,但这三样一定会过一遍眼睛。这个人工复核点不是摆设,它起着安全阀的作用。自动化的目的是把“必须看的东西”变少,而不是把“该承担的判断责任”抹掉。
我见过有些朋友把自动化做得很极端,全流程无人介入,结果某天因子失效了快一周才发现。原因其实很简单:没有人定期停下来问“这个因子现在还在解释收益吗”。一个稳定运行的管道,最危险的时候恰恰是它看起来很顺的时候。
5. space bunny alpha:一个端到端工作流的实际落地样本
前四章讲的都是通用方法论,可能有人觉得有点抽象。这一章我拿自己内部一个真实项目来拆解,代号就是 space bunny alpha,它是我把前面这套思路落地成完整工作流的样本。
5.1 项目代号由来与要解决的问题
很多人第一次看到 space bunny alpha 这个名字,以为是某个策略名,其实它是我给一条自动化因子扫描管道起的内部代号。space bunny 的意象是一只小兔子在太空中蹦跳,方向看似随机,但每一步都在换位置,用来形容在噪声宇宙里找信号的过程非常贴切。alpha 不用多说,就是我们挖的目标。
这个项目想解决的问题很具体:每周手工验证一批低相关因子太慢了,我想让机器在收盘后自动跑完从数据更新到候选因子产出的全流程,第二天早上 9 点半之前把报告放到眼前。这样我就不需要每周花一整个下午去重复劳动,而是每天二十分钟做判断。
5.2 管道结构、结果表与关键代码
项目结构非常简单,核心代码量也不大,但价值都在接口约定上:
space_bunny/ data_fetcher.py # 增量数据与复权因子重算 factor_library/ # 因子算子目录 backtest_engine.py # 模板化回测与批量扫描 report_builder.py # 报告生成与推送 main.py # 编排入口main.py的骨架跟前面展示的任务列表一样,四个任务按依赖顺序串起来。回测结果统一落进一张表,主要字段如下:
factor_name TEXT params TEXT ic_mean REAL icir REAL annual_return REAL max_drawdown REAL turnover REAL status TEXT -- CANDIDATE / WATCH / REJECT updated_at TEXT这张表是每天日报的数据源,也是后续复盘的重要记录。有了它,我随时可以回看任何一个因子的历史表现,而不是依赖零散的截图。
5.3 三周实测效果、踩过的坑与现在的处理方式
项目上线之后,最直接的变化是人工参与时间从每天两三个小时降到了半小时以内。每周稳定测试的因子数量从几十个提高到两百多个,这个量级靠手工根本无法维持。
前两周跑下来,系统筛出三十六个“高 IC”候选因子,经过样本外验证和逻辑检查之后,真正进入策略验证池的是四个。这里要说明一下,这个数字只是一个研究流程样本,不代表任何收益承诺,但流程本身的效率提升是明显的——以前我可能要花一个月才能人工筛完三十六个候选,现在一周内就能把候选池压缩到个位数。
踩过的坑也很有代表性。第一个是日期错位:不同数据源对节假日的处理规则不一样,导致 A 股和某个辅助数据源的“最新交易日”差了整整一天。每天看报告时感觉哪都不对劲,后来用一个统一的交易日历表做基准才解决。跨日错误里,九成以上是日期对齐问题,不是算法问题,这句我踩出来之后一直记到现在。
第二个坑是低流动性股票干扰。早期没有在因子计算和回测两端都过滤异常样本,结果某些因子到月末排名前列的全是低价、高换手的股票,看起来绩效炸裂,实际是流动性陷阱。后来在管道两端都加了过滤规则,候选池才变得干净。
现在这套流程已经稳定跑了大半年。每天早上我看一眼日报,如果候选池里有新面孔,再花十分钟追溯一下逻辑,然后决定要不要进入下一轮深挖。其余时间,该看看书、该复盘复盘,机器替我盯着数据的细枝末节。这就是我理想中“躺平挖 alpha”的样子。