第 3 章 · 03 Shooting Star:流星线 K 线形态 本节摘要:本节从"时段结构"转向"K 线形态"——Shooting Star 流星线。前两节关注"何时"入场(开盘突破),本节关注"何种 K 线"入场。流星线是上升趋势末尾出现的"长上影、几乎无下影、小实体"的 K 线,预示见顶转跌。作者严格按 Investopedia 的定义,用 8 个条件 到 刻画它,8 个条件相乘得信号(全 1 才是流星线)。我们重点讲清楚这 8 个条件的金融含义、为什么用乘法而非加法、以及退出策略(±5% 或 7 工作日)。 涉及源码:原项目 (约 269 行),数据来自 yfinance(Vodafone VOD.L)。
本节摘要:本节从"时段结构"转向"K 线形态"——Shooting Star 流星线。前两节关注"何时"入场(开盘突破),本节关注"何种 K 线"入场。流星线是上升趋势末尾出现的"长上影、几乎无下影、小实体"的 K 线,预示见顶转跌。作者严格按 Investopedia 的定义,用 8 个条件
condition1到condition8刻画它,8 个条件相乘得信号(全 1 才是流星线)。我们重点讲清楚这 8 个条件的金融含义、为什么用乘法而非加法、以及退出策略(±5% 或 7 工作日)。
涉及源码:原项目
Shooting Star backtest.py(约 269 行),数据来自 yfinance(Vodafone VOD.L)。
⚠️ 注意:本节策略是纯做空形态——流星线只在上升趋势末尾出现,信号永远为 -1。看代码时不要期待看到做多信号。
阅读完本节,你应当能够:
流星线(Shooting Star)是反转 K 线,出现在上升趋势末尾,预示见顶转跌。形态特征:
│ ←── 长上影(upper shadow),至少实体 2 倍 │ ┃─┃ ←── 小实体(small body),开盘≈收盘 │ ┴ ←── 几乎无下影(lower shadow)
四个量化特征:① Open >= Close(阴线/红柱);② 下影极小(收盘接近最低);③ 实体小(开盘与收盘之差小于均值);④ 上影长(最高到开盘至少是实体高度的 2 倍)。
金融含义:多头在高位尝试上攻推到最高价,被空头完全打回,收盘接近开盘(且接近最低价)。这是高位抛压的典型信号——多头推不上去,空头在高位接管。
hammer(锤子线)是流星线的垂直镜像——长下影、几乎无上影、小实体,出现在下降趋势末尾,预示见底转涨。作者用押韵口诀记忆:
if u see Thor (with hammer), price shall soar
if u see star (shooting star), price shall fall
看到雷神托尔的锤子,价格要飞;看到流星,价格要跌。简单粗暴。
K 线形态家族容易混淆,作者隐式梳理:
| 形态 | 方向 | 出现位置 | 含义 |
|---|---|---|---|
| Shooting Star 流星线 | 看跌 | 上升末尾 | 见顶转跌 |
| Hammer 锤子线 | 看涨 | 下降末尾 | 见底转涨 |
| Inverted Hammer 倒锤子 | 看涨 | 下降末尾 | 见底转涨(上影长) |
| Hanging Man 上吊线 | 看跌 | 上升末尾 | 见顶转跌(下影长) |
作者调侃:"没有 inverted shooting star,它邪恶的名字叫 hanging man 上吊线"——把流星线垂直翻转得到的不是"倒流星线",而是上吊线(形态恰似上吊的人形)。他还引用 Paramore 乐队主唱 Hayley Williams 的歌词"airplanes in the night sky are like shooting stars",暗示流星线在 K 线序列里是稀有事件(条件苛刻,8 条全中才出现)。
流星线是做空信号,退出有两个触发条件(任一满足即平仓):
abs(data['Close'].loc[ind]/entry_pos-1) > 0.05。跌 5% 止盈,涨 5% 止损。counter >= holding_period。形态没走出来强制离场。这两条是 OR 关系——哪个先到按哪个。典型的"形态策略"退出:给形态一个时间窗,在窗内要么兑现、要么认错。
本策略 signals 取值简化为 -1(入场做空)、+1(平仓)、0(不动)。positions 列是 signals.cumsum()——所以持仓期 positions 恒为 -1(做空 1 张),平仓后回到 0。
Shooting Star backtest.py:27-65。这个函数把 Investopedia 的 8 条定义直接翻译成 8 个 np.where,每条返回 0/1:
def shooting_star(data,lower_bound,body_size): df=data.copy() # 条件1:Open>=Close,红柱(阴线) df['condition1']=np.where(df['Open']>=df['Close'],1,0) # 条件2:下影极小 (Close-Low) < lower_bound * |Close-Open| df['condition2']=np.where( (df['Close']-df['Low'])<lower_bound*abs(df['Close']-df['Open']),1,0) # 条件3:实体小 |Open-Close| < 均值 * body_size df['condition3']=np.where(abs(df['Open']-df['Close'])< abs(np.mean(df['Open']-df['Close']))*body_size,1,0) # 条件4:上影至少实体 2 倍 (High-Open) >= 2*(Open-Close) df['condition4']=np.where( (df['High']-df['Open'])>=2*(df['Open']-df['Close']),1,0) # 条件5/6:前 2 日收盘上行(趋势确认) df['condition5']=np.where(df['Close']>=df['Close'].shift(1),1,0) df['condition6']=np.where(df['Close'].shift(1)>=df['Close'].shift(2),1,0) # 条件7:次日高点不超过流星线高点(确认反转) df['condition7']=np.where(df['High'].shift(-1)<=df['High'],1,0) # 条件8:次日收盘不超过流星线收盘 df['condition8']=np.where(df['Close'].shift(-1)<=df['Close'],1,0) return df
8 个条件可以分 4 类:
| 类别 | 条件 | 含义 |
|---|---|---|
| 形态 | 1, 2, 3, 4 | 阴线 / 下影小 / 实体小 / 上影长 |
| 趋势确认 | 5, 6 | 前 2 日收盘持续上行(必须是上涨趋势末尾) |
| 反转确认 | 7, 8 | 次日不再创新高、收盘不超过流星线 |
注意条件 7、8 用 shift(-1)——这是未来函数!流星线信号只能在"次日收盘后"才能确认。这看起来像偷看未来,但其实合理:流星线本身只是"候选信号",真正的反转必须由次日走势确认。实盘中,你在 T+1 收盘后才能根据 T 日是否是流星线(且 T+1 是否确认)下单,所以实际入场是 T+2 开盘。本回测简化处理,信号标记在 T 日,但回测者要意识到这个 T+1 延迟。
Shooting Star backtest.py:73-120。
def signal_generation(df,method,lower_bound=0.2,body_size=0.5, stop_threshold=0.05,holding_period=7): data=method(df,lower_bound,body_size) # 8 条件相乘:全 1 才是流星线 data['signals']=data['condition1']*data['condition2']*data['condition3']* \ data['condition4']*data['condition5']*data['condition6']* \ data['condition7']*data['condition8'] # 流星线是做空信号,取负 data['signals']=-data['signals']
为什么用乘法而不是加法?因为 8 个条件是"与"(AND)关系——必须全部满足才算流星线。乘法的性质:任何一项为 0,乘积就为 0;只有 8 项全为 1,乘积才是 1。这正好实现"全中才发信号"。如果用加法,某根 K 线可能满足 7 个条件(差一个),加法会得 7,容易被误读为"强信号"——但流星线的定义是严苛的,7/8 不算流星线。
随后 data['signals']=-data['signals'],把 +1(流星线成立)翻成 -1(做空信号)。这就是为什么前面说本策略 signals 永远非正。
Shooting Star backtest.py:94-115。这是本策略最特别的部分——不是逐 K 线判条件,而是每个做空信号向后 while 循环找平仓点:
idxlist=data[data['signals']==-1].index for ind in idxlist: entry_pos=data['Close'].loc[ind] # 入场价 stop=False counter=0 while not stop: ind+=1 counter+=1 # ±5% 触发 if abs(data['Close'].loc[ind]/entry_pos-1)>stop_threshold: stop=True data['signals'].loc[ind]=1 # 7 工作日触发 if counter>=holding_period: stop=True data['signals'].loc[ind]=1
这种写法和前两节"逐 K 线扫描"完全不同——它以入场信号为锚,向前搜索退出点。逻辑直观:每发一个做空,就独立找它的退出。优点是退出逻辑高度内聚,缺点是若有重叠持仓(一个仓还没退出又来一个信号)会冲突,所以本策略实际效果是"每个流星线信号独立处理,不叠加仓位"。
Shooting Star backtest.py:131-188。matplotlib 新版移除了 candlestick,作者又不愿装 mpl_finance,于是手写:实体用 fill_between 画矩形(7 个点的 x 区间,宽度 0.6),上下影用 plot 画细黑线,流星线高亮支持 highlight='signals' + highlightcolor='y',把 signals=-1 的那根涂成黄色。1.001 和 0.999 缩放因子防止影线画进实体造成视觉重叠。

