[F8]每晚22:10,我的服务器自己给自己写投资日报
收盘后的那天晚上十点十分,机器自己开始动笔写当天的复盘。我给它提过最高要求不是算得快,是它不许糊弄我。写完这篇你会明白:这类系统最难的部分从来不是策略本身,是让自己不骗自己。
先交代背景。假设你有一台常年开着的机器,白天它什么都不干,晚上你把当天的行情数据汇总进去,它输出一份日报:市场环境处在什么状态、候选池里哪些标的分数靠前、模拟账户今天的盈亏和违规检查。我自己的这套跑了大半年,改来改去,最后发现值得写的不是它替我挑了什么票,而是它怎么防着我。
这篇是 AI 项目实战第二篇。上一篇聊的是给孩子做的背诗 App,评论区有人问“你有没有做股票相关的自动化”,这篇就是回答。只讲架构分层和方法学,账号、IP、真实参数一概不出现;文中所有代码都在本机示意数据上实跑过,输出是原样贴的;所有阈值都是编的,跟任何真实系统无关。
先给你导读,三句话能读完的部分我放前面,赶时间的读者看完这张表就可以走:
※ ※ ※
※ ※ ※
🌙 它每晚十点十分到底在干什么
整套流水我定在交易日晚上 22:10 触发,错开结算和数据推送的时间窗。把它拆开,其实就是四步:取数、判环境、打分、成稿投递。
取数这步没什么戏剧性。白天收盘后数据源把当天的日线、榜单、估值这些公开数据推到本地,晚上日报任务醒来,先检查数据新不新鲜。这里埋着第一个坑:数据源说“我有 T+1 披露延迟”的字段,你按 T+0 用,就是拿明天才公布的信息做今天的决定,严格说这已经是未来函数了,只是穿了一身“我用的是官方 API”的皮。
判环境是闸门层的事,下一节细说。打分是评分层,再下一节。成稿这步我一开始特别在意文笔,后来彻底放弃:日报正文全部是模板拼数字,一句形容词都不让大模型润色。原因很实在,语言模型给浮亏 8% 的持仓配上“仍需耐心等待估值修复”这种句子,看着是安慰,实际是在替亏损找叙事。投资记录里最不该出现的文风,就是给没发生的事编理由。
投递端还有个细节值得说:我的口径是“没有变化就不出声”。市场状态没翻转、持仓没触发条件、候选池没进新票,任务就一个字都不推。这个规矩被上篇定时任务那篇文章剧透过一次技术原因(空输出和报错输出的区别),这里想强调的是纪律意义:一个每晚都给你发“一切正常”的系统,三个月后你连“不正常”的消息都不会点开。
这是对做量化入门的人最有用的第一段:日报系统不是荐股机器,它是一台约束执行机。你白天盯盘时定的规则,晚上由一台没有情绪的机器替你核对。

