第 3 章 · 04 Heikin-Ashi:平均 K 线与统计大本营 本节摘要:本节是本章重头戏。Heikin-Ashi(日文"平均 K 线")用四个公式对 OHLC 做变换,滤掉噪音让动量更连贯、反转点更清晰,适合震荡市。但本节真正价值不在策略本身,而在它附带的 stats() 模块——这是全项目唯一完整的统计大本营,算 CAGR/Sharpe(S&P500 benchmark)/MDD/Calmar/Omega(student-T 分布 + 数值积分)/Sortino(仅负收益标准差),后续 7 个脚本都链接过来引用。我们花一半篇幅讲 HA 变换,另一半篇幅讲透 stats()。 涉及源码:原项目 (约 432 行,本章最长),数据来自 yfinance(NVDA)。
本节摘要:本节是本章重头戏。Heikin-Ashi(日文"平均 K 线")用四个公式对 OHLC 做变换,滤掉噪音让动量更连贯、反转点更清晰,适合震荡市。但本节真正价值不在策略本身,而在它附带的 stats() 模块——这是全项目唯一完整的统计大本营,算 CAGR/Sharpe(S&P500 benchmark)/MDD/Calmar/Omega(student-T 分布 + 数值积分)/Sortino(仅负收益标准差),后续 7 个脚本都链接过来引用。我们花一半篇幅讲 HA 变换,另一半篇幅讲透 stats()。
涉及源码:原项目
Heikin-Ashi backtest.py(约 432 行,本章最长),数据来自 yfinance(NVDA)。
⚠️ 注意:本节是全项目的"统计枢纽"。后续章节(配对交易、期权、石油货币等)评估策略时都会引用这里的 stats 函数,务必把 Omega/Sortino 的 student-T 推导吃透。
阅读完本节,你应当能够:
stls=3 的作用。mdd() 函数遍历求最大回撤的逻辑。Heikin-Ashi(简称 HA)是日本发明的 K 线变体,日文"平均 K 线"。它对原始 OHLC 做四个公式变换,滤除噪音,让动量更连贯、反转点更清晰。四个公式:
HA_close[n] = (Open[n] + Close[n] + High[n] + Low[n]) / 4 HA_open[n] = (HA_open[n-1] + HA_close[n-1]) / 2 # 递归! HA_high[n] = max(HA_open[n], HA_close[n], High[n], Low[n]) HA_low[n] = min(HA_open[n], HA_close[n], High[n], Low[n])
优点:动量连贯、反转点清晰(连续阳线后第一根阴线就是反转信号)、适合趋势/震荡市。缺点:响应慢——HA_close 是 OHLC 的平均天然滞后;HA_open 递归进一步延迟信号。作者警告:必须配止损防闪崩,HA 滞后在突发事件前毫无防御。
作者在 signal_generation 里用 4 个条件识别 HA 上的多头信号,这套条件"kinda look like marubozu candles"(光头光脚大阳线):
1. HA_open[n] > HA_close[n] # HA 阴线 2. HA_open[n] == HA_high[n] # 开盘=最高(无上影) 3. |HA_open[n]-HA_close[n]| > |前一根实体| # 实体变大 4. HA_open[n-1] > HA_close[n-1] # 前一根也是阴线
注意:HA_open > HA_close 是阴线,看起来矛盾——阴线怎么是多?这是 HA 的特性:HA K 线颜色和原始 K 线不同步时,往往是反转信号。当 HA 出现阴线且符合上述 4 条,作者解读为"原始价格在走强,HA 即将翻阳",所以提前做多。HA 玩法精髓——HA 反转先于原始 K 线反转。
本节最大价值是 stats()。6 个核心比率:4 个常规(CAGR/Sharpe/MDD/Calmar)+ 2 个进阶(Omega/Sortino)。
为什么 Omega/Sortino 不用正态分布? 作者原话:"normal distribution doesnt explain the fat tail of returns"。金融市场收益率有胖尾(fat tail)——极端事件(崩盘、暴涨)的频率远高于正态分布预测(正态 3σ 概率约 0.3%,金融市场几乎每年都有)。student-T 多一个自由度参数,可刻画胖尾。
为什么不用经验分布? 作者:"to make our life easier, i do not use empirical distribution"——经验分布(样本分位数)精确但解析性差,积分困难。student-T 有解析 CDF/PDF 可喂给 scipy.integrate.quad。这是"近似但可算"对"精确但难算"的工程权衡。
Omega 比率:Sharpe 的推广,用积分代替均值/标准差。
Omega = ∫[τ, +∞] (1 - F(r)) dr / ∫[-∞, τ] F(r) dr
F(r) 是 CDF,τ 是阈值(本策略用 benchmark 收益率)。分子是"超过阈值的概率质量"(收益期望),分母是"低于阈值"(损失期望)。Omega > 1 表示收益期望大于损失。
y=scipy.integrate.quad(lambda g:1-scipy.stats.t.cdf(g,degree_of_freedom),risk_free,maximum) x=scipy.integrate.quad(lambda g:scipy.stats.t.cdf(g,degree_of_freedom),minimum,risk_free) z=(y[0])/(x[0])
1-cdf(g) 是生存函数。degree_of_freedom 用样本数(严格应拟合,这里简化)。
Sortino 比率:Sharpe 的另一变体,只用负收益的标准差(下行偏差):
v=np.sqrt(np.abs(scipy.integrate.quad( lambda g:((risk_free-g)**2)*scipy.stats.t.pdf(g,degree_of_freedom), risk_free,minimum))) s=(growth_rate-risk_free)/v[0]
积分里 (risk_free-g)**2 只在 g < risk_free 时累计,正是"下行偏差"。t.pdf 是 student-T 概率密度。
MDD 最大回撤:对每个时点算当前值相对前期最高点的回撤比例,取最负值:
MDD = min over i of ( series[i] / max(series[:i]) - 1 )
本策略 signals 取值丰富:0 不动、+1 加多一张、-1 出场信号(实际写 -cumsum 一次清空)。cumsum 列是当前多头张数,被 stls=3 限制最大值。出场时发 -cumsum[n-1] 一次性清空所有持仓,这是和前几节"逐张平仓"不同的设计。
Heikin-Ashi backtest.py:41-66:
def heikin_ashi(data): df=data.copy() df.reset_index(inplace=True) # HA close:OHLC 简单平均 df['HA close']=(df['Open']+df['Close']+df['High']+df['Low'])/4 # 初始化 HA open,首日 = 当日 Open df['HA open']=float(0) df['HA open'][0]=df['Open'][0] # HA open 递归:每日 = (前日 HA open + 前日 HA close) / 2 for n in range(1,len(df)): df.at[n,'HA open']=(df['HA open'][n-1]+df['HA close'][n-1])/2 # HA high/low:四值取极值 temp=pd.concat([df['HA open'],df['HA close'],df['Low'],df['High']],axis=1) df['HA high']=temp.apply(max,axis=1) df['HA low']=temp.apply(min,axis=1) return df
注意:reset_index 把 Date 转成列方便后续 plot;HA open 必须用 for 循环(递归依赖前一项,pandas 无法纯向量化,大样本下是性能瓶颈);pd.concat + apply(max/min, axis=1) 比 4 次 np.maximum 链式调用更简洁,稍慢。
Heikin-Ashi backtest.py:79-123:
for n in range(1,len(data)): # 多头触发:HA 阴线 + 无上影 + 实体变大 + 前根也是阴线 if (data['HA open'][n]>data['HA close'][n] and data['HA open'][n]==data['HA high'][n] and np.abs(data['HA open'][n]-data['HA close'][n])> np.abs(data['HA open'][n-1]-data['HA close'][n-1]) and data['HA open'][n-1]>data['HA close'][n-1]): data.at[n,'signals']=1 data['cumsum']=data['signals'].cumsum() if data['cumsum'][n]>stls: # 加仓上限 data.at[n,'signals']=0 # 出场:HA 阳线 + 无下影 + 前根也是阳线 elif (data['HA open'][n]<data['HA close'][n] and data['HA open'][n]==data['HA low'][n] and data['HA open'][n-1]<data['HA close'][n-1]): data.at[n,'signals']=-1 data['cumsum']=data['signals'].cumsum() if data['cumsum'][n]>0: data.at[n,'signals']=-1*(data['cumsum'][n-1]) # 一次性清空 if data['cumsum'][n]<0: data.at[n,'signals']=0
关键设计:stls=3 限仓——作者注释:"without certain constraints, you will long indefinite times as long as the market condition triggers the signal. in a whipsaw condition, it is suicidal"。震荡市里同一信号会反复触发,不限仓会被打到爆仓。出场一次性清空 signals=-cumsum[n-1]——不是逐张平,而是直接发 -cumsum[n-1] 把所有仓位一次平掉,这是和前几节最大的不同。if cumsum[n]<0: signals=0——没多头仓时出场信号作废。
Heikin-Ashi backtest.py:223-237。初始资金 10000,每张 100 股。holdings = cumsum × Close × positions(持仓市值);cash = capital0 − 累计买入支出;total asset = 两者和;return = total asset 的 pct_change()。后续 stats 全靠 return 和 total asset 两列。


