ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI写量化作业先打磨需求规格再写代码:避免未来函数和回测失真

AI写量化作业先打磨需求规格再写代码:避免未来函数和回测失真

说实话,在试过这套配合之前,我对“AI 写量化作业还要分两步走”这个思路是瞧不太上的。毕竟工具就在那里,直接让它把整份代码跑出来不是更快吗。我这个想法的代价,是连续两版作业翻车:第一版收益曲线漂亮到离谱,第二版指标算出来跟编的差不多。后来我冷静下来,把流程改成“先用分析侧的 AI 把需求往深处打磨成规格,再把规格丢给执行侧的编码工具去写代码”,结果从翻车到跑通,多花不到半天。

这篇文章就把整个翻车过程、背后原因,以及我亲测有效的分工流程完整还原一遍。不管你是正在写策略类课程作业的学生,还是在日常工作里要写回测脚本的新人,这套思路应该都能帮你省掉好几轮无效返工。

1. 翻车实录:代码看起来正常,收益曲线一眼假

1.1 第一版:双均线策略跑出 42% 年化,我当时就该警觉

第一版作业的需求很简单:实现一个双均线策略,对某只个股的日线数据做回测,输出累计净值曲线、年化收益、最大回撤和夏普比率。我把这段描述直接丢给编码工具,二十秒后代码出来了,结构清爽,注释齐全,甚至还有参数模块。

跑完本地脚本,控制台打出的结果是:年化收益 42%,夏普 3.1,最大回撤只有 8%。我当时甚至有点高兴,觉得作业稳了。但冷静下来一算,不对劲——那一年样本里股价实际涨幅只有 12%,策略却宣称能做出 42% 的年化,回撤小得不像真实交易。量化策略当然有可能跑赢基准,但赢到这种程度,第一反应应该是代码有泄漏,而不是策略多牛。

我一行一行查,最终还是找到了问题:工具为了计算信号,在整段数据上直接用了rolling窗口,但它没有把"信号生成"和"未来信息"隔离。更典型的是,它在做指标标准化时,用了全样本的均值和标准差,这意味着每一个时间点上,策略都偷看了未来数据。这种未来函数在量化回测里是大忌,偏偏生成出来的代码语法正确、结构完整,不仔细验算根本看不出来。

1.2 第二版:明确要求"避免未来函数",却栽在切分窗口上

第一版翻车后,我学乖了一点,第二版在提示词里加了约束:注意避免未来函数、考虑手续费和滑点、对参数做样本外验证。工具看起来也照做了,回测里加了train_test_split,还写了 walk-forward 的雏形。

但结果依然离谱。我把训练段和测试段的净值分开画出来,训练段年化 28%,样本外却变成负收益。这本身其实是可能的,说明策略过拟合了——但问题在于,工具在 walk-forward 参数优化时,用同一段数据既做参数搜索又做评估,等于把测试集的标准答案提前拿给了训练过程。它写出来的代码采用的是固定时间切分,但"固定"得有问题:它把最后 20% 当测试段,却在测试段里重复了一遍"先用全体数据计算最优参数,再在测试段回测"的过程,测试段信息被污染。

到这里我才明白,问题不是提示词写得不够多,而是提示词里面的每个单词,工具都按自己的理解去实现了。"避免未来函数"这句话,在它看来只是一条注释,它并不知道该怎么把这句话转化成可执行的验证逻辑。

1.3 完整排查链路:从收益曲线倒推回代码语义

我复盘整条排查路径,大致是这样走的:

第一,对比预期。先用行情软件或人工手工计算的方式,算出"如果按最简单的买入持有,收益应该是多少"。如果策略回测结果明显高于合理区间,先怀疑数据泄漏,而不是惊喜。

第二,冻结特征。把回测要用的所有输入变量(价格、成交量、均线值)存成快照,人工抽三个连续交易日,手动推一遍持仓和收益变化。拿手算结果跟回测代码的结果比对,看差异出在哪一天。

第三,查时间轴。画出信号时间点与成交时间点的对照表,确认是否存在"当天收盘信号、当天收盘价成交"这种偷看收盘价的行为。真实交易里,更合理的做法是信号产生后次日开盘成交。

