首页 / 资讯中心 / 文章详情

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

AI写量化作业先打磨需求规格再写代码:避免未来函数和回测失真 ★ FEATURED ARTICLE
说实话在试过这套配合之前我对“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 我实测下来最顺的六步循环按我后来稳定的流程整个循环是这样的把原始作业要求丢给分析侧模型不谈实现只谈目标和疑问。让模型逐条反问把数据、成本、口径、切分方式全部问到位自己补上缺失信息。要求模型输出一份完整规格文档用编号列出所有约束并附上验收标准。把规格文档原样交给编码工具明确要求按此实现不得自行修改口径。编码工具跑通后把结果净值曲线、指标表拿回给分析侧模型做审计。审计发现不一致或结果可疑回到第 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(dropTrue) 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(ddof1) * 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, }注意这里年化收益用的是几何年化夏普分母用的是样本标准差ddof1这两点都是规格文档里明确写过的口径。如果没有规格约束工具很可能会用算术年化、或使用总体标准差最后数值差一截你却说不清错在哪。4.5 回流验证把关环节绝不能省跑完脚本后我把训练段和验证段的净值曲线分别导出拿到分析侧模型那边做了一次审计。这一步我称之为把关环节也是我认为让整个流程有效的最重要部分。审计时我通常提如下几个问题验证段净值曲线和训练段净值曲线在形态上是否有明显差异指标数值是否在合理区间例如年化收益明显超过标的实际涨幅就需要警惕。手工抽样三笔交易核对手续费和滑点是否真的计入了每笔交易。样本外参数是否完全冻结有没有出现验证段数据被二次用于调参的痕迹。我这次遇到的情况是训练段年化 18%验证段年化只有 4%回撤明显放大。分析模型给出的判断是策略参数很可能在训练段过拟合不建议直接作为最终决策依据。这个反馈不改变代码实现但会直接影响我用什么指标去交作业也提醒我有没有必要缩小参数优化范围。如果审计结果对不上比如收益曲线出现断崖式跳变我会回到第 3 步修改规格补充交易信号需要额外过滤或涨跌停时不允许开仓这类边界条件再重新让执行工具跑一遍。这套循环多跑两轮出来的结果基本就是干净的了。5. 边界与注意事项不是所有作业都适合走这套流程5.1 什么时候用这套流程收益最大复盘之后我觉得这套先打磨、后执行的分工最适用的是中大型策略作业。特征是数据文件多、回测逻辑长、评价指标复杂、验收标准模糊。比如组合构建类作业要完成资产配置、权重优化、再平衡回测、归因分析这类作业代码量通常都在 500 行以上需要拆成多个模块。这时候需求里任何一条口径没定死后续代码都可能在局部正确的前提下整体失真打磨带来的收益非常大。另外如果作业要求里给出了很多定性描述比如合理设置交易成本在控制回撤的前提下追求收益这种模糊要求最适合先打磨因为打磨就是把合理控制这类词变成具体数字。5.2 什么时候别走这套流程反过来也有几种情况强行分两步反而低效。第一种是纯算法题。要求里输入输出已经说得明明白白测试用例都给好了直接让编码工具生成函数就行多绕一步打磨没有任何收益。第二种是 200 行以内的小脚本。比如只需要读取一个 CSV、算一个指标、画一张图这种任务规格写得再细也不会比直接生成更快。第三种是论文式或研究式的作业重点是创新想法而不是干净的实现。这种场景下过度强调规格和执行分离反而会限制模型与你进行开放式讨论的空间。你更需要的可能是让对话式模型直接陪你推演逻辑、生成实验草图而不是把它当成翻译机。5.3 使用这套流程时最容易踩的坑第一个坑规格文档写得太抽象。比如写考虑交易成本等于没写。我在实际操作中会把成本数值直接写进去最好连同计算举例一起放比如买入 100 股价格 10 元手续费1000×0.00050.5 元。只有给出具体数字执行工具才会生成对应细节。第二个坑执行工具自动优化口径。有时编码工具跑出来以后顺手把日收益换成百分比或出于好意在回测里加了个收益平滑这会破坏指标口径的一致性。我的做法是在给执行工具的最后一行里写明不要增加规格之外的逻辑不要修改规格之内的口径。第三个坑只有一次验证。有些同学跑通之后看到净值曲线就收了觉得稳了。但一次回测通过只能说明代码没有崩溃不能说明口径严谨。至少应该做一次手工抽样核对理想情况下再做一次参数扰动测试看看策略对参数是否敏感。这两步在量化作业的验收里往往比代码本身更值钱。第四个坑成本和时间控制。两套模型配合使用token 消耗自然比单个模型高。尤其规格文档反复打磨时API 调用次数会明显上升。我的经验是打磨最多三轮超过三轮说明你的需求描述本身有问题不如换个方式重写原始需求而不是继续在同一份需求上打转。6. 翻车之后我沉淀下来的使用纪律经过这几版作业我后来给自己立了几条纪律也算是对这篇实录的一个收束。第一条永远先问验收标准是什么再问代码怎么写。量化作业里口径定义的重要性高于实现。代码错了能报错口径错了可能一直不报错。第二条把审计当作流程的一部分而不是最终检查的一步。每完成一个模块就把中间结果拿给分析侧模型看一遍眼光会干净很多。第三条保留每次执行的规格版本。我习惯用一份单独的spec_v1.md、spec_v2.md存档规格文档记录每条口径的变更原因。如果作业被导师追问我能直接说清楚每一项指标是怎么算出来的这本身就是加分项。最后再说一个小技巧如果你发现审计模型和你在某个口径上反复拉扯比如手续费到底算单边还是双边八成是这个口径在原始作业里也没说清楚。这种时候别急着猜宁可先回到作业原文找线索或者向布置作业的人确认。规格里写的每一句话最终都要能追溯到作业要求本身否则你今天定下的口径明天就可能变成新的翻车点。整个流程跑下来我最大的感受是不是 AI 不够聪明而是我一开始把它的角色用错了。让它当执行者之前先得有人把图纸画明白。
阅读完成 · 觉得有帮助?
咨询建站