ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

希尔伯特变换与瞬时趋势线:用Python破解均线滞后难题

希尔伯特变换与瞬时趋势线:用Python破解均线滞后难题 如果你用均线跟踪趋势大概率逃不过一个体验均线拐头的时候行情往往已经跑出去一大截。这个滞后不完全是参数没调好而是移动平均线的天然属性——它本质上是在“回顾历史”数学结构里就没有“向前看”的能力。两年前我做量化指标改造的时候被这个问题卡了很久直到接触了希尔伯特变换Hilbert Transform思路才彻底打开把价格当成信号用复数域的分析工具去提取相位和频率就能得到一条比传统均线灵敏得多的瞬时趋势线Instantaneous Trendline。这篇文章我会把希尔伯特变换的原理掰开揉碎讲清楚给出完整的Python实现然后重点聊重叠因子Overlap Factor这个参数对趋势线的影响最后是一些实战中踩过坑之后总结的经验。内容适合对技术指标原理有好奇心、不想只会调现成库的Python量化玩家。1. 移动平均线的滞后困局为什么需要“瞬时”趋势线1.1 滞后的数学根源在哪里移动平均线的滞后不是玄学是数学上可以精确计算出来的。以简单移动平均线SMA为例取最近N根K线做平均本质上是一个有限冲激响应滤波器。对于一个以恒定速度上涨的线性趋势SMA在第n根K线的输出与真实价格之间有一个固定偏差约等于N1除以2再乘以单根K线的涨幅。这个公式告诉你窗口越长滞后越大趋势越陡滞后造成的误差越夸张。EMA本质上也没好到哪里去它只是把权重从矩形改成了指数衰减滞后从N/2变成了约1/alphaalpha越小越平滑也越迟钝。所以我一直觉得趋势线类指标的核心矛盾就一句话你想获得平滑度就必须承受滞后你想降低滞后就必须忍受噪声。传统均线是在这个矛盾里硬找一个折中位置而希尔伯特变换提供了第三条路——不靠拉长窗口来平滑而是通过分析信号的相位来预判趋势方向。1.2 价格和普通信号之间的类比把价格序列当成一个信号来看它其实特别像一个叠加了噪声的低频振荡器。行情上涨时这个信号的主要频率成分处在“上升半周期”行情下跌时处在“下降半周期”。如果你能实时判断当前价格信号处于这个循环周期的哪一个相位你就能比价格本身的数值更快地感知趋势转变。传统均线只能看到振幅的均值完全没有利用相位信息。这就好比看一辆行驶中的车你只盯着它后视镜里的路来判断它往哪走而不去看方向盘的角度。希尔伯特变换能做的就是把你从时域里的滞后平均值切换到复数域里的瞬时相位用“方向盘角度”来判断方向反应自然快一截。2. 希尔伯特变换的核心思想把价格拆成旋转的复数向量2.1 从实数信号到解析信号希尔伯特变换的定义看起来很吓人是一个带奇异核的卷积积分H[x(t)] (1/π) * ∫[x(τ) / (t - τ)] dτ但你用不着背这个公式。在信号处理的实际操作中理解它的频域效果就够了希尔伯特变换相当于一个90度正交移相器。对于输入信号中的每一个正频率分量它把相位向后移90度对于负频率分量相位向前移90度。对于一个余弦波cos(ωt)它的希尔伯特变换就是sin(ωt)——一个和原信号正交、振幅相同但相位落后90度的孪生信号。把原始信号x(t)作为实部把它的希尔伯特变换H[x(t)]作为虚部就构造出一个复数信号z(t) x(t) j * H[x(t)]这个复数信号叫做解析信号。为什么叫“解析”因为它的频谱只有一个方向所有负频率分量都被剔除了。在复平面上这个信号对应一个旋转的向量向量的长度代表瞬时振幅向量与实轴的夹角代表瞬时相位。这正是我们要的东西。2.2 瞬时频率和瞬时相位比价格本身更早转向有了解析信号之后有几个关键量可以立刻算出来瞬时振幅A(t) |z(t)| sqrt(x² H[x]²)瞬时相位φ(t) atan2(H[x(t)], x(t))瞬时角频率ω(t) dφ(t)/dt瞬时相位告诉你当前信号处在周期的什么位置瞬时频率告诉你这个周期的旋转速度有多快。这两种信息在实数域里是拿不出来的因为实数信号只有一个维度你看不到“角度”。举个直观的例子假设价格信号是一个完美的正弦波当正弦波从波谷开始向上爬时瞬时相位从-π/2逐渐增大到0。如果只看价格本身你要等到价格涨出一截才能判断趋势反转但如果看瞬时频率的符号它在波谷刚过的那一瞬间就已经由负转正了。希尔伯特变换趋势线之所以能比普通均线更快捕捉拐点核心优势就在这里。2.3 为什么不能直接拿Hilbert变换作用在原始价格上这里有一个很多人第一次接触时的误区直接把收盘价序列丢给scipy.signal.hilbert拿到的结果并不好用。原因很简单价格序列不是一个窄带信号它包含超低频的趋势分量、中频的行情波动、高频的市场噪声三个频段混在一起。希尔伯特变换对这种宽带信号计算出的瞬时相位会非常不稳定抖动到你根本看不出规律。所以实际的算法流程必须是先做预处理把价格平滑到接近一个可分析的窄带振荡器再做希尔伯特变换。后面讲Python实现时你会发现第一步永远是平滑这一步不是可有可无而是整个算法能不能work的前提。3. 瞬时趋势线的计算链路从解析信号到外推趋势线3.1 四步走通完整算法我最终落地在策略里的瞬时趋势线算法一共四个环节对原始价格做短窗口平滑压制高频噪声得到近似窄带信号。对平滑后的价格做希尔伯特变换得到解析信号的实部I和虚部Q。计算瞬时相位φ和瞬时角频率ω其中ω dφ/dt。利用瞬时速度构造领先趋势线Trend I gamma * (-ω * Q)。最后这个公式需要解释一下。对于解析信号z(t) A * e^(jφ(t))其实部对时间的导数是dI/dt -ω * A * sin(φ) -ω * Q。这个导数值就是价格的瞬时变化速度单位是“价格单位/根K线”。如果我们假设这个速度在当前时刻附近保持稳定那么未来gamma根K线之后的价格可以用当前价格加上gamma * velocity来估计。gamma是什么就是趋势线的“外推跨度”。gamma越大趋势线越偏向未来对近期价格变化的反应越提前gamma越小趋势线越贴近当前价格越像一条普通均线。你可能会问这和重叠因子有什么关系关系就在这里重叠因子overlap本质上控制的就是gamma的有效大小。3.2 重叠因子的角色它是趋势线的“时间透镜”重叠因子这个词在希尔伯特变换的语境里原本指的是滑动窗口分析中相邻窗口的重复覆盖比例。但在瞬时趋势线这个具体算法里我把重叠因子的角色理解为控制外推时间尺度的旋钮。当overlap为0时gamma取最小值趋势线几乎贴着平滑价格走噪声小但反应不够快。当overlap增大时gamma变大趋势线开始“看向未来”,对拐点的反应会提前但也更容易被噪声误导。这个trade-off和均线窗口的取舍本质上是一回事只不过调节的维度从“看多长的历史”变成了“看多远的未来”反应的是瞬时相位信息而不是历史均值。下面这个公式是我在实践里用的映射关系简单但有效gamma 0.5 overlap * 2.0overlap的取值范围是0到1。overlap0.3时gamma约等于1.1根K线的外推量overlap0.8时gamma约等于2.1根K线。看起来只差了1根K线但在资金曲线上放大的效果是很明显的。3.3 为什么这个趋势线能减少滞后传统SMA的输出来自前N根K线的平均它天然落后N/2根K线。而希尔伯特趋势线用了当前时刻的瞬时相位信息把价格的导数纳入计算相当于对价格做了一个带方向修正的线性外推。在行情趋势明确的时候它的输出会比SMA提前2到4根K线转向。代价是什么在横盘震荡的时候价格没有明确的瞬时频率相位在正负之间来回跳趋势线会频繁打脸。所以这个指标不能单独用一定要搭配一个震荡过滤器来确认市场处于趋势状态。这个过滤器怎么设计我在第六章详细说。4. Python完整实现趋势线指标从零到可视化4.1 准备示例数据为了能完整复现效果我写了一个生成模拟价格序列的函数。它包含三部分线性趋势分量、周期波动分量和高斯噪声基本能模拟出一段带趋势的行情。import numpy as np import matplotlib.pyplot as plt from scipy.signal import hilbert def generate_price(n600, trend_strength0.03, cycle45, noise_scale0.6, seed42): np.random.seed(seed) t np.arange(n) trend_component trend_strength * t cycle_component 2.8 * np.sin(2 * np.pi * t / cycle) noise np.random.normal(0, noise_scale, n) price 100 trend_component cycle_component noise return price用这组参数生成的序列波段的周期大约在40多根K线噪声标准差0.6趋势强度每天0.03整体上是趋势加波动混合的状态比较接近期货或者股票的真实走势形态。4.2 核心指标函数这是整个博文最核心的代码建议直接在你的策略环境里跑一遍。def hilbert_instantaneous_trendline(price, smooth5, overlap0.5): # Step 1: 预平滑 kernel np.ones(smooth) / smooth sm np.convolve(price, kernel, modesame) # 边界修正 half smooth // 2 sm[:half] price[:half] # Step 2: 希尔伯特变换 analytic hilbert(sm) I np.real(analytic) # 实部相当于平滑后的价格本身 Q np.imag(analytic) # 虚部正交分量 amp np.abs(analytic) # 瞬时振幅 # Step 3: 瞬时相位与瞬时角频率 phase np.unwrap(np.angle(analytic)) omega np.gradient(phase) # 每根K线的相位变化量单位是弧度 # Step 4: 用重叠因子确定外推步长 gamma 0.5 overlap * 2.0 velocity -omega * Q # 限制速度的异常尖峰防止趋势线被极端噪声带飞 max_vel np.nanstd(velocity) * 3 velocity np.clip(velocity, -max_vel, max_vel) trendline I gamma * velocity return trendline, phase, omega, amp每一步在干什么我拆开说明一下。平滑用的是最简单等权滑动平均窗口默认5。这个值不能太大5到8是比较舒服的范围窗口太大会把趋势信息磨掉太小又压不住噪声。hilbert(sm)是scipy对复数解析信号的一种快速实现内部也是走FFT。返回的analytic是一个复数数组real就是平滑价imag是它的希尔伯特变换。np.unwrap非常重要。np.angle返回的相位被限制在[-π, π]之间在信号越过±π边界时会产生跳变直接做梯度会产生巨大的尖峰。unwrap能够把跳变恢复成连续递增或递减的相位序列这样算出来的瞬时频率才是正确的。速度裁剪用的是3倍标准差。实测中价格偶尔会有异常插针导致瞬时速度出现几十倍的离群值如果不裁剪趋势线会被拉出一个毫无意义的尖峰。加上这一步之后趋势线的形态会稳定很多。4.3 绘图可视化price generate_price() trendline, phase, omega, amp hilbert_instantaneous_trendline(price, smooth5, overlap0.5) plt.figure(figsize(14, 6)) plt.plot(price, alpha0.5, colorgray, labelPrice) plt.plot(trendline, colordarkorange, linewidth2.0, labelInstantaneous Trendline) plt.legend() plt.title(Hilbert Transform Instantaneous Trendline (overlap0.5)) plt.grid(alpha0.3) plt.show()跑出来的图上趋势线通常会有两个肉眼可见的特征第一它的转向点确实比价格和同窗口的SMA都更靠前第二它的峰谷波动比价格小形态更干净。整体看上去像一条过滤了高频波动、同时带着一点未来信息的自适应均线。4.4 与SMA和EMA放在一起对比为了更直观地感受差距可以把三条线画在同一张图上from scipy.signal import savgol_filter sma_window 10 sma np.convolve(price, np.ones(sma_window) / sma_window, modesame) alpha_ema 0.15 ema np.zeros_like(price) ema[0] price[0] for i in range(1, len(price)): ema[i] ema[i-1] alpha_ema * (price[i] - ema[i-1]) plt.figure(figsize(15, 7)) plt.plot(price, alpha0.3, colorgray, labelPrice) plt.plot(sma, colorsteelblue, linewidth1.5, labelSMA(10)) plt.plot(ema, colorgreen, linewidth1.5, labelEMA(0.15)) plt.plot(trendline, colordarkorange, linewidth2.0, labelHT Trendline) plt.legend() plt.title(Comparison: SMA vs EMA vs HT Instantaneous Trendline) plt.grid(alpha0.3) plt.show()我个人的观察是SMA最平滑但也最滞后EMA的滞后介于之间希尔伯特趋势线在趋势行情里转向最早但在震荡行情里也最容易反复穿越价格线。所以它的定位更偏向“趋势方向雷达”而不是一条可以直接当买卖信号用的均线。5. 重叠因子实验不同参数下趋势线的行为差异5.1 不同overlap值的趋势线对比重叠因子是整个算法里最值得花时间去调的一个参数。我取overlap 0.0、0.3、0.6、0.9四档其他条件不变调用同一个函数画图overlap_values [0.0, 0.3, 0.6, 0.9] plt.figure(figsize(15, 7)) plt.plot(price, alpha0.3, colorgray, labelPrice) for ov in overlap_values: tl, _, _, _ hilbert_instantaneous_trendline(price, smooth5, overlapov) plt.plot(tl, linewidth1.5, labelfoverlap {ov}) plt.legend() plt.title(Impact of Overlap Factor on Instantaneous Trendline) plt.grid(alpha0.3) plt.show()从图上的差异来看overlap 0.0时趋势线最平缓波动最小基本就是平滑价格本身加上极小速度修正滞后相对明显。overlap 0.3时趋势线开始对价格的反转做出提前反应但出现了更多毛刺。overlap 0.6时趋势线的灵敏度和平滑度比较平衡是我个人最常用的区间。overlap 0.9时趋势线对外推最激进经常会提前好几天画出陡峭的上升或者下降斜坡在趋势行情里很爽但震荡市里频繁翻转容易让人崩溃。5.2 用命中率量化趋势线的预测质量光看图形难免主观。我设计了一个简单的预测命中率指标用来量化不同overlap对趋势判断的准确度。规则是如果趋势线在价格上方且未来horizon根K线价格确实上涨记一次正确趋势线在价格下方且未来horizon根K线价格确实下跌也记一次正确。最后除以有判断的总次数。def hit_rate(trendline, price, horizon5): up trendline price down trendline price future_change np.diff(price, nhorizon) future_change np.concatenate([future_change, np.zeros(horizon)]) correct np.sum(up (future_change 0)) np.sum(down (future_change 0)) return correct / max(np.sum(up | down), 1) for ov in overlap_values: tl, _, _, _ hilbert_instantaneous_trendline(price, smooth5, overlapov) print(foverlap {ov:.1f}, hit_rate {hit_rate(tl, price, horizon5):.2%})我这边的运行结果大概是这个趋势overlap命中率horizon5趋势线波动程度0.053.2%低0.355.8%中低0.658.1%中0.957.4%高看到没有overlap从0.6提到0.9之后命中率反而下降了。原因在于外推步长太大趋势线过于敏感噪声被放大成虚假信号导致判断的稳定性下降。这说明重叠因子不是越大越好它存在一个最优区间。5.3 不同市场状态下的选择建议经过在不同模拟数据上的反复测试我总结出几条选参经验趋势型行情比如大宗商品的流畅单边行情overlap可以打到0.6到0.8让趋势线足够灵敏提前捕捉反转。震荡型行情overlap最好压在0.2到0.4宁可迟钝一些也要减少假信号。没有先验判断时0.5是个相当中庸的默认值不会太好也不会太糟。你还可以考虑给overlap加上自适应逻辑用瞬时频率的大小来判断当前行情状态。当瞬时频率的绝对值比较大、且方向明确时把overlap调大当瞬时频率绝对值很小、接近死区时把overlap调小。这样趋势线的灵敏度就能跟着市场节奏来切换。6. 实战应用心得方向判断、震荡过滤与常见坑6.1 基于趋势线的方向过滤策略实际应用我并没有把瞬时趋势线当成孤立的买卖信号而是用它做方向过滤。最简单的用法是价格在瞬时趋势线上方运行只考虑做多或者持有。价格在瞬时趋势线下方运行只考虑做空或者空仓。趋势线本身拐头向上且价格上穿趋势线作为多头入场候选。趋势线本身拐头向下且价格下穿趋势线作为空头入场候选。这个用法比我一开始直接“趋势线上持多、线下持空”要稳定得多因为直接穿越在震荡市里会产生大量来回扇耳光的交易而方向过滤只帮你屏蔽逆势交易具体的入场出场还是交给入场信号去控制。6.2 用瞬时频率过滤震荡市希尔伯特变换的副产品瞬时频率在这个地方很值钱。当价格处于震荡区间时瞬时频率通常会在零轴附近来回跳跃没有稳定的持续方向。我用一个滑动窗口内的瞬时频率符号一致性来定义“趋势可靠度”def trend_regime(omega, window10, threshold0.3): sign_sum np.convolve(np.sign(omega), np.ones(window), modesame) regime np.abs(sign_sum) / window return regime thresholdregime接近1表示相位一直朝一个方向旋转说明市场处于有方向的趋势状态regime接近0表示相位方向不断变化说明市场在震荡。我实际回测的时候在regime小于0.3的阶段强行过滤掉所有交易信号直接砍掉了将近一半的假信号资金曲线的回撤显著变浅。6.3 算出来是未来外推但绝不要忘了噪声希尔伯特趋势线表面看着像是“预测了未来”但它用的瞬时频率和速度都是从过去数据里算出来的。这个外推在趋势延续时很准但它假设了一阶导数在短时间内的稳定性遇到价格跳空或者突发消息瞬时频率会瞬间失真趋势线会打出一个夸张的假突破。所以在我的交易逻辑里每根K线收盘后重新计算一次趋势线绝不盘中实时追价也算一种天然防噪声机制。6.4 参数优化的边界我见过不少人用网格搜索同时优化smooth、overlap、horizon三组参数最后在历史数据上跑出一个漂亮到不真实的资金曲线然后一到实盘就崩。这个指标也一样参数并不是越“精细”越好。我的残酷经验是同一个参数组合只要换一段历史行情最优区间就会偏移。所以别把overlap优化到小数点后三位固定在0.5到0.6之间用交易逻辑去控制风险比用参数去拟合历史可靠得多。6.5 一个少见但很实用的技巧最后分享一个小技巧把希尔伯特趋势线和它的瞬时速度放在一起看。速度从负变正、趋势线仍然在价格下方的时候经常是底部企稳的早期信号速度从正变负、趋势线仍然在价格上方的时候经常是顶部滞涨的早期信号。因为趋势线是位置速度是位置的变化率二者不同步的窗口正好对应动量转换的临界点。我发现这个组合对识别趋势拐点的提前量比单独看趋势线还要再多1到2根K线。这部分原理并不复杂就是位置和动量的经典关系但真正用在希尔伯特变换的语境里之后这套组合让我对“瞬时”这个词有了实感。
返回列表