MF-DFA + 分形动态阈值:让Δ²P拐点交易框架学会“呼吸”

从"固定参数"到"尺度感知",把交易系统的阈值从死的变成活的


一、开场白:为什么你的拐点系统总在震荡市被来回打脸?

如果你已经搭过一个基于二阶差分(Δ²P)的拐点检测框架,大概率遇到过这个问题:

震荡市里信号满天飞,趋势市里信号迟迟不来。

原因很简单------你在用一个固定阈值去判断"加速度是否显著"。但金融市场的噪声强度是时变的:平静期Δ²P的自然抖动很小,高波动期Δ²P被波动率拖着跑。用一个全局常数去卡,等价于假设"背景噪声恒定"------这在分形世界里是错的。

本文把两个方向串起来给你:

方向 解决什么问题 输出
MF-DFA(诊断层) 市场现在是"均匀粗糙"还是"碎裂模式"? 谱宽Δα、机制标签
分形动态阈值(执行层) 此时此刻,Δ²P的"合理抖动边界"是多少? θ(t)逐根K线自适应

两者关系很简单:MF-DFA是显微镜(看结构有多不均匀),动态阈值是恒温器(让阈值跟着粗糙度呼吸)


二、方向一:MF-DFA------检测市场是否进入"碎裂模式"

2.1 为什么普通Hurst不够?

Hurst指数H的本质是问:R/S∼τHR/S \sim \tau^HR/S∼τH。但它有个致命弱点------把所有波动当成同一类

现实中的股价是这样的:

  • 有些天走势温和,H≈0.6(持久性强)
  • 有些天闪崩/暴涨,局部H可能瞬间塌到0.3以下

普通H把这两种混在一起平均,给你一个看似合理的H≈0.58------但这恰恰掩盖了最有交易价值的信息:粗糙度本身在随时间跳变。MF-DFA要解决的,就是这个。

2.2 从DFA起步:先扒掉趋势再量波动

股价里混着低频趋势(宏观漂移、结构性牛熊),直接算方差会把"趋势"误认为"波动"。DFA的核心思想:先去掉局部趋势,再看残差有多跳

Step 1:累积和

给定对数价格序列 y(i)=ln⁡Piy(i)=\ln P_iy(i)=lnPi,做累积偏差:

Y(k)=∑i=1k(y(i)−yˉ)Y(k)=\sum_{i=1}^{k}(y(i)-\bar{y})Y(k)=i=1∑k(y(i)−yˉ)

物理直觉:把局部收益累加,让低频趋势变成Y(k)Y(k)Y(k)里的多项式成分,噪声变成"绕趋势线抖的部分"。

Step 2:分段 + 多项式去趋势

把Y(k)Y(k)Y(k)切成长度为τ\tauτ的窗口(通常取τ=2n\tau=2^nτ=2n,如16、32、64......直到N/4N/4N/4):

Yv(j)=Y((v−1)τ+j),j=1,...,τY_v(j)=Y((v-1)\tau+j),\quad j=1,\ldots,\tauYv(j)=Y((v−1)τ+j),j=1,...,τ

对每个窗口做最小二乘多项式拟合(阶数m=1m=1m=1或2):

Yfit,v(j)=a0+a1j+a2j2+⋯Y_{fit,v}(j)=a_0+a_1j+a_2j^2+\cdotsYfit,v(j)=a0+a1j+a2j2+⋯

残差:

Rv(j)=Yv(j)−Yfit,v(j)R_v(j)=Y_v(j)-Y_{fit,v}(j)Rv(j)=Yv(j)−Yfit,v(j)

Step 3:波动函数

每个窗口的均方残差:

Fv(τ)=1τ∑j=1τRv(j)2F_v(\tau)=\sqrt{\frac{1}{\tau}\sum_{j=1}^{\tau}R_v(j)^2}Fv(τ)=τ1j=1∑τRv(j)2

全局波动(q=2q=2q=2时的特例):

FDFA(τ)=1Nseg∑v=1NsegFv(τ)2F_{DFA}(\tau)=\sqrt{\frac{1}{N_{seg}}\sum_{v=1}^{N_{seg}}F_v(\tau)^2}FDFA(τ)=Nseg1v=1∑NsegFv(τ)2

然后看标度:

FDFA(τ)∼τHDFAF_{DFA}(\tau)\sim\tau^{H_{DFA}}FDFA(τ)∼τHDFA

  • HDFA≈0.5H_{DFA}\approx0.5HDFA≈0.5 → 基本随机
  • >0.55>0.55>0.55 → 趋势持久
  • <0.45<0.45<0.45 → 均值回归倾向

到这里就是普通DFA。它已经比裸方差靠谱了------先扒掉局部趋势再量粗糙度

2.3 进入MF-DFA:从"只看方差"到"看各阶矩"

普通DFA只看L2L_2L2范数(平方和→标准差)。但如果你的粗糙度是多重分形的,不同幅度的波动缩放方式不同 ------小幅波动和大幅波动的τ\tauτ指数不一样。

解法:引入qqq阶矩:

Fq(τ)=1Nseg∑v=1Nseg(Fv(τ))q1/q,q≠0F_q(\tau)=\left\\frac{1}{N_{seg}}\\sum_{v=1}\^{N_{seg}}(F_v(\\tau))\^q\\right^{1/q},\quad q\neq0Fq(τ)= Nseg1v=1∑Nseg(Fv(τ))q 1/q,q=0

q=0q=0q=0时用对数平均的极限形式:

F0(τ)=exp⁡1Nseg∑vln⁡Fv(τ)F_0(\tau)=\exp\left\\frac{1}{N_{seg}}\\sum_v\\ln F_v(\\tau)\\rightF0(τ)=expNseg1v∑lnFv(τ)

然后对每个qqq拟合:

Fq(τ)∼τh(q)F_q(\tau)\sim\tau^{h(q)}Fq(τ)∼τh(q)

这个h(q)h(q)h(q)叫广义Hurst指数

关键直觉

q值 在加权什么 单分形市场 多重分形市场
q<0q<0q<0(如-2、-5) 小幅波动被放大 h(q)h(q)h(q)常数 h(q)h(q)h(q)上升
q=2q=2q=2 普通波动(方差) h(2)=Hh(2)=Hh(2)=H 只是其中一个截面
q>0q>0q>0(如+3、+4) 大幅波动/尖峰被放大 h(q)h(q)h(q)常数 h(q)h(q)h(q)下降

如果h(q)h(q)h(q)随qqq变化→多重分形;如果h(q)h(q)h(q)是一条水平线→单分形

2.4 多重分形谱f(α)f(\alpha)f(α)------把"不均匀"画成一张图

把h(q)h(q)h(q)转换成更直观的奇异性谱。做Legendre变换:

τ(q)=qh(q)−1\tau(q)=qh(q)-1τ(q)=qh(q)−1

α=h(q)+qh′(q)\alpha=h(q)+qh'(q)α=h(q)+qh′(q)

f(α)=q(α−h(q))+1f(\alpha)=q(\alpha-h(q))+1f(α)=q(α−h(q))+1

你不需要记住这些公式。你需要抓住的是:

谱的性质 含义
谱宽Δα=αmax−αmin\Delta\alpha=\alpha_{max}-\alpha_{min}Δα=αmax−αmin 越大=越"多重分形"=越不均匀
谱左偏(f(α)f(\alpha)f(α)左侧更高) 大幅波动主导(危机/暴涨期常见)
谱右偏 小幅波动主导(平静期)

交易翻译

  • 当Δα\Delta\alphaΔα突然扩张→市场从"均匀粗糙"跳到"碎玻璃模式"→紧止损、降杠杆
  • 当Δα\Delta\alphaΔα收窄回稳态→可以重新开趋势策略
  • 它不是价格方向信号,它是机制切换的早期温度计

2.5 最小实现的numpy骨架

python 复制代码
import numpy as np

def poly_detrend(x, order=1):
    """局部多项式去趋势(返回残差)"""
    t = np.arange(len(x))
    coeffs = np.polyfit(t, x, order)
    trend = np.polyval(coeffs, t)
    return x - trend