一句话:取数、判环境、打分、成稿,四步里真正难的是第二步和第四步,因为那两步全是人性。
※ ※ ※
※ ※ ※
🚦 市场闸门:它管的不是买什么,是买多少
闸门层的设计目标一句话能说清:市场环境负责“买多少”,个股条件负责“买不买”,两件事必须解耦。我见过太多新手把这两层搅在一起,用“大盘不好所以这只票也不买”做决策,结果市场回暖那天,他手里的候选票早就涨上去了,个股条件其实三天前就该触发。
最朴素的闸门是一个阈值:给市场打一个环境分,分数高就开闸,分数低就关闸。我第一版就是这么写的,然后被现实教育了。任何阈值型指标,只要行情在它附近震荡,就会翻转。分数在阈值线上下一毫米一毫米地磨,你的仓位就跟着满进满出。手续费是小事,真正难受的是行为本身:一个天天被行情打耳光的系统,你不可能长期信任它,最后你会绕过它,手动来。绕过硬护栏的系统等于没有硬护栏。
解法是控制论里玩了几十年的东西:迟滞(hysteresis)。别用一条线,用两条。往下跌破“下轨”才拉闸,但要回到“退出线”以上才复闸,两条线之间的灰区什么都不做,保持原状态。翻一下你家的空调温控器,原理一模一样,设定 26 度它不会在 26.0 度反复启停,靠的就是迟滞带。
再叠一层防抖:状态翻转不看单日,要求连续若干天满足才认。单日暴跌可以触发拉闸评估,但要连续几天都在闸外才真关;恢复更慢,市场反抽一天就重开闸,是最常见的送钱方式。我现在的实现里,开闸比关闸快、关闸比开闸狠,方向是反的,宁可钝,不要跳。
下面是我在本机跑的示意版闸门,合成一条“市场广度”日线序列(当日创阶段新高的家数占比),做 60 日 z 分,再套迟滞状态机。所有阈值都是示例值,跟真实系统没有任何对应关系:
# 市场闸门:广度 z 分 + 迟滞状态机(示例数据,非实盘参数)
import pandas as pd
import numpy as np
rng = np.random.default_rng(42)
n = 300
# 合成一条"市场广度"日线序列:当日创20日新高家数占比
breadth = np.clip(rng.normal(0.5, 0.12, n) + 0.15*np.sin(np.arange(n)/28), 0.02, 0.98)
px = pd.DataFrame({"date": pd.date_range("2025-01-01", periods=n, freq="B"), "breadth": breadth})
px["z"] = (px["breadth"] - px["breadth"].rolling(60).mean()) / px["breadth"].rolling(60).std()
# 迟滞:进警示易、出警示难。上轨/下轨都是示例口径,写死在配置里,不跟着行情改
UP, LOW, EXIT = 1.0, -1.0, -0.4 # 示意阈值,非任何实盘参数
state, out = "normal", []
for z in px["z"]:
if np.isnan(z):
out.append("warmup"); continue
if state == "normal" and z < LOW: state = "alert" # 跌破下轨才拉闸
elif state == "alert" and z > EXIT: state = "normal" # 回到退出线以上才复闸
out.append(state)
px["gate"] = out
print(px.tail(8).to_string(index=False, float_format=lambda v: f"{v:.3f}"))
print("\n闸门状态分布:", px["gate"].value_counts().to_dict())
实跑输出(末 8 行):
date breadth z gate
2026-02-13 0.550 0.333 normal
2026-02-16 0.063 -2.973 alert
2026-02-17 0.340 -1.024 alert
2026-02-18 0.387 -0.680 alert
2026-02-19 0.399 -0.603 alert
2026-02-20 0.317 -1.129 alert
2026-02-23 0.148 -2.147 alert
2026-02-24 0.397 -0.502 alert
闸门状态分布: {'normal': 148, 'alert': 93, 'warmup': 59}
注意倒数第一行:2 月 24 日 z 是 -0.502,已经回到下轨(-1.0)上方不少,但闸门状态依然是 alert,因为它还没爬过 -0.4 的退出线。这一天如果按朴素阈值早就“金叉复活”了,迟滞让它再忍一天。灰区里的每一分犹豫,都是省下来的手续费。

