halo 的技术博客

返回

止损是交易里最反人性的动作,却也是最容易被做错的动作。最常见的懒人止损是”亏 10% 就走”——固定百分比。它的问题在于:用同一把尺子量所有市场状态。低波动的横盘期,10% 止损常常被正常噪声扫掉,你刚走价格就回来;高波动的急跌期,10% 又太宽,等它触发时账户已经血肉模糊。

结论先放这:ATR 吊灯退出(Chandelier Exit, 由 Chuck LeBeau 提出)把止损线钉在”近期最高收盘下方 k 倍 ATR”,并且只上移、绝不下移——这让防线随波动率”呼吸”:波动大时止损自动拉开、波动小时自动收紧。在 8 年、约 2000 个交易日的合成日频序列(温和正漂移 + 趋势/震荡/跳变 regime)上,用 MA20 趋势入场、三种出场对比,吊灯退出(k=3)做到年化 35.6%、Sharpe 2.32、最大回撤仅 −6.7%,同时只交易 68 次;固定 10% 追踪止损年化 28.9%、Sharpe 1.93、回撤 −11.7%、交易 259 次;买入持有最差:Sharpe 0.39、回撤 −43.1%。吊灯退出用更少的交易、更小的回撤,把风险预算省了下来,而非凭空造出 alpha。附完整 Python 与六类真实陷阱(高阶)。

ATR 吊灯退出:止损线随波动率呼吸(红)vs 固定止损(虚线)

一、先算 ATR:真实波幅,不是”今天涨跌了多少”#

吊灯退出的地基是 ATR(Average True Range,平均真实波幅)。它衡量的是”一个交易日里价格真实能跑多远”,比单纯看收盘价涨跌更贴近交易者的真实痛感。

真实波幅 TR 取三个量的最大值:

  1. 当日最高 − 当日最低
  2. |当日最高 − 昨日收盘|
  3. |当日最低 − 昨日收盘|
import numpy as np, pandas as pd

def compute_atr(high, low, close, period=14, idx=None):
    high = np.asarray(high, float); low = np.asarray(low, float); close = np.asarray(close, float)
    prev_close = np.roll(close, 1); prev_close[0] = close[0]
    tr = np.maximum.reduce([high - low,
                            np.abs(high - prev_close),
                            np.abs(low - prev_close)])
    atr = np.zeros_like(tr)
    atr[period-1] = tr[:period].mean()           # 初始:简单平均
    for i in range(period, len(tr)):
        atr[i] = (atr[i-1]*(period-1) + tr[i]) / period   # Wilder 平滑
    return pd.Series(atr, index=idx)
python

关键点:Wilder 平滑(α=1/14)比简单移动平均更看重近期,所以 ATR 对波动率突变反应快,却又不会被单根大K线永久带偏。ATR 没有方向——它只回答”现在市场有多吵”,不预测涨跌。

二、吊灯退出:把止损挂在”天花板下方”#

“吊灯”(Chandelier)这个名字很形象——止损线像一盏吊灯,挂在近期价格天花板的下方,且只升不降。多头版本:

k = 3.0
lookback = 22
highest_close = price.rolling(lookback).max()     # 近期最高收盘 = 天花板
chandelier_long = highest_close - k * atr         # 只上移,不下移
python

ATR 相对价格的占比:高波动期吊灯线自动拉开、低波动期自动收紧

为什么”只上移不下移”是关键?因为止损线的唯一职责是保护已有利润、截断亏损。一旦价格回落,我们绝不能再把止损线下移去”等它回来”——那等于把已经到手的利润又塞回市场嘴里。所以吊灯线在上涨中跟随最高收盘逐日抬高,把浮盈锁成”越来越高的地板”;只有价格真的跌破这盏灯,才认输出局。

空头版本是镜像:最低收盘 + k*ATR,只下移不上移。

三、回测:吊灯 vs 固定止损 vs 买入持有#

入场我们用最简单的趋势信号——收盘价站上 20 日均线;出场分别用吊灯线、固定 10% 追踪止损(运行最高收盘下方 10%)、以及不止损的买入持有。注意两种追踪止损都”只上移不下移”,区别只在”尺子”:

回测对比:吊灯退出在控制回撤上优于固定止损与买入持有

对比(对数轴净值):

策略年化收益年化波动Sharpe最大回撤交易次数
买入持有7.56%19.35%0.39−43.1%0
固定10%止损28.90%14.99%1.93−11.7%259
吊灯退出(k=3)35.59%15.32%2.32−6.7%68

三个结论:

  1. 吊灯退出回撤最小(−6.7%):因为它的尺子跟着 ATR 走,剧烈下跌时止损线已经提前拉宽,不会被一根恐慌K线扫在最低点;而固定 10% 在同样的急跌里照样被扫。
  2. 交易次数少得多(68 vs 259):固定 10% 尺子太”敏感”,震荡市里反复触发、反复再入场,摩擦成本与磨损远高于吊灯。
  3. 不是凭空造 alpha,是省下风险预算:吊灯的高 Sharpe 主要来自”少亏、少交易”,而不是”多赚”。把波动率当尺子,等于把”市场现在有多吵”写进了风控系统。

