借助成交量精准洞悉交易动态:超越传统OHLC图表·进阶篇
📊

借助成交量精准洞悉交易动态:超越传统OHLC图表·进阶篇

(2/3)·靠OHLC看盘总慢半拍?把成交量拆成一阶、二阶导数再喂给LSTM,拐点可能更早浮现

实战向 第 2/3 篇
不少交易者把成交量只当副图里的柱线扫一眼,忽略了它变化率和加速度里藏着的早期信号。等价格破位才反应,往往已经错过最优入场区。用微积分视角重新拆成交量,配合轻量神经网络,盘口语言会清楚很多。

持仓台账与当日盈亏的底层维护

这段逻辑解决一个实盘常见痛点:EA 重启后怎么知道当前挂着的单子状态、以及今天到底赚了多少。核心是用一个 openTrades 结构数组在内存里记账,开仓时写 openTime 和 barsOpen=0,平仓则调 RemoveTrade 把对应槽位清零。 RemoveTrade 按 ticket 线性扫描数组,命中后把 ticket、openTime、barsOpen 全部置 0 并 break,避免无谓循环。注意这里没有压缩数组,只是逻辑删除,长时间运行数组尾部会有空洞,但 ArraySize 不变。 GetDailyProfit 是当日盈亏的真实来源。它先用 TimeToString+TIME_DATE 拿到当天 0 点,跨天后重置 dailyProfit 并 HistorySelect 拉取今日起的历史成交,只累加 _Symbol 的 DEAL_PROFIT;再遍历 PositionsTotal 把同品种浮动盈亏加进去。 ApplyTrailingStop 的入口已经暴露了关键字段:openPrice 取 POSITION_PRICE_OPEN,currentPrice 取 POSITION_PRICE_CURRENT,后续追踪止损的距离计算就基于这两者的差。外汇与贵金属杠杆高,追踪止损参数设错可能瞬间扫掉盈利单,建议在 MT5 策略测试器用点差 20 点的 XAUUSD 先验证。

MQL5 / C++
openTrades[i].openTime = TimeCurrent();
openTrades[i].barsOpen = class="num">0;
break;
}
}
}
class="type">void RemoveTrade(class="type">ulong ticket) {
   for(class="type">int i = class="num">0; i < ArraySize(openTrades); i++) {
      if(openTrades[i].ticket == ticket) {
         openTrades[i].ticket = class="num">0;
         openTrades[i].openTime = class="num">0;
         openTrades[i].barsOpen = class="num">0;
         break;
      }
   }
}
class="type">class="kw">double GetDailyProfit() {
   class="type">class="kw">datetime today = StringToTime(TimeToString(TimeCurrent(), TIME_DATE));
   if(lastDayChecked != today) {
      lastDayChecked = today;
      dailyProfit = class="num">0;
      HistorySelect(today, TimeCurrent());
      class="type">int deals = HistoryDealsTotal();
      for(class="type">int i = class="num">0; i < deals; i++) {
         class="type">ulong ticket = HistoryDealGetTicket(i);
         if(ticket > class="num">0) {
            if(HistoryDealGetString(ticket, DEAL_SYMBOL) == _Symbol)
               dailyProfit += HistoryDealGetDouble(ticket, DEAL_PROFIT);
         }
      }
   }
   
