涨跌停开关开了又关,三组策略回测一分不差——聊聊回测截图里最容易被跳过的那层成本
本文数字取自 2026-07-16 的面板,为该日快照、不随数据滚动回改;最新数据见因子效力面板,完整口径与已知偏差见方法与口径。
一张策略截图告诉你半年翻倍<!-- copy-guard:allow 引用并反驳的典型截图话术,本文立场是拆解它而非主张它 -->,却没告诉你三件事:涨停那天信号到底买没买得进去、印花税按哪一年的税率算、滑点填的是多少——很多时候答案是"这个数字是算出来的,不是能落地的"。这篇文章不评价任何战法,也不点名任何人,只做一件事:把我们自己在产品里跑的几组真实回测原样摊开,包括亏钱的、包括我们自己踩的坑,让你下次看到一张回测截图时,知道先问哪几个问题。
投资有风险,入市需谨慎。以上为历史数据与回测,不代表未来收益,不构成投资建议。
一、先说清楚:成本口径有两套,不能混着看
回测里最容易注水的地方,不是收益曲线本身,而是曲线下面那层看不见的假设——交易成本。同一个策略,把佣金和滑点调到接近 0,夏普比就能凭空"做"上去。所以第一件事是把口径钉死。
我们产品里其实存在两套成本口径,用途不同,必须分开写清楚:
| 成本项 | 编辑器默认口径(本次实验用) | 市场发布强制统一口径 |
|---|---|---|
| 佣金 | 0.03% | 0.025%(万 2.5) |
| 印花税(卖出) | 0.05% | 0.05% |
| 过户费 | 默认未单列 | 0.001%(双向) |
| 滑点 | 0.1%(10bp) | 0.1%(10bp) |
| 最低佣金 | — | 5 元 |
左边这列,是策略编辑器里手动跑回测时的默认参数(本次实验用的就是它:10 万本金、佣金 0.03%、滑点 0.1%、印花税 0.05% 卖出、开启 T+1)。右边这列,是策略要发布到市场时被强制套用的统一口径 STANDARD_MARKETPLACE_COSTS。为什么发布环节要另立一套、还不许改?代码注释写得很直白:市场上的作弊,九成来自"用不真实(甚至为零)的成本去跑回测,把夏普 / 收益吹上去",所以每个上架策略都被强制穿过同一份经过审计的成本档,让不同策略的历史回测数字直接可比,常量本身也不许随意改(要改得走带版本号的迁移,好让历史挂牌可复现)。这不是可选项,是防作弊的地基。
两套口径的差别不大但真实:发布口径佣金更低(万 2.5 vs 万 3),却多收一道双向过户费。任何时候引用回测收益,都得先问它用的是哪一套口径。 下面的实验数字全部来自左边这套编辑器默认口径,不要和市场挂牌数字混用。
二、实验设计:想看开关差多少,结果差了 0
我们本来的问题很具体:回测面板上有个"涨跌停限制"开关(UI 上写的是「涨跌停限制(±10%成交失败)」),打开时会模拟"涨停买不进、跌停卖不出"。我们想演示——同一个策略,把这个开关打开和关掉,结果能差多少。
设计如下:选内置的「MACD 动量策略」模板(金叉且成交量放大 1.5 倍买入,死叉卖出,参数 12-26-9),分别在两只票上跑,涨跌停开关各开一次、各关一次。请求区间选"全部"(2021-06-02 → 2026-07-15)。这里先埋一个后面要展开的坑:请求虽然填了到 2026,实际回测用到的数据窗口只有 2021-06-03 → 2024-09-12,共 799 根日 K。
结果如下(收益 / 回撤 / 胜率 / 夏普全部逐字取自接口返回):
| 策略模板 | 标的 | 涨跌停开关 | 总收益 | 交易笔数 | 胜率 | 最大回撤 | 夏普比 |
|---|---|---|---|---|---|---|---|
| MACD 动量 | 002230 | 开 | -24.56% | 31(胜 10 / 负 21) | 32.26% | 45.05% | -0.334 |
| MACD 动量 | 002230 | 关 | -24.56% | 31(胜 10 / 负 21) | 32.26% | 45.05% | -0.334 |
| MACD 动量 | 000977 | 开 | -18.03% | 34(胜 11 / 负 23) | 32.35% | 50.23% | -0.216 |
| MACD 动量 | 000977 | 关 | -18.03% | 34(胜 11 / 负 23) | 32.35% | 50.23% | -0.216 |
先把不好看的说在前面:这个模板在这两只标的、这个窗口下的回测结果都是负的——002230 从 10 万本金跑到约 7.54 万,000977 跑到约 8.20 万,我们没挑好看的来展示。需要强调:这只是"该参数在这两只标的、这段窗口"的结果,不构成对 MACD 这类信号本身有效或无效的任何结论,更不是买卖建议;选这两只票只因回测总得有具体标的,不代表任何看多看空。
再看这次实验真正想问的问题——开和关,一模一样。同一只票,涨跌停开关打开与关闭,总收益、交易笔数、胜率、回撤、夏普,每一位小数都完全相同。我们逐条比对了接口返回里的每个统计字段,也确认了请求体里 enableCircuitBreaker 确实在两次之间从 true 翻成了 false——开关真的翻了,结果真的没动。更进一步:四组回测一共 4×799 条逐日日志里,"涨停 / 跌停 / 无法执行"的提及数,全是 0。
顺带补个成本感:光佣金加滑点两项,002230 那组就累计扣掉约 6796 元,接口"交易成本占比"字段显示 6.80%——初始 10 万本金的 6.8% 被交易摩擦吃掉(000977 那组是 7.81%)。这就是开头说的"看不见的那层":若有人把成本填成 0,这几个数字会立刻好看一截。
三、为什么开关没差别?两层机制
一个开关明明翻了,结果为什么纹丝不动?两层原因,第二层才是关键。
第一层,是信号本身的时机。 MACD 金叉是滞后触发——等到金叉成立,那波最猛的拉升往往已经降温了。所以"金叉当天恰好撞上涨停板买不进去"这种情况,在这段窗口里对这个策略就是没怎么发生。这层是"恰好没撞上"。
第二层,是引擎架构根本不给"当日追入"留入口。 这才是决定性的。回测引擎有一条硬规矩,写在代码注释里:信号在前一根 K 线(bar[i-1])收盘时生成,在下一根 K 线(bar[i])的开盘成交——"用今天的收盘数据,生成明天开盘执行的信号"。这是防未来函数(look-ahead bias)的设计:你不可能拿今天收盘才知道的信息,去成交今天盘中的价格。
副作用是:所有信号都"今日收盘生成、明日开盘成交",没有任何一条路径能让策略在信号当天盘中就追进去。那种"涨停板当天也能买进去、于是收益特别好看"的水分,在这个引擎里压根没有入口——不是我们特意堵的,是这条防未来函数的规矩顺带把它堵死了。涨跌停开关能影响的,只是"次日开盘那一刻能不能成交";而在这个窗口,次日开盘全都能成交,于是开关都一样。
四、那什么形态才真的会撞板?
涨跌停约束不是摆设,只是这次没触发到它。真正会让它生效的形态很具体:次日开盘就是一字板——开盘即封死涨停,买单在开盘那一刻根本成交不了。引擎对这种情况有对应的拦截逻辑:当入场那根 K 线是涨停状态时,这笔信号会被标成"无法买入"并从结果里剔除,而不是假装它成交了。
这条逻辑是有测试覆盖的,不是嘴上说说:测试里专门构造了"金叉出现、但入场 K 线是一字涨停(收盘 = 前收盘 ×1.10)"的场景,断言这类信号会被排除。也就是说,撞板拦截确实在工作,只是本次这三组策略在 2021-2024 这段窗口,恰好没有让任何一次入场落在"次日开盘一字板"上而已。
这也解释了结果页那几行小字的用处:"信号状态提示"会分三类如实报——"N 个信号因涨跌停无法执行 / 因停牌延迟出场 / 仍在持有中"。这次涨跌停那项计数就是 0,不是被藏起来,是真的没发生。而这个约束默认开着(配置项默认值为真),你得手动去关它。
五、我们自己的三个坑,也摊开
讲别人容易,讲自己得同样诚实。这次实验暴露了我们产品自己的三处问题,一并摊开。
坑一:单次回测有 800 根 K 线上限,而且当时保留的是最旧的那段(已修复,2026-07-18 合入主干)。 前面埋的坑就是它。取数逻辑里有一行 Math.min(days + 60, 800) 的上限,数据库当时又按日期升序取、取满 800 根就停——于是选"全部"(2021 到 2026)时,系统实际只给了最旧的约 800 根(2021-06-03 → 2024-09-12),最近约两年被静默丢弃。更别扭的是,结果页顶部还标着你请求的"2021-06-02 ~ 2026-07-15",只有翻到下面"回撤分析"那一小行,才露出真实区间"2021-06-03 ~ 2024-09-12"。
当前实现已经换了方向:取数改成按日期降序取满上限根数再翻回升序,也就是超上限时保留的是最近的那段,而不是最旧的那段。本文这种单只股票的回测,上限是 800 根(约 3.2 年),并且接口会额外返回"实际起算日"与"你请求的起始日"两个字段,区间被截断时结果页顶部直接弹横幅说明,不再只藏在回撤分析那一行小字里。
这里还得再诚实一层:多股 / 板块 / 组合回测走的是另一条取数路径,它按每只股票最多取 500 根(约 2 年),而且响应里没有那两个起算日字段,所以那条路径目前仍然是本文吐槽的"静默截断"——只是保留最近段而非最旧段。上限本身两条都还在——想覆盖更长的历史,得分段跑。本文正文的实验数字是修复前跑的,保留原样不回改。
坑二:布林带策略模板的文案,和引擎实际执行的方向是反的(已修)。 我们还跑了第三个模板「布林带突破策略」验证"是不是所有策略都 0 撞板"。结果发现:模板文案白纸黑字写着"价格突破布林上轨买入,跌破中轨卖出",可回测逐日日志里的信号原因却是"触及布林下轨"(买入)和"触及布林上轨"(卖出)——引擎实际执行的是均值回归(触及下轨买、触及上轨卖),和模板宣传的"突破买入"正好相反。核对代码确认这是引擎分支里的真实逻辑,文案与执行不一致是个 bug,已修复(2026-07-16 合入主干)。这里不评价两种做法哪个更好,只说明:模板文案必须和引擎真实行为一致,否则用户以为在测 A,其实在测 B。
坑三:第三组实验没有对照组,因为当天配额被跑光了(10/10)。 布林带这组(000977,涨跌停开关打开)结果是:总收益 +14.46%、9 笔交易、胜率 55.56%、夏普 0.072、同样 0 撞板。但必须如实说——这组没有"关闭涨跌停"的对照,当天回测配额已耗尽(10/10)。所以它只能作为"第三个不同策略、同一窗口同样没撞到涨跌停"的单点佐证,不能被读成"布林带开关也无差别"——那个对照没跑成,就不假装跑过。
六、给你的三个自查问题
这篇不是要告诉你"哪个策略能用",而是想给你三把尺子,下次看到任何一张回测截图时先量一量:
- 成本口径是什么? 佣金、滑点、印花税、过户费各按多少?有没有被调到 0 或调得异常低?一个把成本抹掉的回测,和真实交易之间隔着一层。
- 信号是当天成交还是次日成交? 如果是"当天信号、当天(甚至涨停板上)就成交",那涨停那天到底买没买得进去?"次日开盘成交"和"当天盘中成交",能差出天壤之别。
- 数据区间是不是完整的? 有没有因为某种截断,只用了其中一段——而且未必是你以为的那一段?截图上的时间范围,不一定等于回测真正用到的范围。
七、自己去跑一遍
不用信我们摊出来的这几个数,登录后打开首页 /dashboard 内嵌的策略编辑器 / 回测面板,选一个内置模板,把"涨跌停限制"开关打开跑一次、再关掉跑一次,自己对比。也可以顺手改改佣金和滑点,看成本口径对最终收益影响多大。工具就是拿来自己验证的,数据摆出来,判断和决策请你独立完成。
更新说明
- 2026-08-21:补注坑一的现状。单只股票回测已于 2026-07-18 修复(超上限时保留最新 800 根,并把实际起算区间显性化到结果页横幅);多股 / 板块 / 组合走另一条取数路径,上限为每只 500 根且尚无截断提示,第五节正文已把两条路径分开写明。正文实验数字仍是修复前跑的,保留原样。
- 2026-08-14:正文数字维持 2026-07-16 取数当天的原样。需要补注一处口径变化:本文之后平台修正了回测成本上报口径(买入腿费用曾漏计入 pnl、印花税曾漏计入总成本),成本相关字段的显示值因此与本文引用的当日返回值可能不再逐字相同;本文引用的是当时接口返回的原文,不回改。
风险提示与免责声明
本文所有分析、因子得分、回测结果均基于历史数据,不能代表未来收益。本平台提供的分析仅供参考,不构成投资建议,不提供任何针对具体证券的买卖 / 持有 / 目标价 / 仓位建议。所有策略仅供教育研究与回测验证,真实交易需独立判断并通过持牌券商执行。股票投资有风险,入市需谨慎,过往业绩不代表未来收益。AI 分析可能存在误差,请独立判断并自行承担投资风险。请根据自身风险承受能力理性投资,量力而行。
投资风险提示 | Investment Risk Warning
- •本平台提供的分析和建议仅供参考,不构成投资建议。
Analysis and suggestions provided are for reference only and do NOT constitute investment advice. - •股票投资有风险,入市需谨慎。过往业绩不代表未来收益。
Stock investment carries risks. Past performance does not guarantee future results. - •历史回测结果仅供策略评估参考,不代表实际交易表现。
Backtest results are for strategy evaluation only and do not represent actual trading performance. - •AI分析可能存在误差,请独立判断并自行承担投资风险。
AI analysis may contain errors. Please make independent judgments and bear investment risks yourself. - •请根据自身风险承受能力理性投资,量力而行。
Please invest rationally according to your own risk tolerance and financial capability.