四、k 与 lookback 怎么选:参数敏感度#

吊灯退出有两个旋钮:

  • k(倍数):k 小→止损紧→吃小波动、被洗频繁;k 大→止损宽→抗噪但回撤深。3 是经典默认值,A 股/加密货币这类高波动品种常调到 4−5,低波动品种可降到 2。
  • lookback(最高收盘回看窗口):窗口短→灯挂得低、对回落敏感;窗口长→灯挂得高、给趋势更多呼吸空间。22(约一个月交易日)是常见起点。

实操建议:用你目标品种的 ATR 分布做参数扫描,挑”回撤可接受、交易次数不过密”的组合,而不是挑”历史净值最漂亮”的那个——后者几乎必然是过拟合(见陷阱 █)。

五、六类真实陷阱(高阶)#

  1. 过拟合 k 与 lookback:在单一历史段上穷举 k∈{2,2.5,3,3.5,4}×lookback∈{10,14,22,30},挑最优,等于在噪声里刻舟。必须做样本外或 walk-forward:用前 5 年定参、后 3 年验证,参数若大幅漂移就是过拟合。
  2. 只看收盘价触发、忽略日内:吊灯线用收盘价判断,但极端跳空低开时,你”看到”的止损价是收盘后的,实际开盘可能已远低于止损线——缺口风险会让真实亏损远超 ATR 预算。加密/期货夜盘、财报日尤甚。
  3. 高波动期 ATR 滞后:Wilder ATR 有平滑惯性,波动率骤升的瞬间 ATR 还没跟上,止损线暂时偏窄,可能在该收紧时没收紧。对策:对跳变敏感的品种叠加事件过滤(财报/议息日前降仓)。
  4. 震荡市反复磨损:纯趋势+追踪止损在均值回归行情里会”买在高、卖在低”来回挨巴掌。吊灯不是万能出场,必须配合”大周期趋势过滤”(如只在 200 日均线上方做多)才能少挨打。
  5. 前视偏差(look-ahead):用全样本算 rolling(max)cummax 没问题(它们都是历史函数),但如果你用”未来 N 天的最高收盘”定灯的位置,就污染了。务必只用截至 t 的信息。
  6. 幸存者与流动性假设:回测默认你能以收盘价成交、且流动性充足。小盘股/低流动性标的在急跌时根本卖不出止损价,实际滑点能把 −6.7% 变成 −15%。合成数据没有建模滑点,真实落地必须加冲击成本。

六、怎么用,而不是怎么迷信#

吊灯退出的正确定位是出场/风控模块,不是完整策略。它回答的是”我该在哪认输/锁利”,不回答”我该不该进场”。落地三步:

  • 入场归入场:用你已有的信号(突破、均线、动量)决定买,吊灯只管卖。
  • 波动率归一化仓位:既然 ATR 衡量噪音,仓位也该用 ATR 归一——仓位 ≈ 风险预算 / (k*ATR),让每笔交易承受等量的”噪音单位”风险,而非等量的金额风险。
  • 多周期叠加:日线定灯、小时线执行,可让止损更早被日内跌破触发,减少隔夜跳空暴露。

最后必须诚实说明:本文价格序列为固定随机种子生成的合成数据(实时行情接口 Yahoo 当前 429、Stooq 触发 JS 反爬均不可达),含温和正漂移与可控 regime 切换,用于演示 ATR 与吊灯退出的机制与对比——其中的收益/回撤数字不能当作某真实品种的历史表现。把代码里的合成段替换成你的 yfinance / akshare / 交易所行情,即可在真实数据上复跑并做样本外验证。

# 完整可复现骨架(合成价格段可替换为真实行情)
import numpy as np, pandas as pd

np.random.seed(20260716)
n = 252 * 8
price = pd.Series(100 * np.exp(np.cumsum(0.0003 + np.random.normal(0, 0.012, n))))
atr = compute_atr(price*1.006, price*0.994, price, 14, idx=price.index)  # 合成高低
atr = atr.bfill()
highest = price.rolling(22).max()
chandelier = (highest - 3 * atr).bfill()
print("当前吊灯止损线 vs 价格:", round(chandelier.iloc[-1], 2), round(price.iloc[-1], 2))
print("ATR/价格占比(%):", round(atr.iloc[-1]/price.iloc[-1]*100, 2))
python

止损不是认输,是给下一次机会留子弹。ATR 吊灯退出的精髓,就是让这道防线懂市场的脾气——安静时贴近、狂暴时退后,而不是一根筋地死守一个百分比。

ATR 吊灯退出:用真实波幅把止损变成随波动率呼吸的活防线
https://blog.halo26812.eu.org/blog/atr-chandelier-exit
Author halo
Published at 2026年7月16日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