用大模型写策略代码的正确提示方式
让大模型写出可用的策略代码,关键不是措辞技巧,而是给足三样东西:确切的数据结构(列名、dtype、索引类型)、明确的时点约束(禁止未来函数)、以及先写测试的要求。「帮我写个均线策略」这类请求得到的代码几乎一定要重写,因为模型必须靠猜测填补你没说的部分。
本文要点
- 给出真实的 df.dtypes 输出比用文字描述数据结构有效得多,能消除大部分列名和类型错误。
- 必须在提示里显式写死时点规则:第 t 根收盘计算信号,第 t+1 根开盘成交。
- 要求模型先写测试再写实现,测试会把你没说清的假设逼出来。
- 让模型自查未来函数要给具体清单,问「有问题吗」只会得到风格建议。
- shift(-1)、全样本 fit、bfill 是三个必须在提示里明令禁止的写法。
模型不是猜不到,是你没说
「帮我写一个双均线策略的回测」——这句话里未定义的信息至少有十二项:数据从哪来、DataFrame 的列名是什么、索引是 DatetimeIndex 还是整数、均线用收盘价还是典型价、周期多少、交叉在哪根 K 线确认、在哪根成交、成交价用开盘还是收盘、手续费多少、滑点怎么算、仓位怎么定、要不要止损。
模型不会追问,它会用训练数据里最常见的组合把这十二项全部填上,然后给你一段语法正确、运行无误、但假设和你完全不同的代码。最糟的是它跑得通,还输出了漂亮的年化数字,你很容易就相信了。
所以提示工程在这里的本质不是话术,而是把规格说明写完整。你把四段式伪代码贴进提示里,模型的自由度就从十二项降到零,输出质量的提升是台阶式的。
第一要素:把数据契约贴进去
最省事也最有效的做法是直接把真实数据的元信息粘贴进提示,而不是用文字描述。三样东西必贴:df.dtypes 的输出、df.head(3) 的输出、df.index 的类型与时区。
为什么重要?因为列名猜错会直接报错(这还算好事,你能发现),而索引类型猜错往往不报错但结果全错。举个真实场景:你的索引是 UTC 的 DatetimeIndex,模型假设是本地时间的整数索引,生成的重采样代码会按错误的边界聚合,回测结果偏差可能达到几个百分点,而代码运行毫无异常。
同样要说明时间戳语义:索引是这根 K 线的开盘时间还是收盘时间。这个区别决定了信号在哪一行、成交在哪一行。交易所之间两种约定都有,模型无法从数据本身判断。
另外贴上数据的时间范围和总行数。这让模型知道样本量级,避免它写出在百万行数据上要跑几小时的逐行 apply。
- 列名与 dtype:直接粘贴 df.dtypes,不要写「有开高低收量五列」。
- 索引类型与时区:DatetimeIndex、tz=UTC、单调递增、无重复。
- 时间戳语义:明确是开盘时间还是收盘时间。
- 周期与行数:1h 周期,共 26280 行,覆盖 2023-01 至 2026-06。
- 已知缺陷:是否存在缺口、是否已去重、缺失值如何处理过。
第二要素:把时点约束写成硬规则
未来函数是 AI 生成策略代码时最高频的错误,而且它不会主动避免——训练数据里存在大量带未来函数的教程代码,模型学到的就是那些写法。
解决办法是在提示里写成不可协商的规则条款,用禁令式表述而不是建议式。「注意避免未来函数」这种说法基本无效;「所有特征列必须以 shift(1) 结尾,代码中不得出现 shift 的负数参数」这种就有效,因为它可被机械检查。
有四条禁令覆盖了绝大多数情况。禁止 shift 负参数,这是最直接的未来数据引用。禁止对全样本 fit,StandardScaler、分位数、均值方差都必须用 rolling 或仅用训练段统计量。禁止 bfill 与 interpolate,反向填充等于用未来值补当前值。禁止信号与成交同根 K 线同价格,第 t 根收盘确认的信号只能在第 t+1 根开盘成交。
把这四条写成固定文本块,每次生成代码时都贴上。它们不长,但能挡掉大部分虚高回测。
第三要素:要求先写测试
让模型在写实现之前先写测试,是提升代码可靠性性价比最高的一个要求。原因不在测试本身能捕获多少 bug,而在于写测试会强制把模糊假设显式化。
当模型被要求为「入场信号」写一个测试时,它必须构造一段输入数据并断言在第几行出现信号。这个断言会暴露它对时点的理解。如果它断言信号出现在交叉那一行、成交也在那一行,你立刻就看到问题了,而不用去读几十行 pandas 代码。
有几类测试值得每次都要求:时点测试(用构造数据验证信号行与成交行相差 1)、边界测试(数据不足窗口长度时应返回 NaN 而不是用部分数据计算)、成本测试(零手续费与真实手续费下收益应有确定差额)、无信号测试(横盘数据下不应产生交易)、未来函数回归测试(同一份数据截断到某个时点,之前的信号序列必须完全不变)。
最后一条尤其有价值,它是未来函数的机械检测手段:如果把数据从第 5000 行截断,前 4999 行的信号应当与用完整数据计算时完全一致。不一致就说明代码用到了后面的数据。这个测试可以自动化,比人工审查可靠得多。
| 测试 | 验证什么 | 断言写法 | 能挡住的错误 |
|---|---|---|---|
| 时点测试 | 信号行与成交行相差 1 | 成交 index == 信号 index + 1 | 信号与成交同根 K 线 |
| 边界测试 | 窗口不足时不交易 | 行数 < 最长窗口时信号全为 NaN | 用部分窗口数据算指标 |
| 无信号测试 | 横盘不产生交易 | close 恒定时交易数 == 0 | 条件判断写反或恒真 |
| 成本测试 | 成本确实被计入 | fee=0 的收益严格大于 fee>0 | 硬编码成本为零 |
| 截断不变性 | 不存在时点泄漏 | 全量前 5000 行信号 == 截断后信号 | 全样本 fit、shift 负参数、bfill |
一个可复用的提示模板
下面是一份完整模板,把你的具体策略填进对应位置即可。它比大多数人写的提示长十倍,但你只需要写一次,之后每次改的只有策略规格那一段。
# ===== ROLE =====
你是一名量化工程师,负责把给定的策略规格实现为可回测的 Python 代码。
严格遵守下面的数据契约与硬约束。规格中未定义的内容,不要自行假设,
列出问题清单让我补充;不要用默认值填空。
# ===== DATA CONTRACT =====
输入是单个 pandas.DataFrame,变量名 df。
df.dtypes:
open float64
high float64
low float64
close float64
volume float64
df.index: DatetimeIndex, tz='UTC', 单调递增, 无重复, 无缺口(已预处理)
索引语义: 该行时间戳 = 这根 K 线的【开盘时间】
周期: 1h 行数: 26280 区间: 2023-01-01 ~ 2026-06-30
df.head(2):
open high low close volume
2023-01-01 00:00:00+00:00 16541.8 16558.0 16535.2 16552.4 1043.27
2023-01-01 01:00:00+00:00 16552.4 16571.6 16548.9 16560.1 987.55
# ===== HARD CONSTRAINTS(违反任一条即为错误实现) =====
C1 禁止未来函数。特征只能使用第 t 行及更早的数据。
C2 代码中不得出现 shift 的负数参数(禁止 shift(-1) 等价写法)。
所有由第 t 行数据构成的特征,用于决策前必须已对齐到决策时点。
C3 信号在第 t 行收盘后确认,成交发生在第 t+1 行的 open 价。
不得用第 t 行 close 同时作为信号来源和成交价。
C4 禁止任何全样本统计量:不得对整个 df 做 fit / mean / std / quantile
后再用于早期行。需要标准化时使用 rolling 窗口。
C5 禁止 bfill / interpolate / fillna(method='backfill')。
窗口不足的行应为 NaN,不得参与交易。
C6 手续费与滑点必须计入,参数从函数入参传入,不得硬编码为 0。
C7 不得调用未在 requirements 中列出的库;不得虚构交易所 SDK 接口。
# ===== ALLOWED LIBS =====
python>=3.10, pandas, numpy (不要用 talib / backtrader / ccxt)
# ===== STRATEGY SPEC =====
<在此粘贴四段式伪代码:FILTER / ENTRY / SIZING / EXIT / COSTS / ASSUMPTIONS>
# ===== DELIVERABLE(按此顺序输出,分三步) =====
STEP 1 先只输出 pytest 测试文件 test_strategy.py,不要写实现。
必须包含以下测试,且每个测试自行构造最小输入数据:
t1 test_signal_and_fill_offset
构造一段确定会触发入场的数据,断言:
成交所在行 index == 信号确认行 index + 1
t2 test_insufficient_window_is_nan
数据行数 < 最长窗口时,信号列全为 NaN 且交易数为 0
t3 test_no_trade_on_flat_market
close 恒定的数据不产生任何交易
t4 test_cost_reduces_return
fee=0 与 fee=0.0005 两次运行,后者总收益严格更小
t5 test_truncation_invariance # 未来函数机械检测
用 df 全量算出 signal 序列 A;用 df.iloc[:5000] 算出序列 B;
断言 A.iloc[:5000] 与 B 完全相等(NaN 位置也相同)
STEP 2 等我确认测试无误后,再输出实现文件 strategy.py。
函数签名固定为:
def run_backtest(df, fee=0.0005, slippage=0.0005, **params) -> dict
返回 {"equity": Series, "trades": DataFrame, "stats": dict}
STEP 3 实现给出后,对照 C1-C7 逐条自查,输出一张表格:
| 约束 | 代码中的对应行 | 是否满足 | 依据 |
对每一条给出具体行号和判断理由,不要只回答"满足"。
# ===== 现在只做 STEP 1 =====
模板里有几个设计要点。分三步交付避免模型一口气生成几百行代码,中间任何理解偏差都会被放大;分步骤后你在测试阶段就能拦下来。约束编号(C1-C7)让 STEP 3 的自查可以逐条对应,比笼统提问有效得多。明令禁止虚构 SDK,因为模型很容易写出看起来合理但实际不存在的交易所接口方法。要求列出问题清单而不是自行假设,这一条能把你规格里的漏洞反馈回来。
prompt
让模型自查未来函数:问法决定结果
生成完代码后的审查环节,问法差异导致的效果差距非常大。开放式提问(这段代码有什么问题)会得到一堆变量命名、类型注解、异常处理的建议,真正的时点错误往往被淹没。
有效的做法是要求它做变量级的可用时点追踪:对代码里每一个中间变量,回答两个问题——这个变量在第 t 行的值,用到了哪些行的数据;这个值在真实交易中的哪个时刻才能确定。让它输出一张表。这种问法把审查从主观判断变成了机械核对,模型很难糊弄过去。
另一种有效手段是对抗式提问:假设这段代码存在未来函数,请找出最可能的位置并说明理由。给定「存在问题」的前提,模型不会敷衍地回答「看起来没问题」,会真的去找。这个技巧对代码审查普遍有效。
第三种是要求它自己构造反例:写一段能证明该代码存在时点泄漏的最小测试数据。如果它写不出反例,代码正确的概率就高一些;如果它写出来了,你直接就拿到了 bug 复现。
| 场景 | 差写法 | 好写法 | 差异原因 |
|---|---|---|---|
| 描述数据 | 有一份 BTC 的 K 线数据 | 粘贴 df.dtypes + head(2) + 索引 tz 与语义 | 消除列名、dtype、时区、时间戳语义四类猜测 |
| 描述策略 | 写个双均线策略 | 粘贴完整四段式伪代码,含成交时点与成本 | 把十余项未定义参数降为零 |
| 防未来函数 | 注意不要用到未来数据 | 列 C1-C7 编号禁令,明确禁止 shift 负参数与全样本 fit | 禁令可被机械检查,建议无法验证 |
| 交付方式 | 把完整回测代码给我 | 分三步:先测试、再实现、最后逐条自查 | 早期拦截理解偏差,避免长代码整体重写 |
| 代码审查 | 这段代码有问题吗 | 逐变量追踪可用时点并输出表格 | 把主观判断转为机械核对 |
| 缺信息时 | (不说) | 规格未定义的列成问题清单,不要自行假设 | 阻止模型用训练集常见值静默填空 |
| 库依赖 | 用合适的库实现 | 明确列出允许的库,禁止虚构 SDK 方法 | 避免不存在的接口与不必要依赖 |
| 修改代码 | 改一下止损逻辑 | 给出当前代码 + 只改 EXIT 段 + 保持其余不变并复跑测试 | 防止模型顺手重写无关部分引入回归 |
什么是常见问题?
提示写这么长,模型能记住全部约束吗?
长上下文里靠前和靠后的内容遵守率较高,中间部分容易被忽略。实践上把硬约束放在开头、把「现在只做 STEP 1」这类当前指令放在结尾效果较好。更可靠的是不依赖记忆——用测试和自查表做机械验证,尤其是截断不变性测试。
要不要让模型直接接数据源自己跑回测?
不建议一步到底。让它写纯函数(输入 DataFrame,输出结果字典),数据获取和结果检查由你控制。这样测试更容易写,也避免它虚构交易所接口。数据获取部分见交易所 API 与数据获取基础一篇。
模型说它已经检查过没有未来函数,可信吗?
不能直接采信。它的检查是对文本的推理,无法执行代码验证。把结论落到可运行的检查上:截断不变性测试、全特征额外位移一格的对照回测、以及逐行核对成交时点。三项都通过才算过关。
同一个提示为什么两次输出不一样?
生成本身带随机性,这也是必须有测试的原因——测试把「输出是否可接受」变成确定判断,而不是每次都靠人读代码。约束越具体、规格越完整,两次输出的行为差异就越小,尽管代码写法可能不同。