使用PSAR、Heiken Ashi和深度学习进行交易·进阶篇
「用 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,用这段代码本地跑一遍,看信号密度是否和你的品种周期匹配。
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 历史数据重跑。
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 能直接编译验证。
<span class="keyword">class="type">int</span> 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; handleRSI = <span class="indicators">iRSI</span>(<span class="functions">Symbol</span>, Timeframe, RSIPeriod, <span class="macro">PRICE_CLOSE</span>); 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>); handlePSAR = <span class="indicators">iSAR</span>(<span class="functions">Symbol</span>, Timeframe, PSARStep, PSARMaximum); handleATR = <span class="indicators">iATR</span>(<span class="functions">Symbol</span>, Timeframe, ATRPeriod); <span class="keyword">if</span>(handleRSI == <span class="macro">INVALID_HANDLE</span> || handleSMA == <span class="macro">INVALID_HANDLE</span> || handlePSAR == <span class="macro">INVALID_HANDLE</span> || handleATR == <span class="macro">INVALID_HANDLE</span>) { <span class="functions">Print</span>(<span class="class="type">class="kw">string">"Error creating indicators"</span>); <span class="keyword">class="kw">return</span>(<span class="macro">INIT_FAILED</span>); } <span class="functions">ArraySetAsSeries</span>(rsiBuffer, <span class="macro">true</span>); <span class="functions">ArraySetAsSeries</span>(smaBuffer, <span class="macro">true</span>); <span class="functions">ArraySetAsSeries</span>(psarBuffer, <span class="macro">true</span>); <span class="functions">ArraySetAsSeries</span>(atrBuffer, <span class="macro">true</span>); <span class="functions">ArrayResize</span>(haOpen, <span class="number">class="num">3</span>); <span class="functions">ArrayResize</span>(haClose, <span class="number">class="num">3</span>); <span class="functions">ArrayResize</span>(haHigh, <span class="number">class="num">3</span>); <span class="functions">ArrayResize</span>(haLow, <span class="number">class="num">3</span>); <span class="functions">ArraySetAsSeries</span>(haOpen, <span class="macro">true</span>); <span class="functions">ArraySetAsSeries</span>(haClose, <span class="macro">true</span>); <span class="functions">ArraySetAsSeries</span>(haHigh, <span class="macro">true</span>); <span class="functions">ArraySetAsSeries</span>(haLow, <span class="macro">true</span>); <span class="functions">IndicatorRelease</span>(handleRSI); <span class="functions">IndicatorRelease</span>(handleSMA); <span class="functions">IndicatorRelease</span>(handlePSAR); <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> ExtHandle=<span class="macro">INVALID_HANDLE</span>; <span class="keyword">class="type">int</span> 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> ExtMin=<span class="number">class="num">0.0</span>; <span class="keyword">class="type">float</span> ExtMax=<span class="number">class="num">0.0</span>; CTrade 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 <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> ExtHandle=<span class="functions">OnnxCreateFromBuffer</span>(ExtModel,<span class="macro">ONNX_DEFAULT</span>); <span class="keyword">if</span>(ExtHandle==<span class="macro">INVALID_HANDLE</span>) { <span class="functions">Print</span>(<span class="class="type">class="kw">string">"OnnxCreateFromBuffer error "</span>,<span class="functions">GetLastError</span>()); <span class="keyword">class="kw">return</span>(<span class="macro">INIT_FAILED</span>); }
「把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 策略测试器用真实点差回测。
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 里建议先开专家日志确认这四行无零返回再放行。
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) {