还有个反直觉的点,是环境分档位设计逼我想明白的:市场弱不等于清仓。闸门关的是“新开仓”的权限,卖出永远自由,止损任何时候不许被任何状态覆盖。这条是我在模拟盘里交过一次学费后焊死的:单边下跌里“环境分低所以清仓”和“环境分低所以禁买”是两个世界,前者把浮亏变实亏,后者只是不添新伤。仓位档位也不是线性越跌越买,我最后用的是非线性的形状,健康震荡带给高仓、拥挤追高带主动降仓,具体数值不给它上户口,反正都是示意。
如果你是把钱交给规则的人,这节对你最有用:闸门的价值不在预测,在把“手痒”隔离在灰区之外。
一句话:一条阈值线是给行情当鞭子,两条线加迟滞才是给自己当刹车。
※ ※ ※
※ ※ ※
📊 因子评分:权重不是神谕,是一份可以被审计的立场
闸门放行之后才轮到选股。评分层的规矩同样一句话:市场环境管“买多少”,个股因子管“买不买”,因子分不许覆盖个股硬条件,个股硬条件也不许质疑闸门。两层各审各的。
因子本身没什么神话。我用的全是教科书面孔:动量、低波动、流动性、估值分位。真正值得写的是打分流程里防自己的三个细节。
第一个:截面处理,不是时间序列处理。 因子分要在同一天的全体候选里横向比较,先 winsorize 掐掉两端离群值(最贵的那只票和跌麻的那只票都不许绑架整个截面),再转 z 分。缺失值补中位数而不是删行,删行会让“数据没到”的票变相获得免检,补中位是让缺数据的票拿中性分,老老实实排在中间。
第二个:权重先定死,再谈优化。 这是我在评分层挨过最狠的一课。第一版我把权重写成“可调参数”,允许每周审计微调,结果系统顺着行情一路追打:哪类票最近涨,权重就漂向哪类。两个月后回头看,它不是在做投资,是在做行情的复读机。后来我把权重锁成配置里的常量,改动要走“季度复盘”这种郑重其事的流程,改一次留一次历史。锁死的含义是:这份权重表达的是你对市场的立场,立场可以错,但必须可审计。一个永远在迎合最近行情的权重组合,等于没有立场。
第三个:因子有效性要用 rank IC 定期验尸,验出反向就认。 IC 检验的读法:|t|>2 且 IC 为正,因子在给你赚钱;|t|>2 且 IC 为负,因子在给你反向赚钱,它预测的东西和直觉正好相反;t 不显著,这个因子就是装饰。学术圈早就反复报告过 A 股的短期动量失效甚至反转(追高常常接盘,这与成熟市场的动量效应相反,Liu-Stambaugh-Yuan 那篇《Size and Value in China》和相关行为金融文献都有口径),我自己做截面检验时看到动量 IC 为负的那一刻还是愣了:文档里的常识和我自己的代码打架,最后是我的代码和 A 股的历史数据一起站在了“常识”的反面。认账的方式是降权、或者把动量改造成“倒 U”:适度上涨给高分,追涨过热反而低分。这比假装没看见体面,也比硬扛着高权重省钱。
下面是本机实跑的评分示意片段,宇宙是十只虚构标的(DEMO000 到 DEMO009),权重 0.5/0.3/0.2 是纯示例:
# 因子评分:截面 winsorize + z 分 + 固定权重(示例宇宙,10 只虚构标的)
import pandas as pd
import numpy as np
rng = np.random.default_rng(7)
df = pd.DataFrame({
"code": [f"DEMO{i:03d}" for i in range(10)],
"mom": rng.normal(0, 1, 10), # 动量
"vol": -abs(rng.normal(1, .4, 10)),# 低波动(取负号:波动越小分越高)
"liq": rng.normal(0, 1, 10), # 流动性
})
df.loc[3, "mom"] = np.nan # 故意留一个缺失
def cs_z(s, clip=3.0):
s = s.clip(s.quantile(0.05), s.quantile(0.95)) # winsorize,掐掉离群值
return (s - s.mean()) / (s.std() or 1)
W = {"mom": 0.5, "vol": 0.3, "liq": 0.2} # 权重先定死,季度复盘才允许动
df["score"] = sum(w * cs_z(df[c].fillna(df[c].median())) for c, w in W.items())
print(df.sort_values("score", ascending=False)[["code","mom","vol","liq","score"]]
.to_string(index=False, float_format=lambda v: f"{v:+.2f}"))
实跑输出:
code mom vol liq score DEMO007 +1.34 -0.82 -0.54 +1.07 DEMO001 +0.30 -1.14 -0.24 +0.27 DEMO003 NaN -0.63 +0.27 +0.26 DEMO008 -0.49 -0.24 -0.05 +0.24 DEMO006 +0.06 -0.46 -2.52 +0.20 DEMO009 -0.62 -0.48 +0.11 +0.02 DEMO004 -0.45 -0.99 +0.16 -0.26 DEMO000 +0.00 -1.20 -1.84 -0.44 DEMO002 -0.27 -1.04 -1.27 -0.45 DEMO005 -0.99 -1.28 -0.19 -0.93
盯住 DEMO003 那行:动量缺失,中位数补上之后排第三,没有因为数据缺口被踢出局,也没有被凭空喂成高分。这就是“缺数据=中性分”的形状。
一句话:评分层的本事不在选出最灵的因子,在让每个打分环节都留下可以被翻案的记录。
※ ※ ※
※ ※ ※
🔬 回测方法学:好看得吓人的曲线,我先杀一半
闸门有了,评分有了,接下来是最危险的环节:证明这套东西有用。回测是量化的实验室,也是量化的造假车间。我的日报里每一个策略结论,进闸门参数之前都得过回测这道关,而这道关的设计目标不是“让它通过”,是“让它别轻易通过”。
第一刀砍未来函数。 三个最常见的藏身处:用当日收盘价在当日成交(你不可能在收盘的那一秒按下买入键,正确口径是信号 shift 一天、次日开盘撮合);用披露日早于公告日的财务数据(季报“属于”三月,但四月底才公布,防未来的近似口径是法定披露截止日,宁迟不假);停牌股用停牌前的旧价参与打分(僵尸价格会让排名系统性偏向没人交易的票)。这三处我全踩过,踩的时候曲线都“好看”,修的时候才发现好看是因为偷看了答案。
第二刀砍边界泄漏。 训练和测试切段时,如果信号持有 5 天,那测试段开头 5 天的收益窗口和训练段尾巴是重叠的,朴素的时间序列切分在这里就是作弊。这个叫 purged walk-forward 的做法是 López de Prado 在《Advances in Financial Machine Learning》里系统讲过的,业内早就是标准姿势,但我第一次自己实现还是切漏了,因为重叠不在日历边界上,在收益窗口里。
# purged walk-forward:训练/测试之间留 embargo,掐掉前视泄漏(示意骨架)
import pandas as pd
import numpy as np
rng = np.random.default_rng(1)
idx = pd.date_range("2024-01-01", periods=500, freq="B")
sig = pd.DataFrame({"date": idx,
"x": rng.normal(0, 1, 500),
"y": rng.normal(0.0004, 0.01, 500)})
HOLD = 5 # 持仓周期 5 天 -> 边界要清掉 5 天重叠
folds = []
for t0 in range(0, 400, 80): # 4 折滚动
tr_a, tr_b = t0, t0 + 200
te_a, te_b = tr_b + HOLD, tr_b + HOLD + 50
if te_b > len(sig): break
folds.append({"fold": len(folds)+1,
"train": f"{sig.iloc[tr_a].date:%m-%d}~{sig.iloc[tr_b-1].date:%m-%d}",
"test": f"{sig.iloc[te_a].date:%m-%d}~{sig.iloc[te_b-1].date:%m-%d}",
"purged_days": HOLD})
print(pd.DataFrame(folds).to_string(index=False))
print(f"\n被 purge 掉的边界样本共 {HOLD*len(folds)} 条,它们正是前视藏身的位置。")
实跑输出:
fold train test purged_days
1 01-01~10-04 10-14~12-20 5
2 04-22~01-24 02-03~04-11 5
3 08-12~05-16 05-26~08-01 5
4 12-02~09-05 09-15~11-21 5
被 purge 掉的边界样本共 20 条,它们正是前视藏身的位置。