   class=class="str">"cmt">// Add floating P/L
   class="type">class="kw">double floatingProfit = class="num">0;
   for(class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) {
      class="type">ulong ticket = PositionGetTicket(i);
      if(ticket > class="num">0 && PositionSelectByTicket(ticket)) {
         if(PositionGetString(POSITION_SYMBOL) == _Symbol)
            floatingProfit += PositionGetDouble(POSITION_PROFIT);
      }
   }
   class="kw">return dailyProfit + floatingProfit;
}
class="type">void ApplyTrailingStop(class="type">class="kw">double point) {
   for(class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) {
      class="type">ulong ticket = PositionGetTicket(i);
      if(ticket <= class="num">0) class="kw">continue;
      if(PositionSelectByTicket(ticket)) {
         class="type">class="kw">double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
         class="type">class="kw">double currentPrice = PositionGetDouble(POSITION_PRICE_CURRENT);

「从持仓止损到 OnTick 调度的实现切片」

下面这段 MT5 代码把移动止损和每根 K 线一次的调度逻辑拼在了一起。先在持仓循环里取出现有 SL 和持仓方向,仅当浮盈点数越过 TrailingStart 才重算止损位,避免无谓的修改请求。 double currentSL = PositionGetDouble(POSITION_SL); long posType = PositionGetInteger(POSITION_TYPE); // Calculate and update trailing stop double profitPoints; if(posType == POSITION_TYPE_BUY) { profitPoints = (currentPrice - openPrice) / point; if(profitPoints >= TrailingStart) { double newSL = openPrice + (profitPoints - TrailingStep) * point;

if(currentSL < newSLcurrentSL == 0) {

trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP)); } } } // Similar logic for SELL positions 逐行拆解:第1行用 PositionGetDouble(POSITION_SL) 读出当前止损价;第2行取持仓类型(买或卖)。profitPoints 是浮盈换算成点的数值。买入仓里,若 profitPoints 达到 TrailingStart(例如设 30 点),newSL 会在开仓价基础上保留 TrailingStep(例如 15 点)的回撤空间。只有当新止损严于原止损或原止损为 0 时才调用 PositionModify,卖仓逻辑对称。 OnTick 里用静态变量 lastBar 记住上一根 K 线时间,iTime(_Symbol, PERIOD_CURRENT, 0) 取到当前柱时间,相等就直接 return,保证一天内每根柱只跑一次主体。若当日累计利润低于 MaxDailyLoss 阈值则平仓并退出,这是硬风控。 void OnTick() { static datetime lastBar = 0; datetime currentBar = iTime(_Symbol, PERIOD_CURRENT, 0); if(lastBar == currentBar) return; lastBar = currentBar; if(GetDailyProfit() < MaxDailyLoss) { CloseAllPositions(); return; } // Update and calculate UpdateTradesInfo(); CheckTimeBasedClose(); if(!CanTrade()) return; // Volume analysis and LSTM prediction // ... volume calculations ... if(UseLSTM && volumePredictor != NULL) { // LSTM prediction logic // ... prediction calculations ... } CalculateDerivatives(); if(consecutiveAccel >= AccelBars) {

