基于Python和MQL5的特征工程(第二部分):价格角度(基础篇)
📘

基于Python和MQL5的特征工程(第二部分):价格角度(基础篇)

第 1/3 篇

「模型对特征变换的敏感度」

机器学习模型对外汇与贵金属这类高波动市场的数据变换极其敏感,输入与目标之间的映射关系一旦被重表达,误差水平就会随之漂移。 从原始行情里能造出的新特征没有上限,但每一次转换都会直接改写回测里的误差曲线;同一套变换丢进不同模型架构,结果也可能天差地别。 本篇不谈收益,只帮你在 MT5 里把误差逼向 0 附近:你会看到哪些特征工程手法对特定模型更友好,以及为什么不能拿一种变换通吃所有架构。 外汇与贵金属杠杆交易高风险,特征再漂亮也只是概率层面的辅助,实盘前务必用历史数据自测。

◍ 用开盘收盘差替掉时间轴算斜率

很多人在论坛里问怎么算价格水平变化形成的角度,直觉上跌势出负角、涨势出正角,但真写进 EA 就卡壳。趋势线本来靠连两个或三个极值点画出来,突破上沿算强势、破下沿算弱势,可它太主观——交易员随手一拖就能让线迎合自己的偏见。 于是有人想用「价格差 ÷ 时间差」求斜率,把趋势线客观化。但这模型在 MT5 里有个硬伤:周末市场关闭,终端不记录这段流逝时间,得自己推断。若周末跳空,简单除法算出的斜率会偏高,而且同一段数据,算不算周末能得出两种系数,根本不确定。 换个思路,用开盘价差当 x 轴、收盘价差当 y 轴,斜率 = 收盘差 ÷ 开盘差。这量度的是收盘价对开盘价变化的敏感度:绝对值 >1,说明开盘大动收盘没跟;绝对值 <1,开盘小动收盘反应大;系数为负,两者常反向走。外汇和贵金属杠杆高、跳空频繁,这套对周末不敏感,但新麻烦来了。 它对十字星 K 线极敏感。开盘收盘几乎重合时,分母趋近 0,轻则结果算出 0 或无穷大,重则 EA 运行时直接除零报错。图 1 那组十字星就是典型陷阱,写代码前得先想好过滤逻辑。

用万行M1数据试了12个AI模型

我们拉取了 USDZAR 的 10,000 行 M1 数据,通过 MQL5 脚本从 MT5 终端直接落地。先按既定公式算斜率,角度用 arctan 反三角函数得出,但斜率量和报价本身的相关性很低,说明裸斜率对价格方向几乎没解释力。 随后用三组输入训了 12 种 AI 模型预测 USDZAR 后续汇率:纯 OHLC 报价、价格角度+斜率、以及两者合并。结果最简单的线性回归吃 OHLC 就跑赢其他,而且换到合并组精度没掉;纯角度斜率组在所有模型上都没压过纯报价组。 有意思的是,基于新特征,K近邻(KNeighbors)性能抬了 20%,仅两个模型在喂全量数据时达到最优。我们把 KNN 参数调到位、没过拟合训练集,最终导成 ONNX 塞进 AI 驱动的 EA 里。外汇与贵金属高杠杆,这类实证结论只代表样本内倾向,实盘须自行回测验证。

「把行情拖出来存成 CSV」

想做价格行为统计,第一步是拿到干净的历史 tick 级或棒线级数据。下面这段 MT5 脚本能直接把当前图表品种的历史 OHLC 导出为逗号分隔的 CSV,你只要把脚本拖到任意货币对或 XAUUSD 图表上,它就会按当前周期写文件。 默认参数 size = 100000,意味着脚本会尝试抓取最多 10 万根 K 线——对主流外汇品种日线来说远超所需,对 M1 而言可能接近终端本地历史上限,实操时按自己样本量改小更稳。 文件命名用了 Symbol() 动态拼接,比如拖在 EURUSD 上就生成「Market Data EURUSD.csv」,放在 MT5 的 Files 目录下,用 Excel 或 Python 直接读。外汇与贵金属杠杆高、点差跳空频繁,导出的历史不代表未来波动,回测结论只作概率参考。 别把默认十万根当必须 脚本里 size 写死 100000 只是示例。M1 上十万根约 70 个交易日,日线上则覆盖四百多年——明显超出多数经纪商历史深度,强拉会返回空值或截断,按品种周期手动设 5000~20000 更现实。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  ProjectName |
class=class="str">"cmt">//|                                                 Copyright class="num">2020, CompanyName |
class=class="str">"cmt">//|                                                    http://www.companyname.net |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Gamuchirai Zororo Ndawana"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property script_show_inputs
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script Inputs                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
input class="type">int size = class="num">100000; class=class="str">"cmt">//How much data should we fetch?

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| On start function                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- File name
   class="type">class="kw">string file_name = "Market Data " + Symbol() +".csv";
