借助成交量精准洞悉交易动态:超越传统OHLC图表·进阶篇
(2/3)·靠OHLC看盘总慢半拍?把成交量拆成一阶、二阶导数再喂给LSTM,拐点可能更早浮现
持仓台账与当日盈亏的底层维护
这段逻辑解决一个实盘常见痛点: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 先验证。
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 < newSL | currentSL == 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 && !UseOBV | ConfirmShortSignal()) { |
|---|
// 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);
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 拷贝再接权重初始化,能省掉后续大半调试时间。
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与平台撮合影响大,这类预测在高波动时段误差可能放大,仅作仓位参考而非方向依据。
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 }