if((!UseADX && !UseOBV)ConfirmLongSignal()) {

// Open buy position } } else if(consecutiveAccel <= -AccelBars) {

if(!UseADX && !UseOBVConfirmShortSignal()) {

// Open sell position } } } 开仓判定依赖 consecutiveAccel 与 AccelBars 的比较:连续加速大于等于阈值且过滤条件通过才考虑买,反向则考虑卖。外汇与贵金属杠杆高,这类 EA 在滑点扩大时可能漏单或追错价,上 MT5 策略测试器用 2023 年 XAUUSD 的 M15 数据跑一遍更能看清实际表现。 ADX 模块开头就判 UseADX 开关,关掉直接 return 不占资源。三个缓冲区 adx_buffer、plusdi_buffer、minusdi_buffer 都用 ArraySetAsSeries(..., true) 设成时间序列,最新值落在索引 0,方便后面直接读当前柱。 void CalculateADX() { if(!UseADX) return; double adx_buffer[]; double plusdi_buffer[]; double minusdi_buffer[]; ArraySetAsSeries(adx_buffer, true); ArraySetAsSeries(plusdi_buffer, true); ArraySetAsSeries(minusdi_buffer, true);

MQL5 / C++
class="type">class="kw">double currentSL = PositionGetDouble(POSITION_SL);
class="type">long posType = PositionGetInteger(POSITION_TYPE);
class=class="str">"cmt">// Calculate and update trailing stop
class="type">class="kw">double profitPoints;
if(posType == POSITION_TYPE_BUY) {
   profitPoints = (currentPrice - openPrice) / point;
   if(profitPoints >= TrailingStart) {
      class="type">class="kw">double newSL = openPrice + (profitPoints - TrailingStep) * point;
      if(currentSL < newSL || currentSL == class="num">0) {
         trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
      }
   }
}
class=class="str">"cmt">// Similar logic for SELL positions
}
}
}
class="type">void OnTick() {
   class="kw">static class="type">class="kw">datetime lastBar = class="num">0;
   class="type">class="kw">datetime currentBar = iTime(_Symbol, PERIOD_CURRENT, class="num">0);
   if(lastBar == currentBar) class="kw">return;
   lastBar = currentBar;
   if(GetDailyProfit() < MaxDailyLoss) {
      CloseAllPositions();
      class="kw">return;
   }
   class=class="str">"cmt">// Update and calculate
   UpdateTradesInfo();
   CheckTimeBasedClose();
   if(!CanTrade()) class="kw">return;
   class=class="str">"cmt">// Volume analysis and LSTM prediction
   class=class="str">"cmt">// ... volume calculations ...
   if(UseLSTM && volumePredictor != NULL) {
      class=class="str">"cmt">// LSTM prediction logic
      class=class="str">"cmt">// ... prediction calculations ...
   }
   CalculateDerivatives();
   if(consecutiveAccel >= AccelBars) {
      if((!UseADX && !UseOBV) || ConfirmLongSignal()) {
         class=class="str">"cmt">// Open buy position
      }
   }
   else if(consecutiveAccel <= -AccelBars) {
      if(!UseADX && !UseOBV || ConfirmShortSignal()) {
         class=class="str">"cmt">// Open sell position
      }
   }
}
class="type">void CalculateADX() {
   if(!UseADX) class="kw">return;
   class="type">class="kw">double adx_buffer[];
   class="type">class="kw">double plusdi_buffer[];
   class="type">class="kw">double minusdi_buffer[];
   ArraySetAsSeries(adx_buffer, true);
   ArraySetAsSeries(plusdi_buffer, true);
   ArraySetAsSeries(minusdi_buffer, true);

◍ 把ADX与LSTM权重塞进MT5结构体

在 MT5 里跑 AI 辅助信号,第一步是把指标缓冲和模型参数都装进自定义结构体。下面这段把 ADX 的三条线(主线、+DI、-DI)从指标句柄拷进本地数组,再转存到 adx_main / adx_plus / adx_minus,方便后续做矩阵运算。 CopyBuffer(adx_handle, MAIN_LINE, 0, ArraySize(adx_main), adx_buffer); CopyBuffer(adx_handle, PLUSDI_LINE, 0, ArraySize(adx_plus), plusdi_buffer); CopyBuffer(adx_handle, MINUSDI_LINE, 0, ArraySize(adx_minus), minusdi_buffer); ArrayCopy(adx_main, adx_buffer); ArrayCopy(adx_plus, plusdi_buffer); ArrayCopy(adx_minus, minusdi_buffer); LSTMCell 结构体把遗忘门、输入门、输出门和细胞状态各自的权重矩阵(Matrix2D)与偏置数组分开声明,hidden_size 与 input_size 决定矩阵维度。初始化时用 MathSqrt(2.0/(input_size+hidden_size)) 做 He 缩放,避免深层网络梯度消失——这是 ReLU 类激活常用的经验系数。 double scale = MathSqrt(2.0 / (lstm.input_size + lstm.hidden_size)); lstm.Wf.Init(lstm.hidden_size, lstm.input_size); for(int i=0; i<lstm.hidden_size; i++){ for(int j=0; j<lstm.input_size; j++){ lstm.Wf.Set(i, j, (MathRand()/32768.0 - 0.5) * scale); } } 成交量是价格行为里常被忽视的维度。NormalizeVolumes 先求 historical_volumes 的均值,再准备平方和,典型 Z-score 标准化前半段。外汇与贵金属杠杆高,这类特征若不做归一,LSTM 门控更新可能偏向极端样本,实盘信号失真概率会上升。开 MT5 把上面结构体贴进 EA,先跑通 ADX 拷贝再接权重初始化,能省掉后续大半调试时间。

MQL5 / C++
CopyBuffer(adx_handle, MAIN_LINE, class="num">0, ArraySize(adx_main), adx_buffer);
CopyBuffer(adx_handle, PLUSDI_LINE, class="num">0, ArraySize(adx_plus), plusdi_buffer);
CopyBuffer(adx_handle, MINUSDI_LINE, class="num">0, ArraySize(adx_minus), minusdi_buffer);
ArrayCopy(adx_main, adx_buffer);
ArrayCopy(adx_plus, plusdi_buffer);
ArrayCopy(adx_minus, minusdi_buffer);
}
class="kw">struct Matrix2D {
  class="type">class="kw">double values[];
  class="type">int rows;
  class="type">int cols;
  