图中黄色 K 线就是被 8 条件选中的流星线,红色倒三角是做空入场,绿色三角是平仓。
🎯 反直觉发现(本节无重大反直觉,但有一个值得注意的细节):流星线的"形态"条件(1-4)是肉眼可见的,趋势确认条件(5-6)也是肉眼可见的,但反转确认条件(7-8)用了
shift(-1)——这意味着流星线信号只能在次日才能确认。这不是未来函数作弊,而是形态定义的内在要求:单根 K 线不能定义反转,反转必须由下一根走势验证。实盘使用时,你要意识到从"看到流星线"到"能下单"之间有 1 天的延迟,这段时间价格可能已经跌了一段,实际入场价会比信号 K 线收盘价差。
& 链式调用更紧凑,且 np.where 向量化效率高。shift(-1) 不是未来函数作弊:形态反转需要次日确认,这是定义的一部分。回测报告时要诚实标注 T+1 延迟。lower_bound=0.2 控下影容忍度:(Close-Low) < 0.2*|Close-Open| 意味着下影不能超过实体的 20%。调小(如 0.1)更严格,信号更少但更纯。body_size=0.5 控实体大小:|Open-Close| < 均值*0.5 意味着实体必须小于平均实体的一半。作者注释:"in practise, you may find the definition too rigid, its important to relax a bit on the body size"——实战可放宽到 0.7-0.8。fill_between + plot + 1.001/0.999 缩放,跨 matplotlib 版本兼容。shift(-1) 用于反转确认,实盘有 T+1 延迟,要诚实标注。下一节是本章重头戏——Heikin-Ashi 平均 K 线。它不仅是 OHLC 变换滤噪策略,更是全项目的统计大本营:唯一含完整 stats() 模块的脚本,CAGR/Sharpe/MDD/Calmar/Omega/Sortino 全在里面,后续 7 个脚本都链接过来引用。