在外汇数据分析中使用关联规则(基础篇)
用关联规则拆解外汇品种联动
关联规则(Association Rules)原本是零售篮分析里的老工具,核心是衡量「买 A 的人有多大概率同时买 B」。搬到 MT5 外汇数据上,思路一样:把每段 K 线或每个时段的品种涨跌状态当成「购物项」,就能挖出 EURUSD 与 XAUUSD 在特定时段同涨同跌的隐性规律。 这套做法对外汇和贵金属交易者最直接的价值,是提前识别「避险共振」或「美元系联动」——比如美盘开盘前后,若历史数据里 GBPUSD 下跌伴随 XAUUSD 上涨的置信度长期高于 70%,它就成了一个可验证的偏置信号。外汇与贵金属杠杆高、跳空频繁,任何规律都只是概率倾向,不能当确定性依据。 在 MT5 里跑关联规则不需要自己写挖掘算法底层的 Apriori,关键是先把 Tick 或 M1 数据按时间桶切成事务集。下面这段 MQL5 片段演示了如何把一个交易日的涨跌标记成 0/1 事务并写入文件,供后续 Python 或 R 做规则提取。
class="type">void OnStart() { class="type">int handle = FileOpen("txn.csv", FILE_WRITE|FILE_CSV, ";"); class=class="str">"cmt">// 打开事务文件准备写入 for(class="type">int i=class="num">0; i<class="num">100; i++) class=class="str">"cmt">// 取最近100根M1棒 { class="type">class="kw">double open = iOpen("EURUSD", PERIOD_M1, i); class=class="str">"cmt">// 取第i根开盘价 class="type">class="kw">double close = iClose("EURUSD", PERIOD_M1, i); class=class="str">"cmt">// 取第i根收盘价 class="type">int up = (close > open) ? class="num">1 : class="num">0; class=class="str">"cmt">// 涨标记1,跌标记0 FileWrite(handle, TimeToString(iTime("EURUSD",PERIOD_M1,i)), up); class=class="str">"cmt">// 写入时间戳与涨跌标记 } FileClose(handle); class=class="str">"cmt">// 关闭文件 }
「把零售分析里的关联规则搬进外汇」
经典技术形态在跨品种联动上经常失灵,市场里真正重复的往往是「A 品种先动、B 品种后跟」这类隐性组合。关联规则本是用来挖超市购物篮的:买啤酒的人多大概率顺手拿尿布。放到 EURUSD 与 USDJPY 上,问题就变成——EURUSD 上涨波段后,USDJPY 下跌的出现频率是否稳定高于随机水平。 做法不复杂:用 MQL5 把历史报价和指标序列导出来,丢进 Python 做频繁项集与置信度计算,筛出置信度够高的前因后果组合,再回写成 MT5 信号。从实操看,MQL5 管数据抽取和订单执行,Python 管挖掘,这套组合在外汇高频样本上比纯 MQ 脚本省力得多。 要注意,外汇和贵金属杠杆高、滑点跳空频繁,哪怕关联置信度到 0.7,也只代表历史样本里的倾向,不等于下一根 K 线必然重演。先开 MT5 把两组的 1 小时收盘价拉一年,算一次滞后相关系数,比直接信任何现成结论都靠谱。
◍ 用脚本把两年 H1 外汇裸数据拽出来
要做价格行为层面的统计,先得有干净的历史序列。这里以 2022 整年到 2023 年末(起止 2022.01.01–2024.01.01)四个主要货币对 EURUSD、GBPUSD、USDJPY、USDCHF 的 H1 棒线为基底,时间窗覆盖约 2 年、每对约 17000+ 根小时 K,足够跑分布与波动刻画。 下面这段 MT5 脚本直接把上述范围按品种写出 CSV,表头含 DateTime/Open/High/Low/Close/Volume,复制进 MetaEditor 编译挂到图表就能在终端 Files 目录拿到文件。 别把终端时区当经纪商时区。CopyRates 拉回来的 time 是服务器时间,跨平台比对前先确认经纪商 GMT 偏移,否则你看到的‘亚盘开盘’可能错位 2~3 小时。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Dataset.mq5 | class=class="str">"cmt">//| Copyright class="num">2024, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class="type">class="kw">string pairs[] = {"EURUSD", "GBPUSD", "USDJPY", "USDCHF"}; class=class="str">"cmt">// 定义要导出的四个货币对名称数组 class="type">class="kw">datetime startTime = D&class="macro">#x27;class="num">2022.01.class="num">01 class="num">00:class="num">00&class="macro">#x27;; class=class="str">"cmt">// 数据起点:2022年1月1日0点 class="type">class="kw">datetime endTime = D&class="macro">#x27;class="num">2024.01.class="num">01 class="num">00:class="num">00&class="macro">#x27;; class=class="str">"cmt">// 数据终点:2024年1月1日0点(不含) for(class="type">int i=class="num">0; i<ArraySize(pairs); i++) class=class="str">"cmt">// 遍历每个货币对 { class="type">class="kw">string filename = pairs[i] + "_H1.csv"; class=class="str">"cmt">// 拼接文件名,如 EURUSD_H1.csv class="type">int fileHandle = FileOpen(filename, FILE_WRITE|FILE_CSV); class=class="str">"cmt">// 以写模式+CSV格式打开文件 if(fileHandle != INVALID_HANDLE) class=class="str">"cmt">// 确认文件句柄有效 { class=class="str">"cmt">// Set headers FileWrite(fileHandle, "DateTime", "Open", "High", "Low", "Close", "Volume"); class=class="str">"cmt">// 写入CSV表头 class="type">MqlRates rates[]; class=class="str">"cmt">// 声明汇率结构体数组 ArraySetAsSeries(rates, true); class=class="str">"cmt">// 设为时间序列(索引0为最新) class="type">int copied = CopyRates(pairs[i], PERIOD_H1, startTime, endTime, rates); class=class="str">"cmt">// 复制H1数据到数组,返回根数 for(class="type">int j=copied-class="num">1; j>=class="num">0; j--) class=class="str">"cmt">// 从最早一根写到最新一根 { FileWrite(fileHandle, class=class="str">"cmt">// 将每根K的字段写入CSV TimeToString(rates[j].time), class=class="str">"cmt">// 时间转字符串 DoubleToString(rates[j].open, class="num">5), class=class="str">"cmt">// 开盘价,保留5位小数 DoubleToString(rates[j].high, class="num">5), class=class="str">"cmt">// 最高价,保留5位小数
把K线字段落盘成可读字符串
上面这段收尾代码负责把遍历到的每根K线低、收价与 tick 成交量写成文本,并关闭文件句柄。注意 DoubleToString 第二个参数写死为 5,意味着无论品种报价精度如何,输出都固定保留 5 位小数;做黄金(XAUUSD)这类 2 位报价品种时,落盘数据会补零到 5 位,肉眼校验时别误判精度。 IntegerToString(rates[j].tick_volume) 直接转成字符串写入,不补位。若你只想存真实成交笔数而非 tick 数,这里要换成 Volume 相关字段,否则回测和实盘导出的“量”含义不同。 FileClose(fileHandle) 放在内层 if 成功后执行,说明文件在整个品种循环里只开一次、写完即关;若前面 Open 失败直接跳到外层,不会触发关闭,因此长时间跑多品种脚本时要确认句柄泄露风险。外汇与贵金属波动剧烈,导出数据仅作本地分析,实盘使用前请在 MT5 策略测试器先跑一轮验证字段对齐。
DoubleToString(rates[j].low, class="num">5), DoubleToString(rates[j].close, class="num">5), IntegerToString(rates[j].tick_volume) ); } FileClose(fileHandle); } } } class=class="str">"cmt">//+------------------------------------------------------------------+
「用 Python 把多币种 H1 数据洗成统一特征矩阵」
做跨品种关联分析前,先把 EURUSD、GBPUSD、USDJPY、USDCHF 四个直盘小时的 CSV 读进来统一处理。原始文件按 utf-16 编码、制表符分隔,列名强制定为 DateTime、Open、High、Low、Close、Volume,且 MT5 导出的文件常在中间夹带重复表头行,必须滤掉。 加载后真正的重头戏是指标与离散化。SMA(50) 用作动态支撑阻力参考,RSI(14) 判超买超卖,MACD 抓动量反转,布林带看波动扩张收缩;连续数值不能直送模型,要按价格相对均值位置切成粗类别——分太粗会吞掉细节,分太细又让模式难以收敛。 K线形态按实体极小认定十字星,按极端波动认定长阳/长阴,这样能把犹豫段和冲动段分开标。最后所有货币对并到同一时间轴的一个数组里,才可能观察一个对的趋势怎么撬动另一个对的波动。外汇与贵金属属高风险品种,此类统计关联只作概率参考,不构成方向承诺。 下面这段是加载函数的核心,注意它把 'DateTime' 字符串行剔除后再转 datetime 索引,并在读入时就把 OHLC 强制转数值、丢弃 NaN:
class="kw">import pandas as pd class="kw">import numpy as np from class="type">class="kw">datetime class="kw">import class="type">class="kw">datetime class="kw">import os class="kw">import warnings warnings.filterwarnings(&class="macro">#x27;ignore&class="macro">#x27;) class ForexDataProcessor: def __init__(self): self.pairs = ["EURUSD", "GBPUSD", "USDJPY", "USDCHF"] self.data = {} self.processed_data = {} def load_data(self): """Load data for all currency pairs""" success = True for pair in self.pairs: filename = f"{pair}_H1.csv" try: df = pd.read_csv(filename, encoding=&class="macro">#x27;utf-class="num">16&class="macro">#x27;, sep=&class="macro">#x27;\t&class="macro">#x27;, names=[&class="macro">#x27;DateTime&class="macro">#x27;, &class="macro">#x27;Open&class="macro">#x27;, &class="macro">#x27;High&class="macro">#x27;, &class="macro">#x27;Low&class="macro">#x27;, &class="macro">#x27;Close&class="macro">#x27;, &class="macro">#x27;Volume&class="macro">#x27;]) # Remove lines with duplicate headers df = df[df[&class="macro">#x27;DateTime&class="macro">#x27;] != &class="macro">#x27;DateTime&class="macro">#x27;] # Convert data types df[&class="macro">#x27;DateTime&class="macro">#x27;] = pd.to_datetime(df[&class="macro">#x27;DateTime&class="macro">#x27;], format=&class="macro">#x27;%Y.%m.%d %H:%M&class="macro">#x27;) for col in [&class="macro">#x27;Open&class="macro">#x27;, &class="macro">#x27;High&class="macro">#x27;, &class="macro">#x27;Low&class="macro">#x27;, &class="macro">#x27;Close&class="macro">#x27;]: df[col] = pd.to_numeric(df[col], errors=&class="macro">#x27;coerce&class="macro">#x27;) df[&class="macro">#x27;Volume&class="macro">#x27;] = pd.to_numeric(df[&class="macro">#x27;Volume&class="macro">#x27;], errors=&class="macro">#x27;coerce&class="macro">#x27;) # Remove NaN strings df = df.dropna() df.set_index(&class="macro">#x27;DateTime&class="macro">#x27;, inplace=True) self.data[pair] = df print(f"Loaded {pair} data successfully. Shape: {df.shape}")
◍ 指标计算与分位容错的实现细节
把多周期均线一次性铺开,比在 MT5 里手动画指标更高效。下面这段 Python 逻辑用循环批量生成 SMA 与 EMA:SMA 覆盖 5/10/20/50/200,EMA 覆盖 5/10/20/50,直接挂到数据副本上,回测时调一个周期数字就能增删列。 波动率项用了 20 期收益标准差乘以根号 20,相当于把日频波动年化缩放;若你拿 MT5 的 H1 数据跑,这个缩放系数要按周期重算,否则数值会偏小。 qcut 分位切割有个坑:当某品种.unique() 数小于等于分位数 q 时,pd.qcut 会直接抛错。代码里用 duplicates='drop' 兜底,仍失败就退到 np.percentile 手动算 20/40/60/80 四个分位点再用 pd.cut 切。外汇与贵金属波动跳空多,小样本下 unique 值偏少是常态,这套容错能避免脚本半夜挂掉。 数据加载失败也在函数内捕获并打印具体 pair 和异常,返回 success=False 让主流程跳过该品种。实盘前拿 EURUSD 的 2020 年数据跑一遍 calculate_indicators,看 SMA_200 前 199 行是否为 NaN 即可验证逻辑通顺。
except Exception as e:
print(f"Error loading {pair} data: {str(e)}")
success = False
class="kw">return success
def safe_qcut(self, series, q, labels):
"""Safe quantization with error handling"""
try:
if series.nunique() <= q:
# If there are fewer unique values than quantiles, use regular categorization
class="kw">return pd.qcut(series, q=q, labels=labels, duplicates=&class="macro">#x27;drop&class="macro">#x27;)
class="kw">return pd.qcut(series, q=q, labels=labels)
except Exception as e:
print(f"Warning: Error in qcut - {str(e)}. Using manual categorization.")
# Manual categorization as a backup option
percentiles = np.percentile(series, [class="num">20, class="num">40, class="num">60, class="num">80])
class="kw">return pd.cut(series,
bins=[-np.inf] + list(percentiles) + [np.inf],
labels=labels)
def calculate_indicators(self, df):
"""Calculate technical indicators for a single dataframe"""
result = df.copy()
# Basic calculations
result[&class="macro">#x27;Returns&class="macro">#x27;] = result[&class="macro">#x27;Close&class="macro">#x27;].pct_change()
result[&class="macro">#x27;Log_Returns&class="macro">#x27;] = np.log(result[&class="macro">#x27;Close&class="macro">#x27;]/result[&class="macro">#x27;Close&class="macro">#x27;].shift(class="num">1))
result[&class="macro">#x27;Range&class="macro">#x27;] = result[&class="macro">#x27;High&class="macro">#x27;] - result[&class="macro">#x27;Low&class="macro">#x27;]
result[&class="macro">#x27;Range_Pct&class="macro">#x27;] = result[&class="macro">#x27;Range&class="macro">#x27;] / result[&class="macro">#x27;Open&class="macro">#x27;] * class="num">100
# SMA calculations
for period in [class="num">5, class="num">10, class="num">20, class="num">50, class="num">200]:
result[f&class="macro">#x27;SMA_{period}&class="macro">#x27;] = result[&class="macro">#x27;Close&class="macro">#x27;].rolling(window=period).mean()
# EMA calculations
for period in [class="num">5, class="num">10, class="num">20, class="num">50]:
result[f&class="macro">#x27;EMA_{period}&class="macro">#x27;] = result[&class="macro">#x27;Close&class="macro">#x27;].ewm(span=period, adjust=False).mean()
# Volatility
result[&class="macro">#x27;Volatility&class="macro">#x27;] = result[&class="macro">#x27;Returns&class="macro">#x27;].rolling(window=class="num">20).std() * np.sqrt(class="num">20)
# RSI
delta = result[&class="macro">#x27;Close&class="macro">#x27;].diff()