  class="type">void Init(class="type">int r, class="type">int c) {
    rows = r;
    cols = c;
    ArrayResize(values, rows * cols);
    ArrayInitialize(values, class="num">0);
  }
  class=class="str">"cmt">// ... other methods
}
class="kw">struct LSTMCell {
  class="type">class="kw">double forget_gate[];
  class="type">class="kw">double input_gate[];
  class="type">class="kw">double cell_state[];
  class="type">class="kw">double output_gate[];
  class="type">class="kw">double hidden_state[];
  
  Matrix2D Wf;  class=class="str">"cmt">// Forget gate weights
  Matrix2D Wi;  class=class="str">"cmt">// Input gate weights
  Matrix2D Wc;  class=class="str">"cmt">// Cell state weights
  Matrix2D Wo;  class=class="str">"cmt">// Output gate weights
  
  class="type">class="kw">double bf[];  class=class="str">"cmt">// Forget gate bias
  class="type">class="kw">double bi[];  class=class="str">"cmt">// Input gate bias
  class="type">class="kw">double bc[];  class=class="str">"cmt">// Cell state bias
  class="type">class="kw">double bo[];  class=class="str">"cmt">// Output gate bias
  
  class="type">int hidden_size;
  class="type">int input_size;
};
class="type">class="kw">double Sigmoid(class="type">class="kw">double x) {
  class="kw">return class="num">1.0 / (class="num">1.0 + MathExp(-x));
}
class="type">class="kw">double Tanh(class="type">class="kw">double x) {
  class="kw">return (MathExp(x) - MathExp(-x)) / (MathExp(x) + MathExp(-x));
}
class="type">void InitializeWeights() {
  class="type">class="kw">double scale = MathSqrt(class="num">2.0 / (lstm.input_size + lstm.hidden_size));
  
  class=class="str">"cmt">// Initialize weight matrices
  lstm.Wf.Init(lstm.hidden_size, lstm.input_size);
  lstm.Wi.Init(lstm.hidden_size, lstm.input_size);
  class=class="str">"cmt">// ... other initializations
  class=class="str">"cmt">// Random initialization with scaling
  for(class="type">int i = class="num">0; i < lstm.hidden_size; i++) {
    for(class="type">int j = class="num">0; j < lstm.input_size; j++) {
      lstm.Wf.Set(i, j, (MathRand() / class="num">32768.0 - class="num">0.5) * scale);
      class=class="str">"cmt">// ... other weight initializations
    }
  }
}
class="type">void NormalizeVolumes() {
  volume_mean = class="num">0;
  class="type">class="kw">double sum_squared = class="num">0;
  
  class=class="str">"cmt">// Calculate mean
  for(class="type">int i = class="num">0; i < ArraySize(historical_volumes); i++) {
    volume_mean += historical_volumes[i];
  }

LSTM 推下一根量的归一与反归一细节

这段收尾代码把前面算好的均值和标准差直接用上了。先求历史成交量的标准差:遍历累加每个样本与均值差的平方,再开方;若标准差为 0 则强制置 1,纯粹是为了避免后面归一化时除零崩掉。 归一化后,historical_volumes 里存的就不再是原始手数,而是 z-score。PredictNextVolume 拿最新一根的 z-score 当输入,跑一遍 LSTM 前向:三个门(遗忘、输入、输出)加候选细胞态都用 Sigmoid / Tanh 算,hidden_state 由输出门乘细胞态双曲正切得到。 最终预测值是所有隐藏单元输出的平均值,再乘回 volume_std 加回 volume_mean 反归一。也就是说,模型在标准化空间里预测,吐出来后再还原成真实量纲——你在 MT5 里改 volume_std 初始值或隐藏层大小,预测的绝对量级会明显漂移。 外汇和贵金属成交量受session与平台撮合影响大,这类预测在高波动时段误差可能放大,仅作仓位参考而非方向依据。

MQL5 / C++
   volume_mean /= ArraySize(historical_volumes);
   