class=class="str">"cmt">//--- Write to file
   class="type">int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,",");
   for(class="type">int i= size;i>=class="num">0;i--)
     {
      if(i == size)
        {
         FileWrite(file_handle,"Time","Open","High","Low","Close");
        }
      else
        {
         FileWrite(file_handle,iTime(Symbol(),PERIOD_CURRENT,i),
                                 iOpen(Symbol(),PERIOD_CURRENT,i),
                                 iHigh(Symbol(),PERIOD_CURRENT,i),
                                 iLow(Symbol(),PERIOD_CURRENT,i),
                                 iClose(Symbol(),PERIOD_CURRENT,i)
                                 );
        }
     }
class=class="str">"cmt">//--- Close the file
   FileClose(file_handle);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 用角度刻画价格变化先看数据长相

把 USDZAR 的日线 csv 读进 pandas 后,第一件要命的事是数据顺序是反的,必须用 data[::-1] 翻过来才能按时间正序算斜率。预测窗口先钉死在 look_ahead=20 根 K 线,也就是拿前 20 根的开收关系去推后面一段的走向。 斜率本身用 (Close - Close.shift(20)) / (Open - Open.shift(20)) 算,再套 np.arctan 转成角度。这里有个坑:当某段开盘价没动时,分母为零,斜率直接变成 inf,图上看就是一条水平线配红色基准线,这类记录得单独拎出来瞧。 当前版本懒得填缺失值,直接 dropna 把所有 NaN 和 inf 相关行删掉,再 reset_index。角度画出来基本在 0 附近晃,离 0 越远代表价格变动越陡,机器勉强有了个尺度感。 噪声怎么估?看斜率变小但同段收盘价反而涨的次数——理想特征该跟价格同涨同跌,实测却只有一半时间同步。代码里那句 1 - (…) 算的是:斜率下降且未来收盘上涨的占比,用 1 减完得到 0.51,意味着知道前面斜率往上走,也仅有约一半概率押中后面价格真往上。外汇与贵金属杠杆高,这种弱相关拿来单独开仓风险极大,MT5 上跑一遍就能验证。

MQL5 / C++
class="kw">import numpy as np
class="kw">import pandas as pd
class="kw">import seaborn as sns
class="kw">import matplotlib.pyplot as plt
class="macro">#Read in the data
data = pd.read_csv("Market Data USDZAR.csv")
class="macro">#The data is in reverse order, correct that
data = data[::-class="num">1]
class="macro">#Define the forecast horizon
look_ahead = class="num">20
class="macro">#Calculate the angle formed by the changes in price, class="kw">using a ratio of high and low price.
class="macro">#Then calculate arctan to realize the angle formed by the changes in pirce
data["Slope"] =  (data["Close"] - data["Close"].shift(look_ahead))/(data["Open"] - data["Open"].shift(look_ahead))
data["Angle"] =  np.arctan(data["Slope"])
data.describe()
data.loc[data["Slope"] == np.inf]
pt = class="num">1807
y = data.loc[pt,"Open"]
plt.plot(data.loc[(pt - look_ahead):pt,"Open"])
plt.axhline(y=y,class="type">class="kw">color="red")
plt.xlabel("Time")
plt.ylabel("USDZAR Open Price")
plt.title("A slope of INF means the price has not changed")
data.dropna(inplace=True)
data.reset_index(drop=True,inplace=True)
data.loc[:class="num">100,"Angle"].plot()
class="macro">#How clean are the signals generated?
class="num">1 - (data.loc[(data["Slope"] < data["Slope"].shift(-look_ahead)) & (data["Close"] > data["Close"].shift(-look_ahead))].shape[class="num">0] / data.loc[(data["Slope"] < data["Slope"].shift(-look_ahead))].shape[class="num">0])

