Insights·2026-07-27

让 AI 代理回测交易规则前,必须先说清楚哪些事?

回测就是把自己的交易规则套用到过去的行情上,算出如果真按这套规则买卖,现在账户会是多少钱。用 Toss 证券 Open API 的 K 线接口取回日线的开高低收,再把规则用大白话讲给 Claude Code 或 Codex 这类终端 AI 代理,几分钟内就能拿到成交明细和收益率。但真正决定结果的不是规则,而是你没有说明的假设。判定时点、成交价格、交易成本、数据区间这四项若不钉死,代理不会反问,它会悄悄挑一个最像样的值填上,而那个选择就成了你的业绩表。实测中,同一套规则、同一份数据用在三倍杠杆 ETF 上,只改动平仓成交假设一项,五个月收益率就在 15% 与 95% 之间分开。

承接前文要让 AI 通过 Toss 证券 Open API 交易股票,需要准备什么?
KORU 일봉 차트 위에 매매 시점을 표시한 백테스트 결과 그래프. 위쪽은 종가 선과 매수·매도 삼각형 표시, 음영으로 보유 구간을 나타내고, 아래쪽은 전략 자산 곡선과 단순 보유 자산 곡선을 비교한다.
토스 Open API 일봉(수정주가)으로 돌린 KORU 백테스트. 위 음영이 보유 구간, 아래는 전략과 단순 보유의 자산 곡선 비교다.

回测是什么

回测是把交易规则原样套用到过去行情上的做法。事先定好什么时候买、什么时候卖,然后一天一天地推过去几个月或几年,算出照此买卖如今账户会是多少。它是在真金白银入场之前,确认规则是否说得通的一道程序。

之所以值得做,是因为凭感觉立下的规则大多在数字面前站不住。脑子里听着合理的规则,一放到真实行情上,信号要么太慢,要么太频繁,要么在关键区间反向而动。回测让你在付出学费之前就知道这件事。

同时回测并不是预测未来的工具。过去有效并不保证将来有效。回测真正筛掉的是明显糟糕的规则,剩下的也只是当时不糟糕的规则而已。一开始不接受这个区别,就会过度依赖那些数字。

从前要做这件事,得先把行情数据弄到手、整理成表,再自己写循环。现在两样东西就够了:一个提供行情的 API,以及一个在自己电脑上写代码并运行的 AI 代理。

先取数据 — 用 K 线接口拿日线 OHLC

先说清一个词。K 线把一天(或一分钟)的价格波动压缩成四个数字:开盘价、最高价、最低价、收盘价,取首字母叫 OHLC。一根 K 线代表一天就是日线,一串日线正是回测所需的最少原料。

Toss 证券 Open API 通过 K 线接口提供这份数据。有上一篇里拿到的访问令牌就够,不需要账户请求头,因为行情是对所有用户一视同仁的客观数据。

实际使用时会卡住的地方有两处。第一,单次调用最多返回 200 根。一年的日线约 250 根,一次取不完,要拿着响应里附带的游标往更早的方向续接。第二,必须取复权价。有过分红或拆股的标的若用原始价格,拆股当天价格会腰斩,回测会把那天误当成暴跌。

取回的 K 线按日期升序排好,重复日期合并成一条。分页续接时边界上常会重复同一天,放着不管这天就被算两次,收益率会被悄悄抬高。

K 线请求 — 200 根日线(仅需令牌)
TOKEN=eyJ...

curl -s 'https://openapi.tossinvest.com/api/v1/candles?symbol=KORU&interval=1d&count=200&adjusted=true' \
  -H "Authorization: Bearer $TOKEN"

# 响应(节选)
# {"result":{
#   "candles":[{"timestamp":"2026-07-27T00:00:00",
#               "openPrice":18.02,"highPrice":18.9,
#               "lowPrice":17.71,"closePrice":18.45,"volume":...}],
#   "nextBefore":"..."}}
需要超过 200 根时 — 用 nextBefore 续页
import os, requests

BASE = "https://openapi.tossinvest.com/api/v1/candles"
H = {"Authorization": f"Bearer {os.environ['TOSS_TOKEN']}"}

def daily_ohlc(symbol, need=400):
    rows, before = {}, None
    while len(rows) < need:
        p = {"symbol": symbol, "interval": "1d",
             "count": 200, "adjusted": "true"}
        if before:
            p["before"] = before
        r = requests.get(BASE, params=p, headers=H, timeout=10).json()["result"]
        if not r.get("candles"):
            break
        for c in r["candles"]:                 # 用日期作键去重
            d = c["timestamp"][:10]
            rows[d] = {"d": d, "o": float(c["openPrice"]),
                       "h": float(c["highPrice"]), "l": float(c["lowPrice"]),
                       "c": float(c["closePrice"])}
        before = r.get("nextBefore")
        if not before:
            break
    return [rows[d] for d in sorted(rows)]     # 由旧到新