def mfdfa_profile(y, tau_list, q_list, deg=1):
    """
    y: log-price 或累积和(通常先用累积和)
    tau_list: 窗口长度列表 e.g. [16, 24, 32, 48, 64, ...]
    q_list: e.g. [-5, -3, -2, -1, 0, 1, 2, 3, 5]
    """
    Y = y
    N = len(Y)
    Fq = {q: [] for q in q_list}
    
    for tau in tau_list:
        if tau >= N:
            break
        n_seg = N // tau
        if n_seg < 2:
            continue
        
        Yc = Y[:n_seg * tau]
        segs = Yc.reshape(n_seg, tau)
        Fvs = []
        
        for seg in segs:
            res = poly_detrend(seg, order=deg)
            Fv = np.sqrt(np.mean(res**2))
            Fvs.append(Fv)
        
        Fvs = np.array(Fvs)
        Fvs = Fvs[Fvs > 0]
        
        for q in q_list:
            if q == 0:
                F_val = np.exp(np.mean(np.log(Fvs)))
            else:
                F_val = (np.mean(Fvs**q))**(1.0/q)
            Fq[q].append(F_val)
    
    return Fq, tau_list[:len(Fq[q_list[0]])]

# ---- 用法示例 ----
P = np.array([100, 102, 105, 109, 108, 106, 107, 108, 111, 115, 
              114, 112, 110, 113, 117, 120, 118, 116, 114, 115, 
              119, 122, 121])
y = np.log(P)
Y = np.cumsum(y - y.mean())  # 累积和

tau_list = [8, 12, 16, 20, 24, 28, 32]
q_list = [-5, -3, -2, -1, 0, 1, 2, 3, 5]
Fq, taus = mfdfa_profile(Y, tau_list, q_list, deg=1)

# 粗略看 h(q):对每个q拟合 log(tau)-log(Fq)
for q in [-5, 0, 5]:
    log_t = np.log(taus)
    log_F = np.log(Fq[q])
    h = np.polyfit(log_t, log_F, 1)[0]
    print(f"h({q}) ≈ {h:.3f}")

⚠️ 实操提醒 :上面7个点只是演示骨架,真玩最少要几百根K线,τ\tauτ范围也得覆盖一个合理的倍频程。

2.6 MF-DFA对交易框架的意义(落地)

它不是用来生成"买/卖"的,它是用来回答:"此刻我的Δ²P系统,运行在一个单分形环境还是多重分形环境?"

具体用法:

python 复制代码
if Δα_this_window >> Δα_baseline:  # e.g. > 1.5×baseline
    regime = "FRACTURED"  # → 降杠杆,放宽Δ²P阈值或直接关信号
else:
    regime = "UNIFORM"    # → Δ²P系统正常运作

这比静态ADX聪明得多------ADX看的是"有没有趋势",MF-DFA看的是 "粗糙度本身是否正在裂变" ,前者是表层,后者是结构层。


三、方向二:把Δ²P阈值从固定值升级为分形动态阈值

这是更直接能喂进交易框架的东西。

3.1 固定阈值到底错在哪?

你的框架里有一句:|Δ²P| > threshold(全局固定值)。

假设市场价格服从分形噪声而非i.i.d.:

  • 低波动时段:Δ²P的"自然抖动"很小→固定阈值相对太大→信号迟钝(拐点确认太晚)
  • 高波动时段(波动率聚集) :Δ²P被波动率拖着走→固定阈值相对太小→被噪声频繁击穿,假信号炸裂

本质上:固定阈值暗示"Δ²P的背景噪声水平恒定",但分形世界里背景噪声是时变的且尺度耦合的

3.2 分形缩放告诉我们应该用什么形式

回忆分形标度:σ(τ)∼τH\sigma(\tau)\sim\tau^Hσ(τ)∼τH。Δ²P(差分两次)=相邻ΔP之差,它的"合理抖动幅度"应该正比于局部σ,而不是一个全局常数。

正确的动态阈值长这样:

θ(t)=κ⋅σlocal(t)⋅τscaleHlocal(t)\theta(t)=\kappa\cdot\sigma_{local}(t)\cdot\tau_{scale}^{H_{local}(t)}θ(t)=κ⋅σlocal(t)⋅τscaleHlocal(t)

因子 是什么 怎么估
σlocal(t)\sigma_{local}(t)σlocal(t) 当前局部波动率("现在市场多吵") rolling std of returns,或ATR/N
τscale\tau_{scale}τscale 你操作的时间尺度 常数,取决于你用的K线(日线=1)
Hlocal(t)H_{local}(t)Hlocal(t) 局部持久性 滑动窗估H,或用MF-DFA的h(q=2)h(q=2)h(q=2)近似
κ\kappaκ 置信系数(≈1.5~2.5) 回测校准,或取历史分位数

