使用PSAR、Heiken Ashi和深度学习进行交易·进阶篇
📘

使用PSAR、Heiken Ashi和深度学习进行交易·进阶篇

第 2/3 篇

「用 ONNX 模型跑回测的入场与止损逻辑」

这段 Python 片段把训练好的 ONNX 模型接进回测框架,窗口长度默认 120 根 K 线、初始资金 10000。模型每次用最近 120 个归一化收盘价预测下一价,再反归一化写回 df['predictions'],从第 121 行起才有信号。 多头触发要求收盘价同时高于模型预测值、PSAR 与 SMA,RSI 放宽到小于 60,且连续两根 Heikin-Ashi 收阳、趋势标签为 1。空头对称:收盘价低于预测值/PSAR/SMA,RSI 大于 40,HA 连阴、趋势为 -1。 止损止盈按 ATR 倍数动态挂:多单 SL = 入场价 - 2×ATR、TP = 入场价 + 3×ATR;空单反向。外汇与贵金属杠杆高,ATR 倍数只是概率性缓冲,实盘滑点可能直接打穿。 开 MT5 导出 close/psar/sma/rsi/ha_close/ha_open/trend/atr 到 CSV,用这段代码本地跑一遍,看信号密度是否和你的品种周期匹配。

MQL5 / C++
def backtest(df, model, scaler, window_size=class="num">120, initial_balance=class="num">10000):
    scaled_data = scaler.transform(df[[&class="macro">#x27;close&class="macro">#x27;]])
    predictions = []
    for i in range(window_size, len(scaled_data)):
        X = scaled_data[i-window_size:i].reshape(class="num">1, window_size, class="num">1)
        pred = predict_with_onnx(model, X.astype(np.float32))
        predictions.append(scaler.inverse_transform(pred.reshape(-class="num">1, class="num">1))[class="num">0, class="num">0])
    df[&class="macro">#x27;predictions&class="macro">#x27;] = [np.nan]*window_size + predictions
    df[&class="macro">#x27;position&class="macro">#x27;] = class="num">0
    long_condition = (
        (df[&class="macro">#x27;close&class="macro">#x27;] > df[&class="macro">#x27;predictions&class="macro">#x27;]) &
        (df[&class="macro">#x27;close&class="macro">#x27;] > df[&class="macro">#x27;psar&class="macro">#x27;]) &
        (df[&class="macro">#x27;close&class="macro">#x27;] > df[&class="macro">#x27;sma&class="macro">#x27;]) &
        (df[&class="macro">#x27;rsi&class="macro">#x27;] < class="num">60) &
        (df[&class="macro">#x27;ha_close&class="macro">#x27;] > df[&class="macro">#x27;ha_open&class="macro">#x27;]) &
        (df[&class="macro">#x27;ha_close&class="macro">#x27;].shift(class="num">1) > df[&class="macro">#x27;ha_open&class="macro">#x27;].shift(class="num">1)) &
        (df[&class="macro">#x27;trend&class="macro">#x27;] == class="num">1)
    )
    short_condition = (
        (df[&class="macro">#x27;close&class="macro">#x27;] < df[&class="macro">#x27;predictions&class="macro">#x27;]) &
        (df[&class="macro">#x27;close&class="macro">#x27;] < df[&class="macro">#x27;psar&class="macro">#x27;]) &
        (df[&class="macro">#x27;close&class="macro">#x27;] < df[&class="macro">#x27;sma&class="macro">#x27;]) &
        (df[&class="macro">#x27;rsi&class="macro">#x27;] > class="num">40) &
        (df[&class="macro">#x27;ha_close&class="macro">#x27;] < df[&class="macro">#x27;ha_open&class="macro">#x27;]) &
        (df[&class="macro">#x27;ha_close&class="macro">#x27;].shift(class="num">1) < df[&class="macro">#x27;ha_open&class="macro">#x27;].shift(class="num">1)) &
        (df[&class="macro">#x27;trend&class="macro">#x27;] == -class="num">1)
    )
    df.loc[long_condition, &class="macro">#x27;position&class="macro">#x27;] = class="num">1
    df.loc[short_condition, &class="macro">#x27;position&class="macro">#x27;] = -class="num">1
    sl_atr_multiple = class="num">2
    tp_atr_multiple = class="num">3
    for i in range(window_size, len(df)):
        if df[&class="macro">#x27;position&class="macro">#x27;].iloc[i-class="num">1] != class="num">0:
            entry_price = df[&class="macro">#x27;close&class="macro">#x27;].iloc[i-class="num">1]
            current_atr = df[&class="macro">#x27;atr&class="macro">#x27;].iloc[i-class="num">1]
            if df[&class="macro">#x27;position&class="macro">#x27;].iloc[i-class="num">1] == class="num">1:
                sl_price = entry_price - sl_atr_multiple * current_atr
                tp_price = entry_price + tp_atr_multiple * current_atr

◍ 用 ATR 通道给持仓画止损止盈线

上面这段逻辑处理的是持仓退出:多头时若当根最低价打穿 sl_price 或最高价触及 tp_price,就把 position 置 0 平掉;空头则反过来,最高价破 entry_price + sl_atr_multiple*ATR 或最低价跌破 tp_price 才离场。sl_atr_multiple 与 tp_atr_multiple 是两个可调杠杆,直接决定被扫损还是吃满波动。 退出信号算完之后,代码用 pct_change 算每根收盘价收益,再拿上一根 position 错位相乘得 strategy_returns,避免用未来信号。cumprod 把策略收益滚成资金曲线,initial_balance 乘上去就是账户余额。 一段样本回测跑下来:总收益 1.35%,夏普 0.39,期末余额 10135.02 美元(初始 10000)。外汇与贵金属杠杆高,实盘滑点可能让这类通道策略的夏普进一步走低,参数请先在 MT5 历史数据重跑。

MQL5 / C++
    if df[&class="macro">#x27;low&class="macro">#x27;].iloc[i] < sl_price or df[&class="macro">#x27;high&class="macro">#x27;].iloc[i] > tp_price:
        df.loc[df.index[i], &class="macro">#x27;position&class="macro">#x27;] = class="num">0
    else:  # Posición corta
        sl_price = entry_price + sl_atr_multiple * current_atr
        tp_price = entry_price - tp_atr_multiple * current_atr
        if df[&class="macro">#x27;high&class="macro">#x27;].iloc[i] > sl_price or df[&class="macro">#x27;low&class="macro">#x27;].iloc[i] < tp_price:
            df.loc[df.index[i], &class="macro">#x27;position&class="macro">#x27;] = class="num">0
    
    df[&class="macro">#x27;returns&class="macro">#x27;] = df[&class="macro">#x27;close&class="macro">#x27;].pct_change()
    df[&class="macro">#x27;strategy_returns&class="macro">#x27;] = df[&class="macro">#x27;position&class="macro">#x27;].shift(class="num">1) * df[&class="macro">#x27;returns&class="macro">#x27;]
    
    # Calcular balance
    df[&class="macro">#x27;cumulative_returns&class="macro">#x27;] = (class="num">1 + df[&class="macro">#x27;strategy_returns&class="macro">#x27;]).cumprod()
    df[&class="macro">#x27;balance&class="macro">#x27;] = initial_balance * df[&class="macro">#x27;cumulative_returns&class="macro">#x27;]
    
    class="kw">return df
Retorno total: class="num">1.35%
Ratio de Sharpe: class="num">0.39
Balance final: $class="num">10135.02

把传统指标和ONNX模型塞进同一个EA

这套 EA 只在 EURUSD 的 H6 周期上跑,先把 RSI、SMA、PSAR、ATR 四个常规指标挂上,再用 Heikin Ashi 平滑 K 线滤掉杂波。它的核心不是做圣杯,而是验证「传统技术面 + 深度学习」能不能在外汇市场里抠出一点概率优势——外汇和贵金属本身高杠杆、高波动,任何信号都只是倾向而非确定性。 真正特别的地方是直接把预训练 ONNX 模型加载进 MT5。模型吃最近 120 根归一化行情(宏 SAMPLE_SIZE 写死为 120),输出三分类:0 涨、1 平、2 跌。只有当模型判涨且价格站在 PSAR 和 SMA 上方、RSI 低于 60、HA 收阳,才会触发多单;空单反之。这种多层过滤倾向减少假信号,但不保证盈利。 风控用 ATR 动态算止损止盈,并带追踪止损,随波动自适应。下面这段初始化代码就是指标句柄申请 + HA 数组倒序 + ONNX 模型从资源文件载入的全过程,复制进 MT5 能直接编译验证。

MQL5 / C++
<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;handleRSI, handleSMA, handlePSAR, handleATR;
<span class="keyword">class="type">class="kw">double</span> rsiBuffer[], smaBuffer[], psarBuffer[], atrBuffer[];
<span class="keyword">class="type">class="kw">double</span> haOpen[], haClose[], haHigh[], haLow[];
CTrade trade;
&nbsp;&nbsp; handleRSI = <span class="indicators">iRSI</span>(<span class="functions">Symbol</span>, Timeframe, RSIPeriod, <span class="macro">PRICE_CLOSE</span>);
&nbsp;&nbsp; handleSMA = <span class="indicators">iMA</span>(<span class="functions">Symbol</span>, Timeframe, SMAPeriod, <span class="number">class="num">0</span>, <span class="macro">MODE_SMA</span>, <span class="macro">PRICE_CLOSE</span>);
&nbsp;&nbsp; handlePSAR = <span class="indicators">iSAR</span>(<span class="functions">Symbol</span>, Timeframe, PSARStep, PSARMaximum);
&nbsp;&nbsp; handleATR = <span class="indicators">iATR</span>(<span class="functions">Symbol</span>, Timeframe, ATRPeriod);
&nbsp;&nbsp; <span class="keyword">if</span>(handleRSI == <span class="macro">INVALID_HANDLE</span> || handleSMA == <span class="macro">INVALID_HANDLE</span> ||
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;handlePSAR == <span class="macro">INVALID_HANDLE</span> || handleATR == <span class="macro">INVALID_HANDLE</span>)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">Print</span>(<span class="class="type">class="kw">string">"Error creating indicators"</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="macro">INIT_FAILED</span>);
&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp; <span class="functions">ArraySetAsSeries</span>(rsiBuffer, <span class="macro">true</span>);
&nbsp;&nbsp; <span class="functions">ArraySetAsSeries</span>(smaBuffer, <span class="macro">true</span>);
&nbsp;&nbsp; <span class="functions">ArraySetAsSeries</span>(psarBuffer, <span class="macro">true</span>);
&nbsp;&nbsp; <span class="functions">ArraySetAsSeries</span>(atrBuffer, <span class="macro">true</span>);
&nbsp;&nbsp; <span class="functions">ArrayResize</span>(haOpen, <span class="number">class="num">3</span>);
&nbsp;&nbsp; <span class="functions">ArrayResize</span>(haClose, <span class="number">class="num">3</span>);
&nbsp;&nbsp; <span class="functions">ArrayResize</span>(haHigh, <span class="number">class="num">3</span>);
&nbsp;&nbsp; <span class="functions">ArrayResize</span>(haLow, <span class="number">class="num">3</span>);
&nbsp;&nbsp; <span class="functions">ArraySetAsSeries</span>(haOpen, <span class="macro">true</span>);
&nbsp;&nbsp; <span class="functions">ArraySetAsSeries</span>(haClose, <span class="macro">true</span>);
&nbsp;&nbsp; <span class="functions">ArraySetAsSeries</span>(haHigh, <span class="macro">true</span>);
&nbsp;&nbsp; <span class="functions">ArraySetAsSeries</span>(haLow, <span class="macro">true</span>);
&nbsp;&nbsp; <span class="functions">IndicatorRelease</span>(handleRSI);
&nbsp;&nbsp; <span class="functions">IndicatorRelease</span>(handleSMA);
&nbsp;&nbsp; <span class="functions">IndicatorRelease</span>(handlePSAR);
&nbsp;&nbsp; <span class="functions">IndicatorRelease</span>(handleATR);
<span class="preprocessor">class="macro">#define </span>SAMPLE_SIZE <span class="number">class="num">120</span>
<span class="keyword">class="type">long</span>&nbsp;&nbsp;&nbsp;&nbsp; ExtHandle=<span class="macro">INVALID_HANDLE</span>;
<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ExtPredictedClass=-<span class="number">class="num">1</span>;
<span class="keyword">class="type">class="kw">datetime</span> ExtNextBar=<span class="number">class="num">0</span>;
<span class="keyword">class="type">class="kw">datetime</span> ExtNextDay=<span class="number">class="num">0</span>;
<span class="keyword">class="type">float</span>&nbsp;&nbsp;&nbsp;&nbsp;ExtMin=<span class="number">class="num">0.0</span>;
<span class="keyword">class="type">float</span>&nbsp;&nbsp;&nbsp;&nbsp;ExtMax=<span class="number">class="num">0.0</span>;
CTrade&nbsp;&nbsp; ExtTrade;
<span class="keyword">class="type">int</span> dlsignal=-<span class="number">class="num">1</span>;
<span class="comment">class=class="str">"cmt">//--- price movement prediction</span>
<span class="preprocessor">class="macro">#define </span>PRICE_UP&nbsp;&nbsp; <span class="number">class="num">0</span>
<span class="preprocessor">class="macro">#define </span>PRICE_SAME <span class="number">class="num">1</span>
<span class="preprocessor">class="macro">#define </span>PRICE_DOWN <span class="number">class="num">2</span>
<span class="preprocessor">class="macro">#resource </span><span class="class="type">class="kw">string">"/Files/EURUSD_D1_2024.onnx"</span> <span class="keyword">as</span> <span class="keyword">class="type">uchar</span> ExtModel[]
<span class="comment">class=class="str">"cmt">//--- create a model from class="kw">static buffer</span>
&nbsp;&nbsp; ExtHandle=<span class="functions">OnnxCreateFromBuffer</span>(ExtModel,<span class="macro">ONNX_DEFAULT</span>);
&nbsp;&nbsp; <span class="keyword">if</span>(ExtHandle==<span class="macro">INVALID_HANDLE</span>)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">Print</span>(<span class="class="type">class="kw">string">"OnnxCreateFromBuffer error "</span>,<span class="functions">GetLastError</span>());
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="macro">INIT_FAILED</span>);
&nbsp;&nbsp;&nbsp;&nbsp; }

「把K线塞进ONNX前先归一化」

加载好 ONNX 模型后,输入张量的维度并不全由模型文件给定,必须手动钉死。代码里用 input_shape[] = {1, SAMPLE_SIZE, 1} 显式声明:批次为 1、序列长度为 SAMPLE_SIZE 根 K 线、通道只有收盘价 1 路。若 OnnxSetInputShape 返回失败,直接 INIT_FAILED 退出,避免后续推理在错误维度上裸奔。 每天和每根新 bar 的边界要分开管。TimeCurrent() 越过 ExtNextDay 时重算全周期 ExtMin/ExtMax,并把 ExtNextDay 对齐到 PERIOD_D1 的零点;而 ExtNextBar 用 TimeCurrent() 对 PeriodSeconds() 取模后加一个周期来推进。这样归一化区间按日刷新,推理节奏按 bar 走,互不打架。 真正进模型前,x_norm 用 CopyRates 拉最近 SAMPLE_SIZE 根收盘价。若 ExtMin>=ExtMax 说明当日还没波动,直接把 ExtPredictedClass 置 -1 跳过。否则做 min-max 归一:x_norm-=ExtMin 再除以 (ExtMax-ExtMin),把价格压到 [0,1]。OnnxRun 跑完拿到的 output_data[0] 再反归一化回实际价位:predicted = output[0]*(ExtMax-ExtMin)+ExtMin。 分类逻辑很朴素:用最后一根收盘价 last_close 减 predicted 得 delta,fabs(delta)<=0.00001 判 PRICE_SAME,delta<0 倾向 PRICE_UP,否则为另一类。外汇与贵金属波动受杠杆放大,信号仅代表模型在样本区间的概率倾向,实盘前务必在 MT5 策略测试器用真实点差回测。

MQL5 / C++
class=class="str">"cmt">//--- since not all sizes defined in the input tensor we must set them explicitly
class=class="str">"cmt">//--- first index - batch size, second index - series size, third index - number of series(only Close)
  const class="type">long input_shape[] = {class="num">1,SAMPLE_SIZE,class="num">1};
  if(!OnnxSetInputShape(ExtHandle,ONNX_DEFAULT,input_shape))
    {
      Print("OnnxSetInputShape error ",GetLastError());
      class="kw">return(INIT_FAILED);
    }
class=class="str">"cmt">//--- check new day
  if(TimeCurrent() >= ExtNextDay)
    {
      GetMinMax();
      class=class="str">"cmt">//--- set next day time
      ExtNextDay = TimeCurrent();
      ExtNextDay -= ExtNextDay % PeriodSeconds(PERIOD_D1);
      ExtNextDay += PeriodSeconds(PERIOD_D1);
    }
  class=class="str">"cmt">//--- check new bar
  if(TimeCurrent() < ExtNextBar)
      class="kw">return;

  class=class="str">"cmt">//--- set next bar time
  ExtNextBar = TimeCurrent();
  ExtNextBar -= ExtNextBar % PeriodSeconds();
  ExtNextBar += PeriodSeconds();

  class=class="str">"cmt">//--- check min and max
  class="type">float close = (class="type">float)iClose(_Symbol, _Period, class="num">0);
  if(ExtMin > close)
      ExtMin = close;
  if(ExtMax < close)
      ExtMax = close;
class="type">void PredictPrice(class="type">void)
  {
    class="kw">static vectorf output_data(class="num">1);            class=class="str">"cmt">// vector to get result
    class="kw">static vectorf x_norm(SAMPLE_SIZE);       class=class="str">"cmt">// vector for prices normalize
class=class="str">"cmt">//--- check for normalization possibility
    if(ExtMin>=ExtMax)
     {
       Print("ExtMin>=ExtMax");
       ExtPredictedClass=-class="num">1;
       class="kw">return;
     }
class=class="str">"cmt">//--- request last bars
    if(!x_norm.CopyRates(_Symbol,_Period,COPY_RATES_CLOSE,class="num">1,SAMPLE_SIZE))
     {
       Print("CopyRates ",x_norm.Size());
       ExtPredictedClass=-class="num">1;
       class="kw">return;
     }
    class="type">float last_close=x_norm[SAMPLE_SIZE-class="num">1];
class=class="str">"cmt">//--- normalize prices
    x_norm-=ExtMin;
    x_norm/=(ExtMax-ExtMin);
class=class="str">"cmt">//--- run the inference
    if(!OnnxRun(ExtHandle,ONNX_NO_CONVERSION,x_norm,output_data))
     {
       Print("OnnxRun");
       ExtPredictedClass=-class="num">1;
       class="kw">return;
     }
class=class="str">"cmt">//--- denormalize the price from the output value
    class="type">float predicted=output_data[class="num">0]*(ExtMax-ExtMin)+ExtMin;
class=class="str">"cmt">//--- classify predicted price movement
    class="type">float delta=last_close-predicted;
    if(fabs(delta)<=class="num">0.00001)
        ExtPredictedClass=PRICE_SAME;
    else
     {
       if(delta<class="num">0)
          ExtPredictedClass=PRICE_UP;
       else

◍ 把多指标缓冲和改良K线塞进同一帧

这段逻辑把日线收盘极值、改良K线与三个指标缓冲一次性拉进当前计算帧。GetMinMax 用 vectorf 从 D1 周期拷贝 SAMPLE_SIZE 根收盘价,直接取 Min/Max 喂给全局变量,作为后续归一化的边界参考。 CalculateHeikinAshi 只取最近 3 根 K 线,先 ArraySetAsSeries 把数组倒序,再 MathMin 对齐四个 Copy* 函数的返回长度;不足 3 根就 Print 报错退出,避免脏数据进模型。HA 收盘价用 (O+H+L+C)/4,开盘价首根取 (O+C)/2,其后每根继承前一根 HA 开收均值,高低则取真实高低与 HA 开收的最大最小包络。 指标侧用 CopyBuffer 把 RSI、SMA、PSAR、ATR 各取 3 根缓冲,任一返回值 <=0 就中断——外汇与贵金属杠杆高,缓冲失效时硬跑模型会把信号概率带偏,MT5 里建议先开专家日志确认这四行无零返回再放行。

MQL5 / C++
  ExtPredictedClass=PRICE_DOWN;
   }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Gets Min and Max values                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void GetMinMax(class="type">void)
  {
   vectorf close;
   close.CopyRates(_Symbol,PERIOD_D1,COPY_RATES_CLOSE,class="num">0,SAMPLE_SIZE);
   ExtMin=close.Min();
   ExtMax=close.Max();
  }
class="type">void CalculateHeikinAshi()
{
   class="type">class="kw">double close[], open[], high[], low[];
   ArraySetAsSeries(close, true);
   ArraySetAsSeries(open, true);
   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low, true);
   class="type">int copied = CopyClose(Symbol(), Timeframe, class="num">0, class="num">3, close);
   copied = MathMin(copied, CopyOpen(Symbol(), Timeframe, class="num">0, class="num">3, open));
   copied = MathMin(copied, CopyHigh(Symbol(), Timeframe, class="num">0, class="num">3, high));
   copied = MathMin(copied, CopyLow(Symbol(), Timeframe, class="num">0, class="num">3, low));
   if(copied < class="num">3)
   {
      Print("Not enough data for Heikin Ashi calculation");
      class="kw">return;
   }
   class=class="str">"cmt">// Calculate Heikin Ashi values for the last class="num">3 candles
   for(class="type">int i = class="num">2; i >= class="num">0; i--)
   {
      haClose[i] = (open[i] + high[i] + low[i] + close[i]) / class="num">4;
      
      if(i == class="num">2)
      {
         haOpen[i] = (open[i] + close[i]) / class="num">2;
      }
      else
      {
         haOpen[i] = (haOpen[i+class="num">1] + haClose[i+class="num">1]) / class="num">2;
      }
      
      haHigh[i] = MathMax(high[i], MathMax(haOpen[i], haClose[i]));
      haLow[i] = MathMin(low[i], MathMin(haOpen[i], haClose[i]));
   }
   class=class="str">"cmt">// Debug print
   Print("Heikin Ashi values:");
   for(class="type">int i = class="num">0; i < class="num">3; i++)
   {
      Print("Candle ", i, ": Open=", haOpen[i], " High=", haHigh[i], " Low=", haLow[i], " Close=", haClose[i]);
   }
}
   class=class="str">"cmt">// Copy indicator data
   if(CopyBuffer(handleRSI, class="num">0, class="num">0, class="num">3, rsiBuffer) <= class="num">0 ||
      CopyBuffer(handleSMA, class="num">0, class="num">0, class="num">3, smaBuffer) <= class="num">0 ||
      CopyBuffer(handlePSAR, class="num">0, class="num">0, class="num">3, psarBuffer) <= class="num">0 ||
      CopyBuffer(handleATR, class="num">0, class="num">0, class="num">3, atrBuffer) <= class="num">0)
   {

常见问题

不会明显卡顿,关键是把多指标缓冲和改良 K 线塞进同一帧再送模型,避免重复计算。
用近期 ATR 倍数拉出通道,止损放通道外缘,止盈看通道反向突破;外汇贵金属波动大,务必先开模拟验证。
小布可读取你的品种页,把指标冲突与模型输入维度异常标红,省去你手动查缓冲的麻烦。
价格量纲不一会让模型输出漂移,回测信号可能全反;先减均值除标准差再拼帧。
不够,建议叠加 ATR 通道做硬止损,PSAR 只做趋势退出参考,能降尾部风险。