第四,审查指标口径。看看年化收益是不是用累计收益直接乘了252 / n,夏普比率的分母是不是用了总体标准差而不是样本标准差。这些小口径的差异,在代码层都合法,但在量化作业的验收标准里,差别巨大。

我当时做完这四步,得出一个结论:代码层面没有明显 bug,但语义层面全是漏洞。工具生成的每段函数都"局部正确",拼在一起却整体失真。

2. 我看错的地方:量化作业考验的不是写代码,而是画好边界

2.1 普通编程作业和量化作业的本质差异

这里有个很关键的区别:普通编程作业,比如写一个排序算法、实现一棵二叉树,输入输出是明确定义的,测试用例是固定的。任何人把这类需求丢给编码工具,翻车概率都很低,因为验收标准就是"程序能在给定输入上返回正确输出"。

但量化作业不一样。它有很多隐藏的边界条件,而这些边界条件往往才是作业的真正考点:

  • 数据窗口怎么滑:是滚动式、扩展式,还是 walk-forward 分块?
  • 交易日历是否对齐:有些数据集里混入了非交易日,要不要剔除?
  • 用什么价格成交:收盘价信号,次日开盘成交,还是当日收盘价直接成交?
  • 交易成本怎么计:手续费按固定比例还是按阶梯?滑点算多少?
  • 指标口径用什么公式:年化收益是几何年化还是算术年化?夏普比率是否包含无风险利率?
  • 统计检验怎么处理:样本外数据能不能重复用于参数调优?

这些边界条件,普通算法作业里几乎没有,量化作业里几乎每条都要掰扯清楚。编码工具在没有定义这些条件时,会自动帮你"填空"——它填的往往是最方便写代码的方案,而不是最合理的方案。

2.2 直接让编码工整题,等于边施工边改图纸

我后来想明白一个比喻:直接让编码工具生成整份量化作业,就像你请了一支施工队,却没给完整图纸,只给了一句"盖个能住的房子"。施工队当然能盖,但他们盖出来的门窗朝向、楼梯宽度、承重位置,都是按自己的经验猜的。你觉得能住就行,他们觉得符合常识就行,但你们俩对"能住"的定义可能完全不一样。

编码工具的强项在于执行:读懂上下文、修改局部函数、补齐模块实现、跑通测试用例。它擅长的是在既定框架内做准确的补全和重构。但量化作业的难点在框架本身——样本内外的切分逻辑、交易成本建模、指标计算口径,这些东西一旦没有在开工前定死,工具就会在局部做出一堆"看起来合理"的决定。

等到整体跑完再检查,你面对的就是一整套自我一致却充满隐性假设的代码。那时候想改,等于重新做一遍需求分析,返工成本极高。

2.3 两类作业需求特征的直接对比

对比维度普通算法作业量化策略作业
输入输出明确,测试用例固定半隐式,验收靠口径判定
验收标准代码通过给定用例回测不骗人 + 统计口径正确
隐性前提很少交易日历、复权、成本、样本切分等较多
代码结构核心算法行数少数据清洗、事件循环、指标计算链条长
失败表现直接报错不报错但结果可疑

这也是为什么我后来把问题重心从"让 AI 写更好的代码"转移到了"让 AI 先写出不会产生歧义的作业规格"。

3. 分工逻辑:为什么前置"打磨"能让执行少翻车

3.1 "打磨"到底在打什么

很多人听到"打磨"两个字,以为就是多写几行提示词,把作业要求描述得更详细。这样理解不完全对。真正的打磨,是把一段粗糙的、含混的需求描述,翻译成一份机器和人可以共同认可的规格说明。

我举个例子。你最初的描述可能是:"帮我做一个均线策略的回测,输出收益曲线和一些指标。"这句话本身没有错,但它缺少验收信息。打磨之后,同样的需求会变成:

  • 数据范围:某日线数据集 2015-01-01 到 2024-12-31,仅保留交易日
  • 信号规则:5 日均线上穿 20 日均线次日开盘买入,下穿次日开盘卖出
  • 成交假设:单边手续费万分之五,滑点万分之一
  • 指标口径:年化收益按几何年化,夏普比率使用样本标准差
  • 样本切分:前 80% 训练,后 20% 验证,参数优化只能在训练段完成

看到区别了吗?打磨产出的不是"更长的描述",而是一份没有歧义的验收清单。每条都具体到数字和算法口径。这样交付给编码工具时,它几乎没有自由发挥的空间。

