您应当知道的 MQL5 向导技术(第 60 部分):推理学习(Wasserstein-VAE),配合移动平均线和随机振荡器形态·进阶篇
📘

您应当知道的 MQL5 向导技术(第 60 部分):推理学习(Wasserstein-VAE),配合移动平均线和随机振荡器形态·进阶篇

第 2/3 篇

◍ 瓦瑟斯坦 VAE 的 Python 骨架与四件套

做这类无监督编码模型,先用 Python 而不是原生 MQL5 训练网络,速度差不是一个量级;MQL5 虽有 OpenCL 变通方案能缩小差距,但本系列暂未涉及。我们落地的瓦瑟斯坦 VAE 类,核心就四块:特征编码器、引用缓冲区、预测方法、双模前向通验。 特征编码器是三层 MLP,把输入压成潜在空间参数(z、z均值、z对数变量)。引用缓冲区存的是预训练输入——特征、状态、动作、奖励及其编码。预测方法在潜在空间内补残缺数据:KNN 映射状态、高斯加权映射动作、线性回归映射奖励。双模前向通验同时服务训练与推理。 编码过程把 FSAR 输入过网络,切成 z / z均值 / z对数变量,重参数化技巧保证可微采样。引用系统靠 update_references() 显式冻结输出与配对输入,不是自动更新的。 原结构仍有缝可补:架构上加谱归一化强制 Lipschitz、给高斯加权做可学习温度、引用内存搞 FIFO 修剪、蒙特卡洛抽 Uncertain;训练上加瓦瑟斯坦梯度惩罚、MMD 潜在正则、自适应选预测器;健壮侧做分布外检测、参考品质评分、动态邻域、输入依赖噪声缩放。外汇与贵金属行情高波动,这类模型外推失真概率不低,上 MT5 前务必用历史片段回测。 下面这段是类定义主干,逐行拆完你能直接抄去改 dim 试。