   class=class="str">"cmt">// Calculate standard deviation
   for(class="type">int i = class="num">0; i < ArraySize(historical_volumes); i++) {
      sum_squared += MathPow(historical_volumes[i] - volume_mean, class="num">2);
   }
   volume_std = MathSqrt(sum_squared / ArraySize(historical_volumes));
   
   class=class="str">"cmt">// Normalize volumes
   if(volume_std == class="num">0) volume_std = class="num">1;   class=class="str">"cmt">// Prevent division by zero
   for(class="type">int i = class="num">0; i < ArraySize(historical_volumes); i++) {
      historical_volumes[i] = (historical_volumes[i] - volume_mean) / volume_std;
   }
}
class="type">class="kw">double PredictNextVolume() {
   if(!is_initialized) class="kw">return class="num">0;
   
   class="type">class="kw">double input1 = historical_volumes[ArraySize(historical_volumes)-class="num">1];
   
   class=class="str">"cmt">// LSTM forward pass
   for(class="type">int h = class="num">0; h < lstm.hidden_size; h++) {
      class="type">class="kw">double ft = class="num">0, it = class="num">0, ct = class="num">0, ot = class="num">0;
      
      class=class="str">"cmt">// Calculate gates
      for(class="type">int i = class="num">0; i < lstm.input_size; i++) {
         ft += lstm.Wf.Get(h, i) * input1;
         it += lstm.Wi.Get(h, i) * input1;
         ct += lstm.Wc.Get(h, i) * input1;
         ot += lstm.Wo.Get(h, i) * input1;
      }
      
      class=class="str">"cmt">// Apply gates and calculate states
      lstm.forget_gate[h] = Sigmoid(ft + lstm.bf[h]);
      lstm.input_gate[h] = Sigmoid(it + lstm.bi[h]);
      class="type">class="kw">double c_tilde = Tanh(ct + lstm.bc[h]);
      lstm.cell_state[h] = lstm.forget_gate[h] * lstm.cell_state[h] +
                           lstm.input_gate[h] * c_tilde;
      lstm.output_gate[h] = Sigmoid(ot + lstm.bo[h]);
      lstm.hidden_state[h] = lstm.output_gate[h] * Tanh(lstm.cell_state[h]);
   }
   
   class=class="str">"cmt">// Calculate final prediction
   class="type">class="kw">double prediction = class="num">0;
   for(class="type">int h = class="num">0; h < lstm.hidden_size; h++)
      prediction += lstm.hidden_state[h];
   prediction /= lstm.hidden_size;
   
   class="kw">return prediction * volume_std + volume_mean;   class=class="str">"cmt">// Denormalize
}
把导数计算交给小布盯盘
这些成交量一阶、二阶导数的实时计算和LSTM预测可视化,小布盯盘的AIGC已内置,打开对应品种页即可看到分离窗口里的三个缓冲区,你只管读信号做决策。

常见问题

一阶导数是相邻周期成交量的差分,近似变化率;二阶导数是一阶导的差分,代表加速度。EA中通过指标缓冲区逐根计算并绘制在独立窗口。
不一定。模型输出是概率性前瞻,在算法市中参考价值较高,但仍需结合价格结构和背离验证,外汇贵金属杠杆高、滑点突发行情下可能失效。
可以,小布盯盘品种页已集成分离窗口的三缓冲显示,含导数与LSTM预测,不用自己写EA也能看同类信号。
算法单常在价格显形前就改变成交量结构,纯OHLC滞后于参与度的微妙变化,微积分+ML能提前暴露参与度位移。