把规则交给代理时必须钉死的四件事

这才是本文的核心。把规则一讲,脚本几分钟就出来了。问题在于一句话的规则里总留着空白,而代理不会就此反问。它会用看起来最像样的值悄悄填上,然后给你一张非常像样的收益表。

空白通常有四处。把这四项和规则一起写下来,结果的摆动幅度会大幅收窄。

第一是判定时点。一天当中什么时候检查条件?按收盘价判定,和盘中价格一碰到就触发,是两套完全不同的策略。尤其买入信号若按盘中判定,那些收盘时早已站不住的假突破也会被全部买进。

第二是成交价格。条件满足后,按什么价格算作买卖成交?当天收盘价、条件线本身,还是次日开盘价?后面会看到,这一行对结果的影响最大。

第三是交易成本。手续费、税费以及买卖价差造成的损耗,按往返百分之几计入。若设为 0,频繁交易的策略会比实际好看得多。

第四是数据区间。用哪一段时间?仅仅挪动起始日,排名反转的情况很常见。所以只在一个区间里胜出,还不能算结论。

项目不指定会怎样这样写清楚
判定时点盘中信号全被计入,混进现实中根本买不到的交易买入条件仅按收盘价判定,忽略盘中瞬时突破
成交价格被填成最有利的价格,收益率被抬高平仓按条件线价格成交;跳空时按当天开盘价成交
交易成本按 0 计算,频繁交易的策略被高估每笔交易扣除单边 0.05%、往返 0.1%
数据区间挑到最好看的区间,结论被推翻最近 1 年、3 年、全区间分别输出
交付规则时使用的提示词格式
请用这份 K 线数据写一个回测以下规则的 Python 脚本。

[规则]
- 区间开始时以满仓持有状态起步
- 持有期间自最高点回撤 20% 即全部卖出(最高点按盘中最高价更新)
- 空仓期间收盘价突破此前 20 个交易日新高即全部买入
- 持有期间 RSI(14, Wilder) 超过 80 即全部卖出

[假设 — 必须照此执行]
- 买入与 RSI 按收盘价判定,移动止盈按盘中触碰判定
- 移动止盈成交价 = 最高点 x 0.8;开盘跳空更低时按开盘价成交
- 每笔交易扣除往返 0.1% 交易成本
- 区间分别输出最近 1 年 / 3 年 / 全区间

[输出]
- 成交明细(日期、方向、成交价、原因)
- 各区间收益率、最大回撤、与单纯持有的对比

同一套规则为何分出 +15% 与 +95%

光靠说不够直观,于是我实际跑了一遍。标的是 KORU,一只以三倍跟踪韩国市场的美国上市杠杆 ETF。规则就是上面那套:自最高点回撤 20% 全部卖出、收盘突破 20 日新高全部买入、RSI 超过 80 时过热止盈。区间为 2026 年 2 月 26 日至 7 月 22 日,约五个月。

同样的规则、同样的 K 线,跑了两遍。改动只有一处:平仓成交假设。第一遍按条件满足当天的收盘价成交,第二遍按价格触碰条件线那一刻的价格成交。

收盘成交为 15%,触碰成交为 95%。同期若只是持有则是负 33%。规则一个字都没改,结果却拉开了八十个百分点。

差距只来自一天。六月初见顶之后,这只基金几天之内腰斩。若按触碰条件线成交,是在 49 美元附近离场;若等到当天收盘才卖,则是 30 美元离场。因为是三倍产品,单日跌幅巨大,于是这一项假设支配了整个结果。

哪一边正确,取决于自己的系统实际如何运作。把条件单挂在券商服务器上,价格触及监视线的瞬间就会下单,接近触碰成交;反之,每天看一次收盘价由人判断、次日再下单,则比收盘成交还要吃亏。所以这不是口味问题,而是要照搬自己实际操作方式的一个取值。

而且无论取哪一边,真实成交都更差。急跌之中未必能按想要的价格全部卖出,市价单还会随着价差扩大而进一步滑点。这正是不能把回测数字直接当作预期收益来读的原因。

两行成交假设 — 差出八十个百分点
# A. 收盘成交 - 按收盘价确认条件,并按该收盘价计为卖出
if close[i] <= high * 0.8:
    fill = close[i]

# B. 触碰成交 - 盘中最低价触及条件线,即按该线计为卖出
level = high * 0.8
if low[i] <= level:
    fill = min(level, open[i])   # 开盘跳空更低则按开盘价成交

# 同一规则 · 同一数据 · 2026-02-26 ~ 07-22
# A -> +15%   B -> +95%   单纯持有 -> -33%

在 KORU 上实际验证过的变体