第三刀砍幸存者。 本地日线库如果只含“今天还活着的股票”,退市股从历史里消失,而退市股在退市前通常又烂又跌,你的回测宇宙被悄悄美颜过。加不上全历史成分就至少要在结论里写明这个口径缺陷,装作没有是最坏的姿势。
第四刀砍“试出来的”。 一个策略跑一百个参数挑出最好看的曲线讲给我自己听,这不叫回测,这叫把噪声打印成海报。我的规矩:预注册。跑之前把参数区间、主判据、放弃条件写进日报模板里,跑完只许填数,不许改题。判据不过就归档进“阵亡策略”清单,那份清单现在比赚钱的清单厚。
还有一层诚实压在所有刀法之上:回放和回测永远含后视成分,因为公式是用同一段历史定的。我做过长区间滚动回放,闸门优化最大的确定性收益不是“多赚”,而是把“满仓空仓反复横跳”的形态压到极低频、同时砍掉追高带的仓位敞口。至于压到多少倍,那是实盘统计口径,不该登在公开文章里。择时体系在长期震荡市里跑不赢纯持有,这是它自己回测亲口告诉我的,我在日报里原样贴过,没给它递润色笔。
一句话:回测层的产出不是收益曲线,是一份“哪些结论不许信”的清单,剩下的才敢拿去喂闸门。
※ ※ ※
※ ※ ※
🪞 最难的一层:日报要审的是我
写到这才轮到最里层,也是这个系统对我真正的价值:自审计。日报每天不只汇报行情,还反过来核查它自己。
核查什么?举四类。数据新鲜度:本地库的最新日期和决策里引用的 as-of 日期对不上,直接标红,这意味着闸门用着过期的宽度数据做今天的判断。流水对账:模拟账户的每一笔现金变动,从交易流水重放一遍必须能对上余额,费率口径要和成交时点一致,对不上就是 P0 事故,说明有笔交易在账本外发生过。规则合规:环境分和仓位档位的映射不许自相矛盾,硬止损不许被任何状态关掉,参数不许漂出预注册的界。回执新鲜度:上一轮冒烟测试的结论,必须能对上当时的代码版本,版本变了旧回执一律作废重跑。
这套审计跑出来的东西一开始全是我自己的锅:写死在代码里没走配置的阈值、测试污染了线上状态、部署链路重跑把“当日快照”算成了两天前。每一条都被台账记下,重复出现要加倍扣分。我不给它加新功能的权限,排在修台账后面的队里。
为什么值得花这个功夫?因为量化系统里真正的风险不是策略亏钱,是系统在悄悄变形而你信它还和昨天一样。权重被行情漂走了,你以为是纪律;闸门被一次“临时调整”松了,你以为是例外;审计发现自己连续三天数据没动,你以为是巧合。日报每晚把这三件事打回原形,它不哄我。
这是对已经有一套自动化的人最狠的一条建议:给你的系统写一份它自己不敢看的日报,比给它加一个因子值钱。
一句话:让机器替你执行规则不难,难的是让机器每晚核对“你还在按规则活”。
※ ※ ※
※ ※ ※
说点没解决的
闸门迟滞带的宽度、评分权重的季度复盘节奏、审计台账里 P0 和 P1 的边界,这些我现在都只敢说“用过半年没出事”,不敢说最优。更远的方向我在试两件事:让回测层和闸门层对话(哪些参数域的回测置信度低,闸门的灰区就该更宽),以及把审计里“解释发生了什么”的部分接给语言模型、把“判定违规”的部分永远留在规则里。前者是架构题,后者是信任题,都还没做完。
下一篇想聊点生活里的:假期出门一趟,门票规则、体力约束、预算口径怎么喂给 AI 变成一条流水线,行程、备选、抢票日历一次生成。这里先埋半个头,不剧透。
如果你自己也在跑类似的“每晚自动干活”的东西,评论区聊聊你的系统最容易在哪一层骗你。这篇如果有点用,点个在看,让更多还在给浮亏写小作文的人刷到。
※ ※ ※
※ ※ ※
资料来源
- ▪ López de Prado, Advances in Financial Machine Learning(purged K-fold / embargo 口径)
- ▪ Liu, Stambaugh, Yuan, Size and Value in China(A 股因子异于成熟市场的公开学术口径)
- ▪ 文中代码块均在本地 Python 3.11 + pandas/NumPy/DuckDB 环境对合成示意数据实跑,输出原样粘贴;宇宙标的为 DEMO 虚构代码,阈值与权重均为示例值,与任何真实交易系统无关
- ▪ 系统架构为作者个人项目的通用化转述:不含真实 IP、作业名、脚本路径与策略参数;示例主机与路径(如 /home/deploy)均为文档保留/虚构写法,非作者真实环境
- ▪ 模拟盘全部为虚拟本金,不接任何真实下单通道;本文不构成投资建议
- ▪ 生成日期:2026-09-28
评论 (0)
登录 或 注册 后参与讨论。