3.2 为什么分析侧的 AI 更适合做"打磨"这件事

我之前也试过让编码工具自己先写一份方案再实现,但它很容易陷入"边写边想"的状态。你让它列出方案,它列完方案以后,往往已经顺手把代码也写了,而且方案里的每一条都和代码的实现完全对应。听起来是好事,但问题是:它列方案的时候,用的是和写代码时完全相同的那套隐含假设,不会自我反驳。

分析侧的对话式模型在这点上不太一样。它的优势在于来回确认:你给它一份粗糙需求,它会反过来问你数据源是什么、交易成本怎么算、年化口径偏好哪一种。这种反问式的互动,本身就是把隐藏假设逼出来的过程。

另外,分析侧模型适合把同一份需求反复拆解、合并、改写成不同视角的表述。你可以让它先按"策略逻辑"拆一遍,再按"回测流程"拆一遍,再按"评估指标"拆一遍。三段拆解结果交叉核对,遗漏就会被发现。

3.3 我实测下来最顺的六步循环

按我后来稳定的流程,整个循环是这样的:

  1. 把原始作业要求丢给分析侧模型,不谈实现,只谈目标和疑问。
  2. 让模型逐条反问,把数据、成本、口径、切分方式全部问到位,自己补上缺失信息。
  3. 要求模型输出一份完整规格文档,用编号列出所有约束,并附上验收标准。
  4. 把规格文档原样交给编码工具,明确要求"按此实现,不得自行修改口径"。
  5. 编码工具跑通后,把结果(净值曲线、指标表)拿回给分析侧模型做审计。
  6. 审计发现不一致或结果可疑,回到第 3 步修改规格,再重新执行。

这套循环的核心是:把关的人在工具外部,而不是工具内部。执行工具只负责把规格翻译成代码,评判标准永远留在规格层面。

4. 亲测流程:一套可以照着抄的作业模板

4.1 准备条件

列一下我用到的环境,非常简单:一个 Python 环境,装上pandas、numpy、matplotlib。数据用一份模拟个股日线 CSV,列名是date, open, high, low, close, volume,共约 2400 个交易日。整个作业在脚本里跑,不需要额外数据库。

模型侧,需要两个可以分开对话的入口:一个用来做需求打磨和分析审计,另一个用来执行编码。如果你手上各有一个,就直接按前面说的六步走;如果只有一个对话界面带工具调用功能,也可以先通过对话把规格敲定,再切到"普通代码生成"模式让模型写实现。

4.2 一个可以复现的示例题目

假设作业要求是:基于给定的股票日线数据,实现一个双均线策略回测,输出累计净值曲线、年化收益、最大回撤、夏普比率,并保证无未来函数。

这不是一道难题,但我用老方法依然翻车过。后来按规格流程走,效果就明显不一样。

4.3 打磨后产出的规格文档长什么样

我在打磨环节得到了一份类似这样的规格文档。它不是代码,但比代码更关键:

# 回测规格 v1 数据边界:2015-01-01 至 2024-12-31,仅保留交易日; 复权方式:后复权,close 列已做复权处理; 信号规则:close 的 5 日均线上穿 20 日均线时产生买入信号, 5 日均线下穿 20 日均线时产生卖出信号; 成交时点:信号产生次日开盘价成交; 成本假设:单边手续费 0.0005,滑点 0.0001,买卖各收一次; 资金设定:初始资金 100000,单笔全仓,允许空仓; 指标口径: 年化收益 = 累计净值^(252 / 交易日数) - 1 夏普比率 = (日收益均值 / 日收益标准差) * sqrt(252) 最大回撤 = max(历史峰值 - 后续净值) / 历史峰值 样本切分:前 80% 为训练段,后 20% 为验证段; 均线参数只能用训练段数据优化。 验收标准: 1. 验证段不含未来信息; 2. 手续费与滑点已计入每笔交易; 3. 指标值与手工抽样计算结果一致。

这份规格看起来只是把需求写细了,但它最大的作用,是让编码工具在实现时没有"我以为"的余地。它不会自动去选 5 日和 20 日以外的参数,不会自作主张把交易成本记成单边还是双边,更不会顺手用全样本标准化。

4.4 给执行工具的代码实现样例

