从想法到伪代码:策略描述的标准格式
把策略想法写成代码之前,先用入场、出场、仓位、过滤四段式把它写成没有歧义的伪代码。判断标准只有一条:把这段描述交给三个不同的人去实现,三份代码的回测结果必须一致。做不到,就说明描述还不够精确。
本文要点
- 四段式是策略描述的最小完备结构:入场、出场、仓位、过滤,缺一段就无法实现。
- 「突破后买入」不是策略描述,因为突破的窗口、幅度、确认方式和成交时点全都未定义。
- 每个条件都必须写清用哪根 K 线的哪个字段,以及在哪根 K 线成交。
- 出场必须包含止损、止盈和时间退出三类,只写止盈的策略无法上实盘。
- 先写伪代码再写代码,能在成本最低的阶段发现逻辑漏洞。
为什么必须先写伪代码
跳过伪代码直接写 Python,看起来省了一步,实际上代价很高。问题出在你会在写代码的过程中即兴做决定——止损用固定百分比还是 ATR 倍数、信号在收盘确认还是盘中触发、同时出现多个信号怎么处理。这些决定散落在代码各处,你自己都记不清做了什么假设,回测结果自然无法解释。
伪代码的作用是把所有决定集中到一个地方、用自然语言显式写出来。它不需要能运行,但必须能被无歧义地翻译。这带来三个直接好处。
第一,逻辑漏洞暴露得早。写伪代码时你会发现「价格回落就平仓」根本没定义回落多少,而这个问题在代码里可能表现为一个随手写下的 0.02,永远没人再审视。第二,交给 AI 生成代码时命中率大幅提高,因为模型不需要猜你的意图。第三,回测结果异常时,你有一份权威的规格说明可以逐条核对代码是否忠实实现。
第一段:入场——什么条件下建仓
入场段要回答四件事:用什么数据、什么条件触发、在哪个时点成交、方向是多还是空。四件事缺一件,代码就会有多种写法。
最常见的问题是时点不明。「20 日均线上穿 60 日均线时买入」这句话有三种实现:用第 t 根收盘价确认交叉、第 t 根收盘价成交(含未来函数,因为收盘价确定时该 K 线已结束);用第 t 根确认、第 t+1 根开盘成交(正确);盘中实时判断交叉、立即成交(需要 tick 数据,且交叉可能来回反复)。三种写法的回测收益可能差出一倍。
第二个问题是条件本身没有量化。「突破前期高点」——前期是多少根 K 线?突破需要超过多少幅度才算,还是碰到就算?收盘价突破还是最高价突破?需要连续几根确认?每个未回答的问题都是一个自由参数,而未定义的自由参数会在实现时被随意填上。
第三个问题是重复入场规则缺失。条件持续满足时是每根 K 线都加仓,还是只在条件首次满足的那一根开仓?这决定了策略是单笔还是金字塔加仓,性质完全不同。
- 数据来源:哪个交易对、哪个周期、用 open / high / low / close / volume 的哪个字段。
- 触发条件:用完整的比较式写出,所有窗口长度和阈值都填具体数字。
- 确认时点:在第 t 根 K 线收盘后判断(默认写法)。
- 成交时点与价格:第 t+1 根开盘价成交,或第 t+1 根以限价单挂在某价位。
- 重复处理:条件首次满足才开仓,已有持仓时忽略新信号。
第二段:出场——三类退出必须都写
出场是新手写得最草率的一段,通常只写了「达到目标就卖」。完整的出场必须覆盖三类,缺任何一类都会在某种行情下卡死。
止损:亏损到什么程度必须无条件离场。写法上要指明基准(入场价、还是最近的波动区间)、幅度(固定百分比、ATR 倍数、还是某个结构位)、以及判断字段。用最低价判断止损和用收盘价判断止损差别很大:前者会被盘中插针扫掉,后者可能当天亏损远超设定值。两种都可以,但必须明确选哪种,因为回测和实盘要一致。
止盈或跟踪退出:固定止盈简单但会切断趋势;跟踪止损能吃到大段行情但会回吐一部分利润。跟踪止损要写清从哪个点开始跟踪、回撤多少触发。
时间退出:持仓超过 N 根 K 线仍未触发止损止盈时强制平仓。这一条最常被漏掉,但它很重要——没有时间退出的策略会出现持仓被无限期占用的情况,资金效率极低,而且回测里可能有一笔持仓横跨半年,严重扭曲统计结果。
| 段落 | 必须写明的项 | 漏掉的后果 |
|---|---|---|
| 入场 | 数据字段、条件数值、确认时点、成交时点、重复处理 | 同一描述出现多种实现,回测结果差数倍 |
| 出场 | 止损、止盈或跟踪、时间退出、判断字段 | 缺时间退出会出现持仓被无限期占用 |
| 仓位 | 单笔规模口径、单标的上限、总敞口上限 | 多信号同时触发时意外满仓 |
| 过滤 | 流动性、波动率、市场状态、回撤停机线 | 策略失效期继续消耗资金 |
| 成本 | 手续费率、滑点、是否含资金费率 | 高频策略回测由盈转亏 |
| 假设 | 杠杆、方向限制、成交量假设 | 半年后无法复现当初的简化条件 |
第三段:仓位——每次投入多少
仓位段决定同样的信号能带来多少收益和多大回撤。它至少要回答:单笔投入多少、最多同时持有几个仓位、总敞口上限是多少。
三种常见写法各有适用场景。固定金额最简单,每笔投入固定的数额,适合初期验证逻辑,缺点是不随账户规模调整。固定比例是每笔投入账户权益的固定百分比,天然具备复利与缩量效果,是最常用的做法。固定风险更严谨:先定好单笔最大亏损占权益的比例(比如 1%),再根据止损距离反推仓位大小,止损越远仓位越小,使每笔交易的风险敞口一致。
还要写清总敞口约束。多标的策略里,如果每个标的都按 20% 仓位建仓,同时触发五个信号就满仓了,而这些标的在极端行情下往往同涨同跌,实际风险远高于分散预期。伪代码里要明确总敞口上限,以及超限时是拒绝新信号还是按比例缩减。
第四段:过滤——什么时候不做
过滤段是四段里最容易被省略、但对实盘结果影响最直接的一段。它规定在什么条件下即使入场信号满足也不开仓。
常用的过滤维度包括:流动性过滤(成交额低于阈值不做,避免滑点吞掉利润)、波动率过滤(波动过低时趋势策略容易反复被打,波动过高时止损容易被瞬间击穿)、市场状态过滤(趋势策略在横盘期停止交易)、时间过滤(避开重大数据公布或结算时点)、风控级过滤(账户回撤超过阈值时全局停止开新仓)。
最后一条尤其重要。回撤停机线是把风控写进策略而不是留在心里的唯一方式:当权益从峰值回落超过设定比例时,停止一切新开仓,只允许平仓。它不会提高收益,但能防止策略在失效期继续消耗资金。指标层面的取舍可以参考四个核心指标那篇。
| 模糊写法 | 问题所在 | 精确写法 |
|---|---|---|
| 突破后买入 | 窗口、幅度、字段、时点全未定义 | 第 t 根收盘价 > 前 20 根 high 的最大值 × 1.001,第 t+1 根开盘市价买入 |
| 成交量放大时入场 | 放大相对于什么、多少倍未定义 | 第 t 根 volume > 前 50 根 volume 均值 × 1.5 |
| 回落就止损 | 回落基准与幅度未定义 | 收盘价 < 入场价 × 0.97 时,下一根开盘平仓 |
| 趋势明确时做多 | 趋势无操作定义 | 收盘价 > 200 周期均线,且该均线近 20 周期斜率为正 |
| 适当控制仓位 | 无任何数值 | 单笔风险 = 权益 × 1%,仓位 = 单笔风险 ÷ 止损距离 |
| 行情不好就休息 | 不好无定义 | 权益自峰值回撤 > 15% 时停止开新仓,仅允许平仓 |
| 流动性够的币才做 | 够无阈值 | 过去 24 小时成交额 > 5000 万 USDT |
完整的四段式模板
下面是一份可以直接复制修改的模板,用一条趋势跟随策略填充。注意每一行都可以被唯一地翻译成代码,没有需要读者猜测的地方。
STRATEGY: 20/60 均线趋势跟随(单标的、仅做多、无杠杆)
DATA:
symbol = "BTCUSDT"
interval = "1h"
fields = [open, high, low, close, volume] # 索引为 UTC 开盘时间
注:所有特征只使用第 t 根及更早的数据;成交发生在第 t+1 根开盘。
INDICATORS:
ma_fast[t] = mean(close[t-19 .. t]) # 20 周期
ma_slow[t] = mean(close[t-59 .. t]) # 60 周期
atr[t] = mean(true_range[t-13 .. t]) # 14 周期 ATR
vol_ma[t] = mean(volume[t-49 .. t]) # 50 周期成交量均值
# ---------- 1. FILTER:先判断今天是否允许交易 ----------
FILTER:
f1 = volume[t] > vol_ma[t] * 1.2 # 流动性/参与度
f2 = atr[t] / close[t] > 0.005 # 波动率下限,太死不做
f3 = atr[t] / close[t] < 0.060 # 波动率上限,太疯不做
f4 = equity_drawdown[t] < 0.15 # 权益回撤未超 15%
allow_entry = f1 AND f2 AND f3 AND f4
# ---------- 2. ENTRY:入场 ----------
ENTRY (long):
cond_cross = ma_fast[t] > ma_slow[t] AND ma_fast[t-1] <= ma_slow[t-1]
IF cond_cross AND allow_entry AND position == 0:
在第 t+1 根 open 以市价买入 size(见 SIZING)
记录 entry_price = 第 t+1 根 open
记录 stop_price = entry_price - 2.0 * atr[t]
记录 peak_price = entry_price
记录 bars_held = 0
# 条件持续满足不加仓:已有持仓时忽略新信号
# ---------- 3. SIZING:仓位 ----------
SIZING:
risk_per_trade = equity[t] * 0.01 # 单笔最大风险 1% 权益
stop_distance = 2.0 * atr[t]
size = risk_per_trade / stop_distance
size = min(size, equity[t] * 0.30 / close[t]) # 单标的敞口上限 30%
# 多标的场景追加:sum(所有持仓名义价值) <= equity * 0.60
# ---------- 4. EXIT:出场(三类,按优先级依次判断) ----------
EXIT:
bars_held = bars_held + 1
peak_price = max(peak_price, close[t])
e1 STOP_LOSS: IF close[t] <= stop_price
e2 TRAILING: IF close[t] <= peak_price - 3.0 * atr[t] AND close[t] > entry_price
e3 SIGNAL_OUT: IF ma_fast[t] < ma_slow[t]
e4 TIME_OUT: IF bars_held >= 240 # 约 10 天未走出趋势
IF e1 OR e2 OR e3 OR e4:
在第 t+1 根 open 以市价全部平仓
清空 entry_price / stop_price / peak_price / bars_held
# 止损无条件执行,不存在任何例外分支
COSTS (回测必须设置,不可省略):
taker_fee = 0.0005 (单边)
slippage = 0.0005 (单边,按成交额计)
ASSUMPTIONS (显式记录,便于日后核对):
- 不使用杠杆,不做空
- 忽略资金费率(现货标的)
- 假设第 t+1 根开盘价可全额成交(大额资金需重估)
这份模板里有几个刻意的设计。过滤段放在最前面,因为它是否决权,先判断能省掉后面的计算。成交时点在每一处都显式写出,这是防未来函数最有效的手段。成本参数写进规格,避免回测时忘了加。假设单独成段,因为半年后你回头看时,最想知道的就是当初做了哪些简化。
写完模板之后再交给 AI 生成实现代码,效率和正确率都会明显高于直接口述需求。具体的提示写法见用大模型写策略代码的正确提示方式,实现层面的向量化陷阱见向量化 vs 逐 K 线,完整学习路径在量化学院。
pseudocode
什么是常见问题?
伪代码要写到多细才够?
标准是可被唯一翻译。具体检验方法:通读一遍,把所有形容词、副词和没有数值的条件标出来,全部替换成具体数字和明确字段;再确认每个条件用了哪根 K 线的哪个字段、成交发生在哪根 K 线。都做到了就够细了,再细就变成写代码了。
四段的顺序可以调整吗?
描述顺序不重要,但执行顺序有讲究。代码里建议先算过滤(否决权最高,能提前跳过),再判出场(已有持仓优先处理),最后判入场。把入场放在出场之前容易出现同一根 K 线先平后开的重复交易,回测里表现为交易次数异常偏多。
策略只有做多逻辑,需要写做空段吗?
不需要,但要在假设段里显式写明「不做空」。显式记录限制条件和写下规则同样重要,否则日后你或别人读这份规格时,会不确定做空是被排除了还是忘了写。
写好伪代码后回测结果不理想,先改哪一段?
先不要改参数。按顺序核对:成交时点是否有未来函数、成本是否已计入、交易笔数是否足够多。三项确认无误后,优先检查出场段和过滤段,这两段对结果的影响通常大于入场条件的参数微调。反复微调入场参数是过拟合的主要来源,回测数字为历史数据,不构成收益承诺。