角度特征在EDA里露了底

先把输入输出钉死:OHLC四价加一个Angle作为触发类输入,目标变量直接用Close向后移位look_ahead根K线来定义。再补两个类别标签——若当前收盘处在过去20根上涨走势里标1否则0,角度增减也用同一套二元标识,方便后面看模型能不能学出映射。 相关性热图先泼了冷水。之前估角度噪声时就发现价格和算出来的角度只有约50%时间同向,所以heatmap里Angle跟任何价格特征相关系数都低并不意外。散点图把Close放y轴、Angle放x轴,涨跌两类蓝点绿点大面积重叠,线性可分性基本为零。 换非线性视角看,Slope对Angle的散点呈明显弯曲两端分布,中间没有清晰分界带隔开多空,预测未来价时这套变换可能反而拖后腿。3D散点(Slope-Angle-Close着色Bull Bear)也没冒出纯涨或纯跌的簇,说明信号噪声肉眼估约51%不是开玩笑。 不过小提琴图留了口气:按Angle Up Down分组的箱线图,白线标出的均值在两类间有细微错位。人觉得微不足道,但机器学习模型对分布偏移敏感,这点差异够它啃。外汇和贵金属波动大、噪声高,真拿去训练前建议在MT5导出的tick级数据上重跑一遍相关性再定。

MQL5 / C++
class="macro">#Define our inputs and target
ohlc_inputs      = ["Open","High","Low","Close"]
trig_inputs      = ["Angle"]
all_inputs       = ohlc_inputs + trig_inputs
cv_inputs        = [ohlc_inputs,trig_inputs,all_inputs]
target           = "Target"
class="macro">#Define the target
data["Target"] = data["Close"].shift(-look_ahead)
class="macro">#Add a few labels
data["Bull Bear"] = np.nan
data["Angle Up Down"] = np.nan
data.loc[data["Close"] > data["Close"].shift(look_ahead), "Bull Bear"] = class="num">0
data.loc[data["Angle"] > data["Angle"].shift(look_ahead),"Angle Up Down"] = class="num">0
data.loc[data["Close"] < data["Close"].shift(look_ahead), "Bull Bear"] = class="num">1
data.loc[data["Angle"] < data["Angle"].shift(look_ahead),"Angle Up Down"] = class="num">1
data.dropna(inplace=True)
data.reset_index(drop=True,inplace=True)
data
class="macro">#Let&class="macro">#x27;s analyze the correlation levels
sns.heatmap(data.loc[:,all_inputs].corr(),annot=True)
sns.scatterplot(data=data,y="Close",x="Angle",hue="Bull Bear")
sns.scatterplot(data=data,x="Angle",y="Slope",hue="Bull Bear")
sns.swarmplot(data=data,x="Angle Up Down",y="Close",hue="Bull Bear")
class="macro">#Define the 3D Plot
fig = plt.figure(figsize=(class="num">7,class="num">7))
ax = plt.axes(projection="3d")
ax.scatter(data["Slope"],data["Angle"],data["Close"],c=data["Bull Bear"])
ax.set_xlabel("Slope")
ax.set_ylabel("Angle")
ax.set_zlabel("Close")
sns.violinplot(data=data,x="Angle Up Down",y="Close",hue="Bull Bear",split=True)

常见问题

先把每根K线的开收差算出来,按差值的累计和当横轴、价格当纵轴重画走势,再用最小二乘求斜率,能避开时间不均带来的伪信号。
实测12个模型里树类模型对线性变换不敏感,神经网络类对标准化和角度编码反应明显,调特征前先看模型类型再动手。
可以,小布能按你选的品种和周期把Tick或K线拖成CSV,并直接给出开收差、角度等基础特征,省去手写导出脚本。
常会暴露异常陡角集中在数据头部、以及横盘段角度噪声过大,前者提示换帧或去极值,后者建议加滑窗平滑。
不行,先跑一遍分布和分位看长相,确认角度均值和尾部再写特征,否则容易把偶发尖角当规律喂给模型。