拿到规格后,我把内容贴给编码工具,只加了一句话:"严格按规格实现,不要修改任何口径。"

下面是它生成代码的核心结构,我做了精简和注释说明。第一段是数据加载和信号生成:

import pandas as pd import numpy as np def load_data(path: str) -> pd.DataFrame: df = pd.read_csv(path, parse_dates=["date"]) df = df.sort_values("date").reset_index(drop=True) df = df[df["date"].dt.dayofweek < 5] # 剔除周末(如果有非交易日) return df def gen_signals(df: pd.DataFrame, fast: int = 5, slow: int = 20) -> pd.DataFrame: df = df.copy() df["ma_fast"] = df["close"].rolling(fast).mean() df["ma_slow"] = df["close"].rolling(slow).mean() df["signal"] = 0 df.loc[df["ma_fast"] > df["ma_slow"], "signal"] = 1 df.loc[df["ma_fast"] < df["ma_slow"], "signal"] = -1 df["position"] = df["signal"].diff().fillna(df["signal"]) return df

第二段是回测主体,重点在成交时点和成本:

def run_backtest(df: pd.DataFrame, initial_capital: float = 100000.0, commission: float = 0.0005, slippage: float = 0.0001): cash = initial_capital position = 0 equity_curve = [] prev_signal = 0 for i in range(len(df)): today = df.iloc[i] signal = today["position"] # 对应次日开盘执行 if signal != prev_signal: if signal == 1 and position == 0: price = df.iloc[i + 1]["open"] if i + 1 < len(df) else today["close"] price = price * (1 + slippage) position = cash / price cash = 0 cash -= position * price * commission elif signal == -1 and position > 0: price = df.iloc[i + 1]["open"] if i + 1 < len(df) else today["close"] price = price * (1 - slippage) cash = position * price position = 0 cash -= cash * commission equity = cash + position * today["close"] equity_curve.append(equity) prev_signal = signal if i + 1 < len(df) else prev_signal return pd.DataFrame({"date": df["date"], "equity": equity_curve})

代码本身不复杂,但它和规格文档是严格对应的。信号在当天计算,成交却在次日开盘,这就在代码层面物理屏蔽掉了时间偷看。

第三段是指标计算。这一步最容易出错,我把关键几行列出来:

def compute_metrics(equity_curve: pd.Series) -> dict: daily_ret = equity_curve.pct_change().dropna() total_return = equity_curve.iloc[-1] / equity_curve.iloc[0] - 1 n = len(daily_ret) annual_return = (1 + total_return) ** (252 / n) - 1 sharpe = daily_ret.mean() / daily_ret.std(ddof=1) * np.sqrt(252) drawdown = (equity_curve / equity_curve.cummax()) - 1 max_dd = drawdown.min() return { "annual_return": annual_return, "sharpe": sharpe, "max_drawdown": max_dd, "total_return": total_return, }

注意,这里年化收益用的是几何年化,夏普分母用的是样本标准差(ddof=1),这两点都是规格文档里明确写过的口径。如果没有规格约束,工具很可能会用算术年化、或使用总体标准差,最后数值差一截,你却说不清错在哪。

4.5 回流验证:把关环节绝不能省

跑完脚本后,我把训练段和验证段的净值曲线分别导出,拿到分析侧模型那边做了一次审计。这一步我称之为"把关环节",也是我认为让整个流程有效的最重要部分。

审计时我通常提如下几个问题:

  1. 验证段净值曲线和训练段净值曲线在形态上是否有明显差异?
  2. 指标数值是否在合理区间?例如年化收益明显超过标的实际涨幅,就需要警惕。
  3. 手工抽样三笔交易,核对手续费和滑点是否真的计入了每笔交易。
  4. 样本外参数是否完全冻结?有没有出现验证段数据被二次用于调参的痕迹。

我这次遇到的情况是:训练段年化 18%,验证段年化只有 4%,回撤明显放大。分析模型给出的判断是"策略参数很可能在训练段过拟合,不建议直接作为最终决策依据"。这个反馈不改变代码实现,但会直接影响我用什么指标去交作业,也提醒我有没有必要缩小参数优化范围。

如果审计结果对不上,比如收益曲线出现断崖式跳变,我会回到第 3 步修改规格,补充"交易信号需要额外过滤"或"涨跌停时不允许开仓"这类边界条件,再重新让执行工具跑一遍。这套循环多跑两轮,出来的结果基本就是干净的了。