假设固定下来之后,真正的实验才开始:在同一骨架上一次只改一处。在三倍杠杆 ETF 上,这些变体大致分出了如下结果。

出现得最多的想法是让再入场更容易,因为等 20 日新高显得太慢。于是把等待窗口缩到 10 天,把门槛打折让它在略低于新高时就买,又换成 RSI 30 或均线交叉这类反弹信号。这些方向各异的尝试,全都不如原版。

反倒只有两处改动带来改善:把等待窗口拉长到 30 天,以及在 RSI 超过 80 的过热区间不等移动止盈先行止盈。也就是说,这套策略的力量不来自聪明的入场信号,而来自等到新高被确认为止的耐心。

我也试过把仓位拆成两份、分别设置不同止盈比例的所谓口袋分割。一半按 15%、另一半按 20% 卖出,看起来能缓和回撤,实际并没有。同一只标的,两份仓位一起承受同一场暴跌,只是离场日期错开几天,最大回撤几乎没变;反倒因为混入了始终更差的那组参数,收益被削掉一块。拆分相关性为 1 的资产并不是分散。

把在别的市场行得通的逻辑搬过来同样失败。分钟级加密交易里用的多指标 z 分数集成,一旦铺到日线 ETF 上就产生了数千笔交易。成本设为 0 时看着像样,单边算 0.05% 就崩了。问题不在逻辑质量,而在时间尺度与成本结构不匹配。

还有一点。在两倍产品上表现最好的 200 日均线过滤,放到三倍上反而有害。波动更大,于是在均线附近反复买卖,只累积了亏损。即便是同一系列的产品,倍数不同,合适的过滤器也不同。

改动意图结果
再入场窗口 20 天 → 10 天更快再入场大幅落后
入场门槛打 10% 折扣在新高之前先买单调恶化
改用反弹信号再入场(10 种)抄底全部落后
再入场窗口 20 天 → 30 天确认更久改善
加入 RSI 80 过热止盈在高点附近先行离场改善
止盈比例分割(15+20、20+25)缓和回撤三个区间全部落后
移植分钟级 z 分数集成复用其他市场的逻辑计入成本后崩溃
200 日均线过滤跌破趋势即离场两倍最优,三倍不适用

回测悄悄撒谎的五个地方

结果好看时,这几处是最该先怀疑的。五项都不会报错,只会给出一个漂亮的数字,所以必须靠眼睛去找。

第一是前面看到的成交假设。若按在条件线上精确卖出来计算,暴跌行情中的表现会远好于现实。加上一点滑点重跑一遍,看看结论是否还站得住。

第二是未来信息渗入。如果某个需要知道当天收盘价才能算出的值,被用来在当天盘中交易,那么现实中根本不可能的交易就进入了业绩。指标计算里包含了当前这根 K 线,是最常见的出事点。

第三是成本为 0。先看交易次数,乘上往返成本,算一算扣掉之后还剩不剩。不剩的话,这个策略就不必再回测了。

第四是过拟合。一点点调参去找最好成绩,最后会落到只有那个取值才好的地方。前面采用的 RSI 80,也只在 78 到 82 这段狭窄区间里表现良好。窄峰大多是偶然。要选那种上下摇动之后仍然平缓保持的取值。

第五是数据与可复现性。不用复权价,除权日和拆股会被当成暴跌;只用当前在市的标的验证,已消失的标的被剔除,结果会变好看。而把回测脚本放在临时目录里,最后只剩结论、无法复现。我自己就踩过这个坑:几个月前跑过的脚本没了,这次只好从头重写。请把规则、假设和脚本连同结果一起放进代码仓库。

把验证过的规则搬到真实账户时

通过回测并不等于可以立刻上实盘。填补两者之间落差是有顺序的。

首先,让回测假定的判定时点与实际系统的判定时点对齐。如果是以收盘确认为前提做的验证,实盘下单也必须只在收盘前那段时间发出。这一点不对齐,就会不断被盘中假突破触发,变成与回测不同的另一套策略。

接着,把需要计算的判定与单纯的监视分开。像移动止盈那样基准线每天变动,或像 RSI 那样需要计算的条件,交给自己的脚本;单纯的价格触达监视交给券商条件单,这样自己的电脑关着,券商服务器也在盯。应对突发跳空下跌的安全网也挂在这里。

而实盘下单的闸门,就保持上一篇里做好的样子:默认演练模式,实盘下单同时要求执行标志与标的确认,超过金额上限即拒绝,用幂等键防止重复。回测越好看,越会想把这些闸门松开;而越好看的回测,需要被验证的假设越多。

最后是规模。即便是验证过的规则,一开始也要用输得起的金额,杠杆产品更是如此。自动交易在规则错了的时候,会又快又忠实地重复那个错误。本文数字来自特定标的与特定区间,不构成投资建议。判断与责任均在本人。