Heikin-Ashi backtest.py:329-392。全项目唯一完整的统计模块。核心 6 个比率 + 5 个交易统计:
def stats(portfolio,trading_signals,stdate,eddate,capital0=10000): stats=pd.DataFrame([0]) maximum=np.max(portfolio['return']) minimum=np.min(portfolio['return']) # 几何平均增长率(非算术) growth_rate=(float(portfolio['total asset'].iloc[-1]/capital0))**(1/len(trading_signals))-1 std=float(np.sqrt((((portfolio['return']-growth_rate)**2).sum())/len(trading_signals))) # S&P500 benchmark(几何平均) benchmark=yf.download('^GSPC',start=stdate,end=eddate) return_of_benchmark=float(benchmark['Close'].iloc[-1]/benchmark['Open'].iloc[0]-1) rate_of_benchmark=(return_of_benchmark+1)**(1/len(trading_signals))-1 # 6 个比率 stats['CAGR']=growth_rate stats['portfolio return']=portfolio['total asset'].iloc[-1]/capital0-1 stats['benchmark return']=return_of_benchmark stats['sharpe ratio']=(growth_rate-rate_of_benchmark)/std stats['maximum drawdown']=mdd(portfolio['total asset']) stats['calmar ratio']=growth_rate/stats['maximum drawdown'] stats['omega ratio']=omega(rate_of_benchmark,len(trading_signals),maximum,minimum) stats['sortino ratio']=sortino(rate_of_benchmark,len(trading_signals),growth_rate,minimum) # 交易统计:longs / shorts / trades / total length / avg length / profit per trade
11 个指标分 3 组:
| 组 | 指标 | 含义 |
|---|---|---|
| 收益 | CAGR / portfolio return / benchmark return | 几何平均增长率 / 总收益 / S&P500 收益 |
| 风险调整 | Sharpe / MDD / Calmar / Omega / Sortino | 经典 + 进阶 |
| 交易 | longs / shorts / trades / length / profit per trade | 次数与时长 |
注意 Sharpe 的"无风险利率"用 S&P500 的几何平均增长率(rate_of_benchmark)——这是相对 Sharpe,衡量"相对大盘的超额收益",而非"相对国债"。
def mdd(series): minimum=0 for i in range(1,len(series)): if minimum>(series[i]/max(series[:i])-1): minimum=(series[i]/max(series[:i])-1) return minimum
对每个 i,series[i]/max(series[:i])-1 是"当前值相对前期最高点的跌幅",取最负值。max(series[:i]) 是 O(i),整体 O(n²),回测样本小可接受。
🎯 反直觉发现:Heikin-Ashi 多头信号的第一个条件是
HA_open > HA_close(HA 阴线)——阴线怎么是多? 答案在 HA 的递归特性:HA 的"颜色"和原始 K 线不同步。当原始价格持续走强但 HA 仍是阴线时(因为 HA_open 递归偏高),意味着 HA 即将翻阳——HA 的滞后反而成了"提前预警"。这就是为什么作者注释说"kinda look like marubozu"——这套条件识别的不是已发生的阳线,而是"即将翻阳的前夜"。这种"用滞后的反向信号做正向入场"的玩法,是 HA 区别于普通 K 线的核心价值。
stls 限仓防震荡:同一信号在震荡市反复触发,不限仓会被打爆。3 是 NVDA 回测的经验值。-cumsum[n-1] 一行平掉所有持仓,代码紧凑但风险高(大单子一次性出入有滑点)。scipy.integrate.quad;经验分布精确但难积分,作者选"近似但可算"。degree_of_freedom 用样本数:严格应拟合自由度,作者简化为 len(trading_signals),样本越大越接近正态——保守选择。mdd() 是 O(n²):max(series[:i]) 每次重新算,小样本无妨,大样本应该用 np.maximum.accumulate 优化为 O(n)。stls=3 限仓,出场一次性 -cumsum 清空。scipy.integrate.quad 数值积分,作者原话"to make our life easier"。下一章我们从"形态"转向"统计检验"——配对交易与协整。Heikin-Ashi 的 stats 模块会在那里反复用到,记得回来查。