3.3 工程简化版(不需要MF-DFA也能跑)

严格求Hlocal(t)H_{local}(t)Hlocal(t)每根K线很贵。实战上用两层局部估计替代,效果够用。

第一层:σlocal\sigma_{local}σlocal ------用滚动std或ATR

python 复制代码
# σ_local: 过去L根K线return的滚动std
sigma_local = pd.Series(r).rolling(L).std()

# 或者用ATR归一化(更稳健对抗跳空)
atr = ATR(high, low, close, L)
sigma_local = atr / close  # 相对形式

第二层:HHH代理 ------用ΔP的自相关区分"趋势态"和"碎裂态"

python 复制代码
# ΔP = diff(close)
rho = roll_autocorr(delta_P, lag=1, window=20)
H_proxy = np.where(np.abs(rho) > 0.15, 0.62, 0.42)

当∣ρΔP∣>ρthr|\rho_{\Delta P}| > \rho_{thr}∣ρΔP∣>ρthr(ΔP自相关显著)→Htrend≈0.62H_{trend}\approx0.62Htrend≈0.62;否则→Hfractured≈0.42H_{fractured}\approx0.42Hfractured≈0.42

组装动态阈值

python 复制代码
kappa = 2.0

# 基础版(推荐从这里开始)
threshold_t = kappa * sigma_local

# 精细版:加一个碎裂惩罚
fracture_penalty = 1.0 + 0.8 * (0.5 - H_proxy).clip(0)  # H低→阈值放大
threshold_t = kappa * sigma_local * fracture_penalty

3.4 塞回买卖决策树(升级版)

旧版

复制代码
if sign(Δ²P) flips AND |Δ²P| > FIXED_THEN → 候选信号

新版

复制代码
σ_t = rolling_std(r, L)
H_t = rolling_hurst_proxy()  # 或 h(q=2) from mini-DFA
θ_t = κ · σ_t · φ(H_t)

if sign(Δ²P) flips AND |Δ²P| > θ_t:
    → 候选信号
    → 等下一根K线确认(不变)
    → 额外检查:if Δα_t expanded → 否决(碎裂期不追)

效果

  • 震荡市:σ_t小但H_t也降→θ_t自适应收紧/放松,假翻号被σ_t自然压下
  • 趋势爆发行情:σ_t飙升→θ_t同步抬高,避免被"正常的剧烈加速度变化"误杀
  • 闪崩碎裂:Δα扩→额外否决层兜底

这才是"分形思维"喂进交易系统的正确姿势:不是预测方向,是让阈值呼吸


四、一张表总结:两个方向的关系

维度 MF-DFA路线 动态阈值路线
在测什么 波动的多重性(谱宽、机制裂变) 当前尺度的"合理抖动边界"
输出 Δα、h(q)、机制标签 θ(t)逐根K线自适应
喂给交易 开关层:什么时候信信号/什么时候关系统 过滤层:单个信号是否超噪声
计算成本 高(多尺度+多项式拟合+q阶矩) 低(滚动std+自相关+乘因子)
先搞哪个 研究/周级别监控 ✅ 先上这个,明天就能跑

五、完整可运行信号模块

下面是一个可直接接入pandas DataFrame(OHLCV)的信号模块:

python 复制代码
import numpy as np
import pandas as pd

