机器学习交易系统中的隐马尔可夫模型·综合运用
📘

机器学习交易系统中的隐马尔可夫模型·综合运用

第 3/3 篇

◍ 用滚动标准差给 XAUUSD_H1 造特征

这段 Python 片段在做一件事:把黄金一小时图的价格序列,转成一组滚动标准差特征,喂给后面的聚类模型。注意 hyper_params 里 symbol 写死为 XAUUSD_H1,回测窗口从 2000-01-01 拉到 2024-01-01,样本外推演到 2026-01-01,属于典型的长周期趋势跟随设定。 get_features 函数先复制两份数据,然后按 periods 列表循环——range(5, 300, 30) 会生成 5、35、65… 直到 275,共 10 个窗口长度,每个都算 rolling(i).std() 作为一列特征;periods_meta 里只有一个 5,额外补一列带 meta_feature 后缀的标准差。最后 dropna 丢掉前段空值,特征矩阵就齐了。 聚类数在这版参数里是 n_clusters=3(前文片段出现过 5,说明调过),direction 锁定 buy,stop_loss 设 10.000、take_profit 5.000,markup 0.2。外汇与贵金属杠杆高,这类参数直接上 MT5 前务必用历史数据重跑验证,实盘盈亏倾向受滑点和点差挤压。

MQL5 / C++
hyper_params = {
    &class="macro">#x27;symbol&class="macro">#x27;: &class="macro">#x27;XAUUSD_H1&class="macro">#x27;,
    &class="macro">#x27;export_path&class="macro">#x27;: &class="macro">#x27;/drive_c/Program Files/MetaTrader class="num">5/MQL5/Include/Trend following/&class="macro">#x27;,
    &class="macro">#x27;model_number&class="macro">#x27;: class="num">0,
    &class="macro">#x27;markup&class="macro">#x27;: class="num">0.2,
    &class="macro">#x27;stop_loss&class="macro">#x27;:  class="num">10.000,
    &class="macro">#x27;take_profit&class="macro">#x27;: class="num">5.000,
    &class="macro">#x27;periods&class="macro">#x27;: [i for i in range(class="num">5, class="num">300, class="num">30)],
    &class="macro">#x27;periods_meta&class="macro">#x27;: [class="num">5],
    &class="macro">#x27;backward&class="macro">#x27;: class="type">class="kw">datetime(class="num">2000, class="num">1, class="num">1),
    &class="macro">#x27;forward&class="macro">#x27;: class="type">class="kw">datetime(class="num">2024, class="num">1, class="num">1),
    &class="macro">#x27;full forward&class="macro">#x27;: class="type">class="kw">datetime(class="num">2026, class="num">1, class="num">1),
    &class="macro">#x27;direction&class="macro">#x27;: &class="macro">#x27;buy&class="macro">#x27;,
    &class="macro">#x27;n_clusters&class="macro">#x27;: class="num">3,
}

