止损:从心理问题变成数学问题
你在成本价下方 3% 的地方设了止损。第二天开盘,价格直接跳到比那个位置还低 5% 的地方才有第一笔成交,你的单子在那儿出掉了。
止损执行了。你亏的不是 3%,是 8%。
这件事让很多人第一次意识到:自己脑子里那个「止损 = 亏到 3% 就出来」的想法,和市场里真正发生的事,中间隔着好几层。而这几层,恰好就是量化系统必须逐个写清楚的东西——一个止损位在系统里从来不是一个数,它是三样东西:一个价格、一条判定规则、一笔订单。 这三样各自都会跑偏,跑偏的方向还不一样。
止损这个动作本身是什么,术语卡里一句话讲完了。这篇要说的是另一件事:当你不再用手点鼠标,而是让程序替你执行它的时候,这个「事先定好、事中执行」到底被拆成了什么。
先说人话:警报响了,不等于人已经出来了
想象一栋楼装了火灾报警器。你关心的其实是两个完全不同的问题:
一是警报什么时候响——温度到多少度触发?这是一个你可以事先设定的阈值。 二是响了之后你多久才能真站到楼外——楼梯有多挤、门有没有卡住、你在几层。这个不归你设定,归当时的现场情况。
绝大多数人在讨论止损的时候,只在讨论第一个问题(警报设在几度),却默认第二个问题不存在——以为警报一响,人就自动在楼外了。市场里那些「明明设了止损还是亏了一大截」的经历,几乎都发生在第二个问题上。
再换个更贴近的比方:你跟司机说「超过 120 就减速」,这是触发条件;车实际停下来的位置,取决于刹车距离。触发价和成交价是两个数,中间那段就是刹车距离。 平静的路面上它可以短到忽略不计,塌方的路面上它可以长到吓人。
投资层:止损位该从哪儿来
先解决一个更前置的问题:那个数字从哪儿来。
多数人的答案是「亏 3% 就走」或者「跌破 10 块整数位就走」。这两种定法有个共同毛病——它们跟这个品种本身平时晃多大完全没关系。
同样是 3%,对一个平时一天上下晃动不到 1% 的品种来说,是一道结结实实的防线;对一个一天能晃四五个点的品种来说,你的止损位压根就落在它日常呼吸的范围里。后一种情况下你不是「被行情证伪了」,你是被噪音扫出来了——方向可能完全没判错。
所以量化系统里对止损位的定法是反过来的:先量这个品种现在晃多大,再把止损放到日常晃动之外。 衡量「晃多大」最常用的工具是 ATR(平均真实波幅,简单说就是「最近一段时间平均每天能晃多少钱」),这套通用做法在 ATR 平均真实波幅 那篇里讲得更细,包括它为什么要把隔夜跳空也算进波动,以及为什么它的绝对值不能跨品种直接比。
这里只留一句最要紧的分工,因为下面全靠它:波动率决定止损放多远,你能承受的亏损金额决定这一笔买多少。 这是两件事,混在一起想是很多人止损用不好的根源。
真正的增量在下一层——当这个想法要变成一行能跑的代码时,会冒出一堆你原本不会想到的问题。
系统层之一:那个价格,是被算出来又被反复重算的
在 freqtrade 里,一笔交易开仓的那一刻就会被赋予一个止损价,算法就是从「愿意亏的比例」反推一个绝对价格:
if self.is_short:
new_loss = float(current_price * (1 + abs(stoploss / leverage)))
else:
new_loss = float(current_price * (1 - abs(stoploss / leverage)))
两个细节值得停一下。
除以杠杆的那一步。 上游文档把 stoploss 定义成「这一笔我愿意承担的风险」,而不是「价格要移动多少」。带杠杆的时候这两个数不是一回事:同样是愿意亏本金的一成,杠杆越高,价格只需要动很小一点就够了。把「愿意亏多少钱」和「价格要走多远」当成同一个数,是杠杆交易里最容易出事的误解之一——你以为你在设一个价格,其实你在设一个损失额度。
取整那一步。 算出来的价格还要按交易所的最小价位取整,而取整方向是有讲究的:多头的止损价往上取,空头的往下取。都是朝着「更早触发」的那一侧。这不是代码洁癖,是一个态度——遇到模棱两可的半个价位,宁可让止损早响,不让它晚响。
还有一条硬规矩,写在代码注释里,短得像句口号:
# stop losses only walk up, never down!
止损只能往上走,绝不往下走。这一行是本篇标题的全部含义。人做不到这件事——价格跌到止损位,你的第一反应是「再给它一点空间」,于是把止损往下挪一挪,然后再挪一挪。系统做得到,因为它压根没提供往下挪的入口。把止损从心理问题变成数学问题,实质就是把「要不要再等等」这个选项从流程里删掉。(止损位随着盈利往上抬的那套机制是另一个话题,见 追踪止损。)
系统层之二:谁来判定「碰到了」
价格有了,接下来是判定。这一步在回测和实盘里是两套完全不同的东西,而多数人从来没意识到它们不同。
回测里,判定的依据是这根 K 线的最低价(做多)或最高价(做空)——只要最低价穿过了止损价,就算触发。听起来天经地义,可一根日 K 线只给你四个数:开、高、低、收。这一天里价格是先跌到最低再拉回来,还是一路阴跌,K 线不告诉你。回测判的不是「你的止损被触发了」,而是「这一天的价格范围覆盖了你的止损」。 这两句话在多数时候等价,在关键的少数时候不等价。
更棘手的是同一根 K 线里止损和止盈都被覆盖到的情况:最低价穿了止损,最高价也够到了止盈目标。到底哪个先发生?K 线里没有这个信息,引擎只能预先规定一个顺序。freqtrade 把这个顺序直接写在注释里:
# Sequence:
# Exit-signal
# Stoploss
# ROI
# Trailing stoploss
这是一个假设,不是一个事实。它排在这里,你的回测结果就长这样;换个排法,同一段历史能跑出不同的成绩。而它是被写死在源码注释里的,不是你在配置文件里能看到的东西——你的回测成绩里,藏着一批你从没同意过、也从没被告知过的默认约定。
实盘里,判定分成两种形态,差别很大。
一种是机器人自己盯着:拉到最新价,跟止损价比,够了就发单。止损这时候是一个只存在于你自己电脑里的数字——你的程序崩了、网断了、机器休眠了,这个数字就不存在了。
另一种是把止损单交给交易所(在 freqtrade 里叫 stoploss_on_exchange):开仓成交后立刻在交易所挂一张条件单。上游文档给出的理由很实在——执行完全发生在交易所内部,没有网络往返的风险。 代价是这张挂单需要定期检查和更新(更新的间隔是可配置项,当前默认为 60 秒),因为查询太频繁会被交易所限流。
这两种形态的区别,在市场平静时几乎看不出来,在暴跌那一天就是全部区别。
系统层之三:那笔订单,成交在哪儿
判定完了,要真去市场上出掉。这一层是落差最大的地方。
回测的默认假设:正好在止损价成交
freqtrade 的回测模块在开头就把交易所止损单强制关掉了,注释写得很坦白:回测里反正假定了一个「完美的」止损成交。翻成人话——回测默认你正好按止损价出掉了,一分不多一分不少。
这是一个对你有利的假设,而所有对你有利的默认假设都值得多看两眼。
跳空:回测里唯一诚实的那个分支
好在缺口这件事被认真处理了。做多的时候,如果算出来的止损价比这根 K 线的最高价还高,说明整根 K 线全都待在止损价下方——价格是跳过去的,你根本没机会在止损价成交:
if stoploss_value > row[HIGH_IDX]:
return row[OPEN_IDX]
成交价改用开盘价。开头那个「设 3% 亏 8%」的例子,就是这一行代码在描述的世界。止损保护不了缺口,因为缺口的定义就是「中间那段价格从没被交易过」。 任何声称能控制单笔最大亏损的说法,都要先在这里打个折扣。
滑点:成交价上的那一层附加费
即便没有缺口,成交价也不会正好等于你看到的价格。Vibe-Trading 的引擎在每一笔成交价上乘一个带方向的系数——买入时价格往上偏一点,卖出时往下偏一点(A 股引擎里这个比例是可配置的,默认值很小,因为最小价位本身就细)。方向永远是对你不利的那一侧,这才是滑点的本质。止损单尤其吃这一口,因为触发止损的时刻,通常正是市场往那个方向跑得最急的时刻。这一层单独展开在 滑点。
订单类型:出得快,还是出得准
上游文档把两种止损单翻译成大白话,值得抄下来:
- 止损市价单:触发了,什么价都行,先让我出去。
- 止损限价单:触发了,在止损价下方一点挂个限价单。运气好只多亏一点点;运气不好价格一路往下走,单子挂在那儿没人接,最后亏的可能远超你的设想。
文档给的倾向很明确:能用市价就用市价,因为止损存在的意义就是在市场崩的时候把你弄出来,而那种时刻正是限价单最容易挂空的时刻。这个取舍属于订单类型的通用话题,限价单与市价单 里讲得更全。
顺带一个反直觉的边界:止损设得过宽反而可能挂不上去,因为交易所对条件单的价格范围有限制。这种情况下机器人会退回到一个「紧急退出」的市价单。你的止损参数不只是个风控数字,它还得是交易所肯接受的一张单子。
涨跌停:想止也止不掉
A 股这类有涨跌幅限制的市场里还有一层更硬的墙。Vibe-Trading 的引擎在成交前会先算出当天的合法价格带,再拿「这一笔会成交在哪个价」去比对;碰到带子边缘,这笔成交直接被拒绝。
跌停封死那天,你的止损条件早就满足了,订单也发出去了,就是没人跟你成交。止损是一个你单方面的意愿,成交需要对手方同意。
这个价格带的算法本身还藏着一个教训:注释里记着,早先的版本用当天的收盘价去推算价格带,那是前视——撮合发生在开盘那一刻,当天的收盘价那时还不存在。现在改成了严格属于历史的前收盘价。这类「用了当时还拿不到的信息」的问题,统一在 前视偏差 里讲。
把波动率变成止损位:系统里的那道换算
回到最开始那个分工——止损距离应该由波动率决定。这件事在系统里落地时,会撞上一个接口层面的小别扭,而这个别扭本身很能说明问题。
动态止损的回调函数每根 K 线都会被调用一次,它要返回的是「相对当前价格的一个百分比」。可你心里想的是一个绝对价:当前价往下 2 个 ATR 的那个位置。两者之间需要一次换算,上游提供了一个 helper,核心就一行:
stoploss = 1 - (stop_rate / current_rate)
别被这行简单的除法骗过去,它背后有件事很重要:同一个绝对止损价,随着价格变动,对应的百分比是一直在变的。 所以在系统里,止损位不是开仓时设一次就锁死的常量,而是每根 K 线重新算一遍的结果。ATR 变大,距离自动拉宽;ATR 收缩,距离自动收紧。你配置的是「几倍 ATR」这个规则,不是某一个价格。
至于几倍合适——这里不给数。不同品种、不同周期、不同持仓时长的答案完全不同,任何一个被写成「就用 X 倍」的答案都值得怀疑。这里唯一要你记住的是机制:止损位是从一个可测量的市场属性推出来的,而不是从你的心理承受力推出来的。 心理承受力那部分,应该体现在你买多少上。
「设了止损」不等于「最大亏损被锁定」。 缺口跳过你的止损价、跌停封死没有对手方、限价单挂在那儿没成交、程序崩了没发出单——这几种情况下你的实际亏损都会超出设定值,而且它们往往同时出现在同一天。止损降低的是大多数时候的单笔亏损,它压不住尾部。真正压尾部的是仓位大小,那是另一套东西,见 风险管理卷R。
失效边界与常见误用
用回测里的止损效果推断实盘。 回测默认完美成交,实盘要过滑点、订单类型、涨跌停三道关。一套严重依赖止损的方法,回测成绩里有相当一部分是这个默认假设送的。
止损设得太近。 落在日常波动范围内的止损,会把你反复扫出来。每次亏得不多,加起来能把一整年的盈利磨没,而且你每次都是「方向对了但没拿住」。
止损设得太远。 单笔亏损大到需要好几笔盈利才能填平。亏损和盈利在数学上不对称——亏掉一半要涨一倍才回本,这层不对称性 亏损不对称的数学 讲得比这里细。远近之间不存在「最优解」,只存在权衡。
在回测里反复调止损参数,直到曲线好看。 止损也是参数,也会过拟合。一个只在某个特定倍数上才成立的方法,多半什么都不成立。
把止损当成亏了之后才想起来的动作。 系统里止损价是开仓那一刻就写进这笔交易记录的字段。人的版本应该一样:下单前就得有那个数,不是套住以后再决定。
指望止损替你判断对错。 止损只回答「这一笔到此为止」,它从不回答「这个判断是不是错的」。被扫出来之后价格立刻涨回去,这是止损系统的正常输出,不是它的故障。
小结
- 一个止损位在系统里被拆成三样东西:一个不断重算的价格、一条判定是否触发的规则、一笔真去市场成交的订单。三样各自都会跑偏。
- 价格这一层:从「愿意亏多少」反推绝对价,带杠杆时要除以杠杆(你设的是损失额度不是价格幅度),取整方向一律偏向更早触发,并且只许往上走不许往下挪。
- 规则这一层:回测拿 K 线的极值判触发,一根 K 线里止损和止盈同时被覆盖时,靠一个预先规定的顺序决定谁先;实盘则分「程序自己盯」和「挂在交易所」两种形态,平时看不出差别,崩盘那天就是全部差别。
- 订单这一层:回测默认按止损价完美成交;现实里缺口让你按开盘价出、滑点让成交价再偏一点、跌停封死让你压根出不掉、限价止损单可能挂空。
- 止损位应该从波动率推出来,而且是每根 K 线重算的规则,不是开仓时拍的一个常量。距离由波动率定,仓位由你能承受的亏损定,两件事别混。
- 止损降低的是大多数时候的单笔亏损,它压不住尾部风险。
一句话记住这篇:你能决定什么时候喊停,但停在哪儿是市场说了算——所以真正保护你的,从来不是那个止损价,是你一开始买了多少。
本文所引源码与文档均来自上述快照版本,是阅读代码得出的机制说明,不是运行结果;具体行为请以你手上那个版本为准。回 量化与 AI 投研概念库 看其它概念各自在说什么;本站内容为投资教育,不构成任何投资建议,边界见 免责声明。
本文为投资教育整理,关键数据与结论请结合下列权威来源验证。
- freqtrade 源码快照 b3404c9(2026-08-18):freqtrade/optimize/backtesting.py、freqtrade/strategy/interface.py、freqtrade/persistence/trade_model.py、freqtrade/strategy/strategy_helper.py、docs/stoploss.md、docs/strategy-callbacks.md ↗
- Vibe-Trading 源码快照 3a752d5(2026-08-04):agent/backtest/engines/base.py、agent/backtest/engines/china_a.py ↗