5. 边界与注意事项:不是所有作业都适合走这套流程

5.1 什么时候用这套流程收益最大

复盘之后我觉得,这套"先打磨、后执行"的分工,最适用的是中大型策略作业。特征是:数据文件多、回测逻辑长、评价指标复杂、验收标准模糊。

比如组合构建类作业,要完成资产配置、权重优化、再平衡回测、归因分析,这类作业代码量通常都在 500 行以上,需要拆成多个模块。这时候需求里任何一条口径没定死,后续代码都可能在局部正确的前提下整体失真,打磨带来的收益非常大。

另外,如果作业要求里给出了很多定性描述,比如"合理设置交易成本""在控制回撤的前提下追求收益",这种模糊要求最适合先打磨,因为打磨就是把"合理""控制"这类词变成具体数字。

5.2 什么时候别走这套流程

反过来也有几种情况,强行分两步反而低效。

第一种是纯算法题。要求里输入输出已经说得明明白白,测试用例都给好了,直接让编码工具生成函数就行,多绕一步打磨没有任何收益。

第二种是 200 行以内的小脚本。比如只需要读取一个 CSV、算一个指标、画一张图,这种任务规格写得再细,也不会比直接生成更快。

第三种是论文式或研究式的作业,重点是创新想法而不是干净的实现。这种场景下,过度强调规格和执行分离,反而会限制模型与你进行开放式讨论的空间。你更需要的可能是让对话式模型直接陪你推演逻辑、生成实验草图,而不是把它当成"翻译机"。

5.3 使用这套流程时最容易踩的坑

第一个坑:规格文档写得太抽象。比如写"考虑交易成本",等于没写。我在实际操作中会把成本数值直接写进去,最好连同计算举例一起放,比如"买入 100 股,价格 10 元,手续费=1000×0.0005=0.5 元"。只有给出具体数字,执行工具才会生成对应细节。

第二个坑:执行工具自动"优化"口径。有时编码工具跑出来以后,顺手把日收益换成百分比,或出于好意在回测里加了个"收益平滑",这会破坏指标口径的一致性。我的做法是在给执行工具的最后一行里写明:不要增加规格之外的逻辑,不要修改规格之内的口径。

第三个坑:只有一次验证。有些同学跑通之后看到净值曲线就收了,觉得稳了。但一次回测通过只能说明代码没有崩溃,不能说明口径严谨。至少应该做一次手工抽样核对,理想情况下再做一次参数扰动测试,看看策略对参数是否敏感。这两步在量化作业的验收里,往往比代码本身更值钱。

第四个坑:成本和时间控制。两套模型配合使用,token 消耗自然比单个模型高。尤其规格文档反复打磨时,API 调用次数会明显上升。我的经验是,打磨最多三轮,超过三轮说明你的需求描述本身有问题,不如换个方式重写原始需求,而不是继续在同一份需求上打转。

6. 翻车之后我沉淀下来的使用纪律

经过这几版作业,我后来给自己立了几条纪律,也算是对这篇实录的一个收束。

第一条,永远先问"验收标准是什么",再问"代码怎么写"。量化作业里,口径定义的重要性高于实现。代码错了能报错,口径错了可能一直不报错。

第二条,把审计当作流程的一部分,而不是最终检查的一步。每完成一个模块,就把中间结果拿给分析侧模型看一遍,眼光会干净很多。

第三条,保留每次执行的规格版本。我习惯用一份单独的spec_v1.md、spec_v2.md存档规格文档,记录每条口径的变更原因。如果作业被导师追问,我能直接说清楚每一项指标是怎么算出来的,这本身就是加分项。

最后再说一个小技巧:如果你发现审计模型和你在某个口径上反复拉扯,比如"手续费到底算单边还是双边",八成是这个口径在原始作业里也没说清楚。这种时候别急着猜,宁可先回到作业原文找线索,或者向布置作业的人确认。规格里写的每一句话,最终都要能追溯到作业要求本身,否则你今天定下的口径,明天就可能变成新的翻车点。

整个流程跑下来,我最大的感受是:不是 AI 不够聪明,而是我一开始把它的角色用错了。让它当执行者之前,先得有人把图纸画明白。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进