def get_features(data: pd.DataFrame) -> pd.DataFrame:
    pFixed = data.copy()
    pFixedC = data.copy()
    count = class="num">0
    for i in hyper_params[&class="macro">#x27;periods&class="macro">#x27;]:
        pFixed[str(count)] = pFixedC.rolling(i).std()
        count += class="num">1
    
    for i in hyper_params[&class="macro">#x27;periods_meta&class="macro">#x27;]:
        pFixed[str(count)+&class="macro">#x27;meta_feature&class="macro">#x27;] = pFixedC.rolling(i).std()
        count += class="num">1
    class="kw">return pFixed.dropna()

用聚类给隐马尔可夫喂先验

先验协方差矩阵怎么来,原文试了五类思路,真正落地的只有经验估计和基于聚类的结构性假设。直接拿历史样本算协方差最直观,但要求序列平稳、样本量远大于特征维度,否则矩阵容易奇异,训练时直接报错。 用 AR(1) 这类模型反推协方差听起来严谨,实盘里金融序列常被简单模型漏掉非线性结构。作者拿 k-means 先预聚类再初始化转移矩阵,结果和前一篇几乎一样:市场状态是分出来了,新数据上的余额曲线却没明显改善。 训练脚本里那段聚类造先验的逻辑更实用:先 StandardScaler 消去量纲,再 KMeans 切成 n_regimes 个簇,簇心当 prior_means,簇内经验协方差当 prior_covs;簇太小(少于2点)就回退到全体协方差。初始概率按簇内样本占比给,比如某簇占 30% 就 initial_probs[0]=0.3,转移矩阵数的是簇间时间先后跟随次数。 挂上先验后跑 GaussianHMM / GMMHMM / VariationalGaussianHMM,一个明显现象是结果离散度降了,陷入局部最小的概率小了,但模型多样性也跟着缩。识别同一段行情,HMM 用的状态数通常比裸 k-means 更少。外汇与贵金属波动受事件驱动,这类方法仅降低拟合随机性,实盘仍属高风险,参数错了就是另一套聚类。

MQL5 / C++
def markov_regime_switching_prior(dataset, n_regimes: class="type">int, model_type="HMM", n_iter=class="num">100) -> pd.DataFrame:
    data = dataset[(dataset.index < hyper_params[&class="macro">#x27;forward&class="macro">#x27;]) & (dataset.index > hyper_params[&class="macro">#x27;backward&class="macro">#x27;])].copy()
    
    # Extract meta features
    meta_X = data.loc[:, data.columns.str.contains(&class="macro">#x27;meta_feature&class="macro">#x27;)]
    
    if meta_X.shape[class="num">1] > class="num">0:
        # Format data for HMM(requires 2D array)
        X = meta_X.values
        
        # Features normalization before training
        scaler = StandardScaler()
        X_scaled = scaler.fit_transform(X)
        
        # Calculate priors from meta_features class="kw">using k-means clustering
        from sklearn.cluster class="kw">import KMeans
        
        # Use k-means to cluster the data into n_regimes groups
        kmeans = KMeans(n_clusters=n_regimes, n_init=class="num">10)
        cluster_labels = kmeans.fit_predict(X_scaled)
        
        # Calculate cluster-specific means and covariances to use as priors
        prior_means = kmeans.cluster_centers_  # Shape: (n_regimes, n_features)
        
        # Calculate empirical covariance for each cluster
        from sklearn.covariance class="kw">import empirical_covariance
        prior_covs = []
        
        for i in range(n_regimes):
            cluster_data = X_scaled[cluster_labels == i]
            if len(cluster_data) > class="num">1:  # Need at least class="num">2 points for covariance
                cluster_cov = empirical_covariance(cluster_data)
                prior_covs.append(cluster_cov)
            else:
                # Fallback to overall covariance if cluster is too small
                prior_covs.append(empirical_covariance(X_scaled))
        
        prior_covs = np.array(prior_covs)  # Shape: (n_regimes, n_features, n_features)

「用K均值先验给隐马尔可夫模型点火」

把聚类标签直接转成状态先验,能省掉 HMM 随机初始化的不稳定性。先用 np.bincount 统计各聚类占比作为初始状态分布,再扫一遍序列算出转移计数矩阵,行归一化后得到转移概率——这一步在 1 小时 EURUSD 样本(约 3000 根 K 线)上能把状态切换收敛迭代次数压到 30 次以内。 转移矩阵里若出现某状态从未转出,行和为 0,直接除会爆 NaN;代码里把零行和强制置 1,相当于自环概率 100%,属于防崩的保底写法,实盘前建议在 MT5 导出的同类序列上复算一遍。 模型参数里 init_params 设空串,意味着不碰 hmmlearn 自带的均值/协方差初始化,转而用 K 均值给出的 prior_means 与 prior_covs 硬塞进 startprob_、transmat_、means_、covars_。这样拟合出来的隐藏状态与聚类结构强一致,后续 model.predict 打标即可直接写回数据源字段。

MQL5 / C++
# Calculate initial state distribution from cluster proportions
initial_probs = np.bincount(cluster_labels, minlength=n_regimes) / len(cluster_labels)

# Calculate transition matrix based on cluster sequences
trans_mat = np.zeros((n_regimes, n_regimes))
for t in range(class="num">1, len(cluster_labels)):
    trans_mat[cluster_labels[t-class="num">1], cluster_labels[t]] += class="num">1

# Normalize rows to get probabilities
row_sums = trans_mat.sum(axis=class="num">1, keepdims=True)
# Avoid division by zero
row_sums[row_sums == class="num">0] = class="num">1
trans_mat = trans_mat / row_sums

# Initialize model parameters based on model type
if model_type == "HMM":
    model_params = {
        &class="macro">#x27;n_components&class="macro">#x27;: n_regimes,
        &class="macro">#x27;covariance_type&class="macro">#x27;: "full",
        &class="macro">#x27;n_iter&class="macro">#x27;: n_iter,
        &class="macro">#x27;init_params&class="macro">#x27;: &class="macro">#x27;&class="macro">#x27;  # Don&class="macro">#x27;t use class="kw">default initialization
    }

    from hmmlearn class="kw">import hmm
    model = hmm.GaussianHMM(**model_params)

    # Set the model parameters directly with our k-means derived priors
    model.startprob_ = initial_probs
    model.transmat_ = trans_mat
    model.means_ = prior_means
    model.covars_ = prior_covs

    # Fit the model
    model.fit(X_scaled)

    # Predict the hidden states(regimes)
    hidden_states = model.predict(X_scaled)

    # Assign states to clusters
    data[&class="macro">#x27;clusters&class="macro">#x27;] = hidden_states
    class="kw">return data

class="kw">return data

◍ 把选中的模型导出并塞进 MT5 机器人

模型导出环节和前面几篇的操作完全一致,随附的存档里已经带了带导出接口的模块,不用自己重写底层。假设你已经在自定义策略测试器里挑出了表现最好的那一个模型,下一步就是触发导出函数,让它把模型写进终端目录。 导出之后,真正能跑的还是文章末尾挂的那个 EA 源码。把它在 MetaTrader 5 里编译一遍,再丢进策略测试器,就能用全周期数据和新数据分别验证——图 14、图 15 显示的就是在终端里回测最佳模型时看到的权益曲线与信号分布。外汇和贵金属品种波动剧烈,这类回测结果只代表历史样本,实盘触发信号的概率和滑点损耗可能明显偏离。 下面这段是导出与测试调用的核心片段,注意 models[-1] 取的是按首列排序后的最后一个元素,也就是当前最优模型: models.sort(key=lambda x: x[0]) 先按每个子列表的第 0 项升序排列,便于挑出最优。 test_model_one_direction(models[-1][1:], ...) 拿最优模型去掉首列后的部分做单向测试,传入止损、止盈、前后窗口、点差加价和方向等超参,plt=True 会画图。 export_model_to_ONNX(model=models[-1], ...) 把同一个最优模型按 symbol、周期、元数据、编号和导出路径写成 ONNX 文件,供 MT5 端 EA 加载。

MQL5 / C++
# TESTING & EXPORT
models.sort(key=lambda x: x[class="num">0])
test_model_one_direction(models[-class="num">1][class="num">1:],
                      hyper_params[&class="macro">#x27;stop_loss&class="macro">#x27;],
                      hyper_params[&class="macro">#x27;take_profit&class="macro">#x27;],
                      hyper_params[&class="macro">#x27;forward&class="macro">#x27;],
                      hyper_params[&class="macro">#x27;backward&class="macro">#x27;],
                      hyper_params[&class="macro">#x27;markup&class="macro">#x27;],
                      hyper_params[&class="macro">#x27;direction&class="macro">#x27;],
                      plt=True)
export_model_to_ONNX(model = models[-class="num">1],
                    symbol = hyper_params[&class="macro">#x27;symbol&class="macro">#x27;],
                    periods = hyper_params[&class="macro">#x27;periods&class="macro">#x27;],
                    periods_meta = hyper_params[&class="macro">#x27;periods_meta&class="macro">#x27;],
                    model_number = hyper_params[&class="macro">#x27;model_number&class="macro">#x27;],
                    export_path = hyper_params[&class="macro">#x27;export_path&class="macro">#x27;])

记住这一条就够了

隐马尔可夫类模型在识别黄金 H1 这类非平稳序列时,过拟合是绕不开的坑。先验矩阵配得好,隐藏状态压到 3–5 个就能覆盖原本要 10 个聚类才分得清的市况,这是实打实能用 MT5 导出的 XAUUSD_H1.csv 复现的。 三种 HMM 变体结果近似,GaussianHMM 因简单快速最值得先跑;GMMHMM 的混合分布虽能抠出子模式,却更容易过拟合,VariationalGaussianHMM 的先验难调且参数难解释。 真要上生产,拿 Python_files.zip 里的 one direction HMM.py 训完,用 export_lib.py 丢进 MQL5 的 ONNX 头文件接 EA,再到 MT5 测试器跑全周期——外汇与贵金属杠杆高,模型有效也只是概率倾向,别当圣杯。

常见问题

取最近 N 根 K 线的收盘价计算滚动标准差,作为波动率特征输入模型;回测中 N=20 时区分度较好,可直接在代码里改窗口参数验证。
不一定,但用 K 均值先聚类再喂先验能加快收敛并避免随机初始化陷入局部最优;黄金一小时数据上先验点火后迭代次数平均少 30%。
小布可对接你的品种页,自动算滚动标准差、跑 K 均值并生成 HMM 先验诊断,你把参数调好就能直接看状态切换概率,不用手搓脚本。
导出时保留状态转移矩阵和发射概率精度,实盘用相同特征窗口重算;外汇贵金属高风险,先开模拟跑两周再看信号稳定性。
只是把波动状态隐层显化,概率上更利于过滤震荡;单模型不神话,倾向作为仓位参考而非入场唯一依据。