def compute_dynamic_threshold(close, high=None, low=None, 
                               L=20, kappa=2.0, method='std'):
    """
    计算分形动态阈值 θ(t)
    
    Parameters
    ----------
    close : pd.Series
        收盘价序列
    high, low : pd.Series, optional
        用于ATR计算
    L : int
        滚动窗口长度
    kappa : float
        置信系数
    method : str
        'std' 或 'atr'
    
    Returns
    -------
    theta : pd.Series
        动态阈值序列
    H_proxy : pd.Series
        Hurst代理序列
    """
    r = np.log(close).diff()
    
    # 第一层:局部波动率
    if method == 'std':
        sigma_local = r.rolling(L).std()
    else:  # 'atr'
        if high is None or low is None:
            raise ValueError("ATR method requires high and low")
        tr1 = high - low
        tr2 = (high - close.shift()).abs()
        tr3 = (low - close.shift()).abs()
        tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
        atr = tr.rolling(L).mean()
        sigma_local = atr / close
    
    # 第二层:H代理(基于ΔP自相关)
    delta_P = close.diff()
    rho = delta_P.rolling(L).apply(lambda x: x.autocorr(lag=1), raw=False)
    H_proxy = np.where(np.abs(rho) > 0.15, 0.62, 0.42)
    H_proxy = pd.Series(H_proxy, index=close.index)
    
    # 组装动态阈值
    fracture_penalty = 1.0 + 0.8 * (0.5 - H_proxy).clip(0)
    theta = kappa * sigma_local * fracture_penalty
    
    return theta, H_proxy


def compute_signals(df, L=20, kappa=2.0, method='std', 
                    min_confirm_bars=1):
    """
    完整的信号生成函数
    
    Parameters
    ----------
    df : pd.DataFrame
        必须包含 'close',可选 'high', 'low'
    L : int
        滚动窗口长度
    kappa : float
        置信系数
    method : str
        'std' 或 'atr'
    min_confirm_bars : int
        确认所需K线数
    
    Returns
    -------
    df : pd.DataFrame
        添加了 'signal' 和 'regime' 列
    """
    df = df.copy()
    close = df['close']
    
    # 1. 计算Δ²P(加速度)
    logP = np.log(close)
    delta_P = logP.diff()
    delta2_P = delta_P.diff()
    
    # 2. 计算动态阈值
    theta, H_proxy = compute_dynamic_threshold(
        close, 
        high=df.get('high'), 
        low=df.get('low'),
        L=L, 
        kappa=kappa, 
        method=method
    )
    
    # 3. 计算Δα(MF-DFA谱宽)------简化版用H_proxy的波动替代
    #    严格MF-DFA需要多尺度计算,这里用H_proxy的rolling std作为代理
    delta_alpha = H_proxy.rolling(L).std() * 10  # 经验缩放
    
    # 4. 机制判断
    baseline_alpha = delta_alpha.rolling(L*3).mean()
    regime = np.where(
        delta_alpha > 1.5 * baseline_alpha,
        'FRACTURED',
        'UNIFORM'
    )
    
    # 5. 信号生成
    sign_flip = np.sign(delta2_P) != np.sign(delta2_P.shift())
    threshold_breach = np.abs(delta2_P) > theta
    
    raw_signal = sign_flip & threshold_breach
    
    # 6. 碎裂期否决
    final_signal = raw_signal & (regime != 'FRACTURED')
    
    # 7. 确认机制(等N根K线)
    if min_confirm_bars > 1:
        confirm = final_signal.rolling(min_confirm_bars).sum() >= min_confirm_bars
        final_signal = confirm
    
    df['delta2_P'] = delta2_P
    df['theta'] = theta
    df['H_proxy'] = H_proxy
    df['regime'] = regime
    df['signal'] = final_signal.astype(int)
    
    return df


# ---- 使用示例 ----
# df = pd.read_csv('your_data.csv')
# df['date'] = pd.to_datetime(df['date'])
# df = df.set_index('date')
# 
# result = compute_signals(df, L=20, kappa=2.0, method='std')
# 
# # 查看信号
# buy_signals = result[result['signal'] == 1]
# print(f"共产生 {len(buy_signals)} 个买入信号")
# 
# # 查看机制分布
# print(result['regime'].value_counts())

六、收束

分形的真正用处不是让你"看见图案→押注",而是把交易里三个最古老的陷阱------固定止损、固定参数、假设平稳------一个个撬开,换成尺度感知的版本。

  • MF-DFA是显微镜:看结构有多不均匀
  • 动态阈值是恒温器:让系统跟着结构的粗糙度呼吸

两个一起上,你的Δ²P拐点框架就从"聪明的TA"变成 "尺度感知的滤波器"


📌 下一步建议 :先把动态阈值版(compute_signals)接上真实数据跑一遍,观察θ(t)在不同市场环境下的行为。跑通之后,再把MF-DFA的Δα计算从简化版升级为完整的多尺度版本,作为周级别的机制监控仪表盘。