基于Python和MQL5的特征工程(第二部分):价格角度(基础篇)
「模型对特征变换的敏感度」
机器学习模型对外汇与贵金属这类高波动市场的数据变换极其敏感,输入与目标之间的映射关系一旦被重表达,误差水平就会随之漂移。 从原始行情里能造出的新特征没有上限,但每一次转换都会直接改写回测里的误差曲线;同一套变换丢进不同模型架构,结果也可能天差地别。 本篇不谈收益,只帮你在 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 更现实。
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 上跑一遍就能验证。
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级数据上重跑一遍相关性再定。
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)