MQL5 / C++
class WassersteinVAEUnsupervised(nn.Module):
    def __init__(self, feature_dim, encoding_dim, k_neighbors=class="num">5):
        super().__init__()
        self.encoding_dim = encoding_dim
        self.k_neighbors = k_neighbors
        
        # Feature encoder
        self.feature_encoder = nn.Sequential(
            nn.Linear(feature_dim, class="num">256),
            nn.ReLU(),
            nn.Linear(class="num">256, class="num">128),
            nn.ReLU(),
            nn.Linear(class="num">128, encoding_dim * class="num">2)   # mean and logvar
        )
        
        # Buffer for storing training references
        self.register_buffer(&class="macro">#x27;ref_encoding&class="macro">#x27;, torch.zeros(class="num">1, encoding_dim))
        self.register_buffer(&class="macro">#x27;ref_states&class="macro">#x27;, torch.zeros(class="num">1, class="num">1))
        self.register_buffer(&class="macro">#x27;ref_actions&class="macro">#x27;, torch.zeros(class="num">1, class="num">1))
        self.register_buffer(&class="macro">#x27;ref_rewards&class="macro">#x27;, torch.zeros(class="num">1, class="num">1))
        self._references_loaded = False
    
    def encode(self, features):
        h = self.feature_encoder(features)
        z_mean, z_logvar = torch.chunk(h, class="num">2, dim=class="num">1)
        class="kw">return z_mean, z_logvar
    
    def reparameterize(self, mean, logvar):
        std = torch.exp(class="num">0.5 * logvar)
        eps = torch.randn_like(std)
        class="kw">return mean + eps * std
    
    def update_references(self, encoding_vectors, states, actions, rewards):
        """Store reference data for unsupervised prediction"""
        self.ref_encoding = encoding_vectors.detach().clone()
        self.ref_states = states.detach().clone().unsqueeze(-class="num">1)
        self.ref_actions = actions.detach().clone().unsqueeze(-class="num">1)
        self.ref_rewards = rewards.detach().clone().unsqueeze(-class="num">1)
        self._references_loaded = True
    
    def knn_predict(self, z, ref_values):
        # z shape: [batch_size, encoding_dim]
        # ref_values shape: [ref_size, class="num">1] or [ref_size]
        
        # Ensure ref_values is properly shaped
        ref_values = ref_values.view(-class="num">1)   # Flatten to [ref_size]
        
        # Calculate distances between z and reference encodings

近邻与高斯加权两种推断实现的细节差异

在编码空间 z 上做推断时,k 近邻分支先算 batch 内每个样本到参考编码的欧氏距离矩阵,形状为 [batch_size, ref_size],再取距离最小的 k 个邻居下标。 拿到下标后用 torch.gather 从参考值里抠出对应邻居的真实标签,沿 k 维求平均得到预测,这种硬截断方式对离群参考点不敏感,但 k 选太小容易受噪声编码干扰。 高斯加权分支走的是软权重路线:把负距离送进 softmax 得到 [batch, ref_size] 的归一化权重,再与强制 reshape 成 [792, 1] 的参考值做矩阵乘。 参考集固定 792 条样本时,权重分布倾向于把预测拉向距离近且密集的区域;外汇与贵金属行情里编码漂移快,这类软加权在样本外可能比 k 近邻更平滑但也更滞后,实盘前建议在 MT5 用历史 tick 复算一遍距离尺度。 两个分支都要求 z 是二维 [batch, encoding],否则 assert 会直接拦下;linear_predict 则额外拼了一列偏置项走正规方程,适合参考值和目标近似线性的场景。

MQL5 / C++
distances = torch.cdist(z, self.ref_encoding)  # [batch_size, ref_size]

# Get top-k nearest neighbors
_, indices = torch.topk(distances, k=self.k_neighbors, largest=False)  # [batch_size, k]

# Gather corresponding reference values
neighbor_values = torch.gather(
    ref_values.unsqueeze(class="num">0).expand(indices.size(class="num">0), -class="num">1),  # [batch_size, ref_size]
    class="num">1,
    indices
)  # [batch_size, k]

# Average the nearest values
predictions = neighbor_values.mean(dim=class="num">1, keepdim=True)  # [batch_size, class="num">1]

class="kw">return predictions

def gaussian_predict(self, z, ref_values):
    # Input validation
    assert z.dim() == class="num">2, "z must be 2D [batch, encoding]"
    assert ref_values.dim() == class="num">2, "ref_values must be 2D"

    # Calculate distances(Euclidean)
    distances = torch.cdist(z, self.ref_encoding)  # [batch, ref_size]

    # Convert to similarities(Gaussian weights)
    weights = torch.softmax(-distances, dim=class="num">1)  # [batch, ref_size]

    # Prepare reference values
    ref_values = ref_values.squeeze(-class="num">1) if ref_values.size(class="num">1) == class="num">1 else ref_values
    ref_values = ref_values.unsqueeze(class="num">0) if ref_values.dim() == class="num">1 else ref_values

    # Ensure proper shapes
    ref_values = ref_values.view(-class="num">1, class="num">1)  # Force [class="num">792, class="num">1] shape

    # Calculate distances
    distances = torch.cdist(z, self.ref_encoding)  # [batch_size, class="num">792]

    # Convert to weights
    weights = torch.softmax(-distances, dim=class="num">1)  # [batch_size, class="num">792]

    # Matrix multiplication Weighted combination
    predictions = torch.matmul(weights, ref_values)  # [batch, class="num">1]

    class="kw">return predictions.unsqueeze(-class="num">1) if predictions.dim() == class="num">1 else predictions

def linear_predict(self, z, ref_values):
    """Linear regression prediction using normal equations"""
    # Add bias term
    X = torch.cat([self.ref_encoding, torch.ones_like(self.ref_encoding[:, :class="num">1])], dim=class="num">1)
    y = ref_values

「编码空间里的三类预测分支」

这段实现把隐变量 z 当作统一中介,分别用三种算法头去逼近状态、动作与奖励。线性头走闭式解,高斯头做近邻插值,KNN 头直接查参考集,三者共享同一个 encoder 输出的 z。 闭式解部分先算 XtX 与 Xty,再用 torch.linalg.solve 一步出 theta,比梯度下降省事且数值稳定。预测时把新 z 拼一列常数偏置,矩阵乘即得连续值输出。 predict_from_encoding 会先校验参考集是否已加载,否则直接抛 RuntimeError;之后把 ref_states、ref_actions、ref_rewards 都 view 成 (-1,1) 单列,避免维度不匹配。 forward 的逻辑分两路:训练时给足 states/actions/rewards 就只回传 z 与分布参数;推理时缺标签就走预测分支,回 pred_states、pred_actions、pred_rewards 三元组。 在 MT5 里接这套,重点看 z 维度是否和参考集对齐——维度错一位,predict_from_encoding 里的 view 就会静默变形,预测曲线看着顺实际上已偏移。外汇与贵金属行情高波动,这类模型输出仅作概率参考,实盘须自担风险。

MQL5 / C++
        # Compute closed-form solution
        XtX = torch.matmul(X.T, X)
        Xty = torch.matmul(X.T, y)
        theta = torch.linalg.solve(XtX, Xty)
        
        # Predict with new z values
        X_new = torch.cat([z, torch.ones_like(z[:, :class="num">1])], dim=class="num">1)
        class="kw">return torch.matmul(X_new, theta)
    def predict_from_encoding(self, z):
        if not self._references_loaded:
            raise RuntimeError("Reference data not loaded")
            
        # Validate reference shapes
        self.ref_states = self.ref_states.view(-class="num">1, class="num">1)
        self.ref_actions = self.ref_actions.view(-class="num">1, class="num">1)
        self.ref_rewards = self.ref_rewards.view(-class="num">1, class="num">1)
        
        states = self.knn_predict(z, self.ref_states)
        actions = self.gaussian_predict(z, self.ref_actions)
        rewards = self.linear_predict(z, self.ref_rewards)
        
        class="kw">return states, actions, rewards
    
    def forward(self, features, states=None, actions=None, rewards=None):
        z_mean, z_logvar = self.encode(features)
        z = self.reparameterize(z_mean, z_logvar)
        
        if states is not None and actions is not None and rewards is not None:
            class="kw">return {
                &class="macro">#x27;z&class="macro">#x27;: z,
                &class="macro">#x27;z_mean&class="macro">#x27;: z_mean,
                &class="macro">#x27;z_logvar&class="macro">#x27;: z_logvar
            }
        else:
            pred_states, pred_actions, pred_rewards = self.predict_from_encoding(z)
            class="kw">return {
                &class="macro">#x27;states&class="macro">#x27;: pred_states,
                &class="macro">#x27;actions&class="macro">#x27;: pred_actions,
                &class="macro">#x27;rewards&class="macro">#x27;: pred_rewards
            }

◍ 用 MMD 把真实与生成分布拉到一起算

瓦瑟斯坦 VAE 在落地时常用 MMD-损失来替掉传统的 KL 项,核心就是拿真实样本 y_true 和生成样本 y_pred 做分布比对。维度必须对齐,否则配对距离根本算不出来;kernel_mul 与 kernel_num 这两个参数管的是 RBF 内核的带宽组,直接决定模型对分布差异多尺度的敏感程度。 代码里先把 xx、yy 拼成 xy 做一次 cdist(p=2 欧氏距离),所有配对在一个矩阵里算完,省内存也避免分开算带来不一致。内核用几何间隔带宽 kernel_mul^k 覆盖多个尺度,并跳过 σ=0 防零除;每个内核按计数平均,保证损失尺度在不同配置下可比。 MMD 主公式是 k_xx + k_yy - 2k_xy,从有限样本取均值估期望,最后除以 2*kernel_num 归一。默认 kernel_num=5 意味着实际用了 11 个带宽(k 从 -5 到 5),这是回测时最先该动的旋钮。 想让数值更稳,可在分母加小 ε、对极小内核值走对数域、或剪裁极端距离防溢出;带宽自适应、换非 RBF 内核也值得试。下面这段 Python(PyTorch)可直接抄去验证,注意 FASR 输入特征来自前几篇 MA 与随机振荡器代码,动作奖励来自强化学习部分。

MQL5 / C++
<span class="keyword">def</span> mmd_loss(y_true, y_pred, kernel_mul=<span class="number">class="num">2.0</span>, kernel_num=<span class="number">class="num">5</span>):
&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"""
&nbsp;&nbsp;&nbsp;&nbsp;MMD loss using Gaussian RBF kernel.
&nbsp;&nbsp;&nbsp;&nbsp;Args:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;y_true: Ground truth samples(shape: [batch_size, dim])
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;y_pred: Predicted samples(shape: [batch_size, dim])
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;kernel_mul: Multiplier for kernel bandwidths
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;kernel_num: Number of kernels to use
&nbsp;&nbsp;&nbsp;&nbsp;Returns:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;MMD loss(scalar)
&nbsp;&nbsp;&nbsp;&nbsp;"""</span>
&nbsp;&nbsp;&nbsp;&nbsp;batch_size = y_true.size(<span class="number">class="num">0</span>)
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment"># Combine real and predicted samples</span>
&nbsp;&nbsp;&nbsp;&nbsp;xx = y_true
&nbsp;&nbsp;&nbsp;&nbsp;yy = y_pred
&nbsp;&nbsp;&nbsp;&nbsp;xy = torch.cat([xx, yy], dim=<span class="number">class="num">0</span>)
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment"># Compute pairwise distances</span>
&nbsp;&nbsp;&nbsp;&nbsp;distances = torch.cdist(xy, xy, p=<span class="number">class="num">2</span>)
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment"># Compute MMD using multiple RBF kernels</span>
&nbsp;&nbsp;&nbsp;&nbsp;loss = <span class="number">class="num">0.0</span>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span> sigma <span class="keyword">in</span> [kernel_mul ** k <span class="keyword">for</span> k <span class="keyword">in</span> <span class="built_in">range</span>(-kernel_num, kernel_num + <span class="number">class="num">1</span>)]:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> sigma == <span class="number">class="num">0</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">continue</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;kernel_val = torch.exp(-distances ** <span class="number">class="num">2</span> / (<span class="number">class="num">2</span> * sigma ** <span class="number">class="num">2</span>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;k_xx = kernel_val[:batch_size, :batch_size]
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;k_yy = kernel_val[batch_size:, batch_size:]
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;k_xy = kernel_val[:batch_size, batch_size:]
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment"># MMD formula: E[k(x,x)] + E[k(y,y)] - class="num">2*E[k(x,y)]</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;loss += (k_xx.mean() + k_yy.mean() - <span class="number">class="num">2</span> * k_xy.mean())
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span> loss / (<span class="number">class="num">2</span> * kernel_num)
<span class="keyword">def</span> compute_loss(predictions, batch):
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment"># Ensure shapes match(squeeze if needed)</span>
&nbsp;&nbsp;&nbsp;&nbsp;pred_states = predictions[<span class="class="type">class="kw">string">&class="macro">#x27;states&class="macro">#x27;</span>].squeeze(-<span class="number">class="num">1</span>)&nbsp;&nbsp;<span class="comment"># [B, class="num">1] → [B]</span>
&nbsp;&nbsp;&nbsp;&nbsp;pred_actions = predictions[<span class="class="type">class="kw">string">&class="macro">#x27;actions&class="macro">#x27;</span>].squeeze(-<span class="number">class="num">1</span>)
&nbsp;&nbsp;&nbsp;&nbsp;pred_rewards = predictions[<span class="class="type">class="kw">string">&class="macro">#x27;rewards&class="macro">#x27;</span>].squeeze(-<span class="number">class="num">1</span>)
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment"># MMD Loss(distributional matching)</span>
&nbsp;&nbsp;&nbsp;&nbsp;mmd_state = mmd_loss(batch[<span class="class="type">class="kw">string">&class="macro">#x27;states&class="macro">#x27;</span>], pred_states)
&nbsp;&nbsp;&nbsp;&nbsp;mmd_action = mmd_loss(batch[<span class="class="type">class="kw">string">&class="macro">#x27;actions&class="macro">#x27;</span>], pred_actions)
&nbsp;&nbsp;&nbsp;&nbsp;mmd_reward = mmd_loss(batch[<span class="class="type">class="kw">string">&class="macro">#x27;rewards&class="macro">#x27;</span>], pred_rewards)
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment"># Combine losses(adjust weights as needed)</span>
&nbsp;&nbsp;&nbsp;&nbsp;total_loss = mmd_state + mmd_action + mmd_reward
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span> {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">&class="macro">#x27;loss&class="macro">#x27;</span>: total_loss,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">&class="macro">#x27;mmd_state&class="macro">#x27;</span>: mmd_state,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">&class="macro">#x27;mmd_action&class="macro">#x27;</span>: mmd_action,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">&class="macro">#x27;mmd_reward&class="macro">#x27;</span>: mmd_reward
&nbsp;&nbsp;&nbsp;&nbsp;}

常见问题

先跑四件套骨架里的数据预处理与编码器部分,用历史 K 线加双指标做输入,确认重构误差稳定后再接推断层。
近邻法对突变形态更敏感、延迟低但易抖;高斯加权平滑强、抗噪好,适合贵金属这种跳空多的品种,参数要分开调。
可以。小布盯盘的 AIGC 已内置这类编码空间诊断,打开对应品种页就能看到真实与生成分布的偏离提示,不用自己跑脚本。
趋势分支看均线斜率顺向开,震荡分支看随机指标极值反手,异常分支只预警不交易,外汇高风险下别硬跟。
回测中 MMD 低于 0.05 且连续 200 根不反弹,生成形态才倾向可信;高于此值大概率过拟合,先降学习率。