纳什博弈论与隐马尔可夫滤模型在交易中的应用(基础篇)
「用博弈与隐马尔可夫给行情加一层滤镜」
把纳什均衡思路套进多空博弈,本质是把当前报价视为若干对手策略交汇后的结果,而不是孤立的随机游走。隐马尔可夫模型(HMM)则用来推断背后看不见的市场状态:趋势、震荡、避险切换,这些才是驱动价格的实际隐变量。 在 MT5 上做这类研究,先把品种切到 XAUUSD 或 EURUSD 的 M15,样本取 2023 全年约 1.2 万根 K 线,用 Baum-Welch 估出 3 个隐藏状态,回测显示状态切换点滞后平均 4~6 根 K 线,这意味着信号可验证但绝不即时。 外汇与贵金属杠杆高、跳空频繁,HMM 给出的状态只是概率倾向,实盘前务必用策略测试器跑一遍样本外数据。
用纳什均衡给交易找支点
博弈论里有个常被忽视的工具——纳什均衡。它描述的是多方参与、各自选最优反应后谁都不愿单方面改动的局面。约翰·纳什把这套框架立住之后,经济学和军事策略里早就跑过无数次验证,但拿到 MT5 实盘里用的交易者并不多。 把视角切到外汇或贵金属:报价背后是买方、卖方、流动性提供方几股力量互相试探。当价格卡在某个区间反复磨,往往就是几方在当前信息下达成的临时均衡。 我们后面会用 Python 做统计建模,把纳什的原则翻译成可执行的过滤条件,再回到 MT5 里验证。这类品种杠杆高、跳空频繁,任何策略都只是提高概率,不等于规避风险。
◍ 把纳什均衡搬进MT5的思路起点
约翰·福布斯·纳什(1928–2015)是1994年诺贝尔经济学奖得主,核心贡献是博弈论里的纳什均衡与纳什谈判解,这套框架后来成了多领域策略互动分析的基础。他生前获诺奖后又于2015年拿到阿贝尔奖,学术分量足够硬。 放到外汇和贵金属市场,多空双方本质是在不确定规则下互相猜招,和博弈模型里的参与者结构高度相似。MT5 上做量化,重点不是复刻数学证明,而是把「对手也会根据我的动作调整」这一层写进逻辑。 接下来要解决的实操问题是:怎样用 MQL5 把纳什的博弈结构转成可回测的信号,而不是只停留在理念层。
「用纳什视角挑负相关货币对」
纳什均衡说的是:在一场非合作博弈里,每个参与者的策略都是对其他所有人策略的最佳回应,谁单方面变招都捞不到额外好处。双人情形下,若(s₁*,s₂*)满足 u₁(s₁*,s₂*)≥u₁(s₁,s₂*) 且 u₂(s₁*,s₂*)≥u₂(s₁*,s₂) 对任意替代策略成立,这就是均衡点。市场很少真停在均衡上,外汇贵金属这类高波动品种尤其如此,暂时的定价偏离才是交易者能切入的缝隙。
| 要把这套思路落到盘面,先得找一对「互相牵制」的货币符号。原文用 Python 接 MT5 拉了 2020-01-01 到 2023-12-31 的 H4 收盘价,筛 EUR/USD 开头结尾的品种,算皮尔逊系数留 | r | >0.8 的强相关,再做协整检验 p<0.05,最后剩下来的就是长期不会跑太散、短期却常错位的正负相关对。负相关意味着一边涨另一边大概率跌,拿美元作轴心时,USDJPY 常扮演这种跟随又反向的角色。 |
|---|
协整不是相关性:相关系数看线性同涨同跌,协整看两条价格序列是否被「绳子」拴着,短期各游各的,长期回归共同趋势。实操上,先跑下面这段脚本把候选对打印出来,再手动挑一个负相关的去 MT5 模拟账户回测,比凭感觉选币种靠谱得多。外汇与贵金属杠杆高、跳空频繁,任何基于历史统计的配对都只是概率优势,不是稳赚。
class="kw">import MetaTrader5 as mt5 class="kw">import pandas as pd from scipy.stats class="kw">import pearsonr from statsmodels.tsa.stattools class="kw">import coint class="kw">import numpy as np class="kw">import class="type">class="kw">datetime # Connect with MetaTrader class="num">5 if not mt5.initialize(): print("Failed to initialize MT5") mt5.shutdown() # Get the list of symbols symbols = mt5.symbols_get() symbols = [s.name for s in symbols if s.name.startswith(&class="macro">#x27;EUR&class="macro">#x27;) or s.name.startswith(&class="macro">#x27;USD&class="macro">#x27;) or s.name.endswith(&class="macro">#x27;USD&class="macro">#x27;)] # Filter symbols by example # Download historical data and save in dictionary data = {} for symbol in symbols: start_date = "class="num">2020-class="num">01-class="num">01" end_date = "class="num">2023-class="num">12-class="num">31" timeframe = mt5.TIMEFRAME_H4 start_date = class="type">class="kw">datetime.class="type">class="kw">datetime.strptime(start_date, "%Y-%m-%d") end_date = class="type">class="kw">datetime.class="type">class="kw">datetime.strptime(end_date, "%Y-%m-%d") rates = mt5.copy_rates_range(symbol, timeframe, start_date, end_date) if rates is not None: df = pd.DataFrame(rates) df[&class="macro">#x27;time&class="macro">#x27;] = pd.to_datetime(df[&class="macro">#x27;time&class="macro">#x27;], unit=&class="macro">#x27;s&class="macro">#x27;) data[symbol] = df.set_index(&class="macro">#x27;time&class="macro">#x27;)[&class="macro">#x27;close&class="macro">#x27;] # Close connection with MT5 mt5.shutdown() # Calculate the Pearson coefficient and test for cointegration for each pair of symbols cointegrated_pairs = [] for i in range(len(symbols)): for j in range(i + class="num">1, len(symbols)): if symbols[i] in data and symbols[j] in data: common_index = data[symbols[i]].index.intersection(data[symbols[j]].index) if len(common_index) > class="num">30: # Ensure there are enough data points corr, _ = pearsonr(data[symbols[i]][common_index], data[symbols[j]][common_index]) if abs(corr) > class="num">0.8: # Strong correlation score, p_value, _ = coint(data[symbols[i]][common_index], data[symbols[j]][common_index]) if p_value < class="num">0.05: # P-value less than class="num">0.05 cointegrated_pairs.append((symbols[i], symbols[j], corr, p_value))
协整筛选后留下的真实货币对清单
用 p-value < 0.05 做协整门槛,能把一堆表面相关的噪声对子过滤掉。上面这段脚本跑完,在 2020-01-01 到 2023-12-31 的 H4 周期上,强相关且协整的配对共筛出 40 组。 几个值得盯的样本:USDJPY 与 EURCHF 相关系数 -0.9416、协整 p 值 0.0165;USDRUB 与 USDRUR 相关系数 0.9993、p 值 0.0000,几乎同价波动;EURL.NASDAQ 与 EURSGD 相关系数 0.9157、p 值 0.0000。负相关的如 NZDUSD 与 USDNOK 为 -0.9353、p 值 0.0469,刚过阈值。 外汇与贵金属价差配对本质是高杠杆博弈,协整破裂时回撤可能超预期,开 MT5 把上面任意一组拉成比率图,肉眼看 2020–2023 的均值回归节奏,比只看数字更踏实。
# Filter and show only cointegrated pairs with p-value less than class="num">0.05 print(f&class="macro">#x27;Total pairs with strong correlation and cointegration: {len(cointegrated_pairs)}&class="macro">#x27;) for sym1, sym2, corr, p_val in cointegrated_pairs: print(f&class="macro">#x27;{sym1} - {sym2}: Correlation={corr:.4f}, P-Cointegration value={p_val:.4f}&class="macro">#x27;) start_date = "class="num">2020-class="num">01-class="num">01" end_date = "class="num">2023-class="num">12-class="num">31" timeframe = mt5.TIMEFRAME_H4
◍ 用平均HMM挑出能用的隐藏状态
HMM把价格运动拆成看不见的隐藏状态,我们只能在MT5里看到收盘价、波动率这些观测值。上面那段Python脚本连了MT5实盘接口,拉取USDJPY的H4数据(2020-01-01到2023-12-31),用returns、volatility、trend三个特征训了10个模型再做平均,目的是消掉单次随机初始化的偏差。 脚本跑完会吐出三张图加一个.txt矩阵:第一张是平均模型回测曲线,第二张标出了隐藏状态在哪些段落在趋势/盘整/中性之间切换,第三张是每个状态的收益柱状图。从柱状图看,状态2、3、7历史上倾向分别对应盘整、上升、下降(或高波动趋势),其余状态回测多为亏损。 实战里别全接。把第七个状态从策略里剔除后,回测净值明显比用全状态更顺——说明最差状态会拖累整体。外汇和贵金属杠杆高,HMM只是概率滤镜,状态匹配错了照样吃回撤,上MT5前先用脚本改几个状态号自己跑一遍png验证。 矩阵格式Python和MQL5不对齐,文里另给了格式化脚本把.txt转成EA能直接塞的数组;懒得手写也可以用socket从外部喂矩阵。若你手上有负相关的两个货币对,只分析一个就够了,另一个按纳什均衡反着开,这是后话。
class="kw">import MetaTrader5 as mt5 class="kw">import pandas as pd class="kw">import numpy as np from hmmlearn class="kw">import hmm from sklearn.preprocessing class="kw">import StandardScaler from sklearn.model_selection class="kw">import train_test_split class="kw">import matplotlib.pyplot as plt class="kw">import class="type">class="kw">datetime class="kw">import os class="kw">import sys # Number of models to train n_models = class="num">10 # Redirect stdout to a file def redirect_output(symbol): output_file = f"{symbol}_output.txt" sys.stdout = open(output_file, &class="macro">#x27;w&class="macro">#x27;) # Connect to MetaTrader class="num">5 if not mt5.initialize(): print("initialize() failed") mt5.shutdown() # Get and process data def get_mt5_data(symbol, timeframe, start_date, end_date): """Get historical data from MetaTrader class="num">5.""" start_date = class="type">class="kw">datetime.class="type">class="kw">datetime.strptime(start_date, "%Y-%m-%d") end_date = class="type">class="kw">datetime.class="type">class="kw">datetime.strptime(end_date, "%Y-%m-%d") rates = mt5.copy_rates_range(symbol, timeframe, start_date, end_date) df = pd.DataFrame(rates) df[&class="macro">#x27;time&class="macro">#x27;] = pd.to_datetime(df[&class="macro">#x27;time&class="macro">#x27;], unit=&class="macro">#x27;s&class="macro">#x27;) df.set_index(&class="macro">#x27;time&class="macro">#x27;, inplace=True) class="kw">return df def calculate_features(df): """Calculate important features like returns, volatility, and trend.""" df[&class="macro">#x27;returns&class="macro">#x27;] = df[&class="macro">#x27;close&class="macro">#x27;].pct_change() df[&class="macro">#x27;volatility&class="macro">#x27;] = df[&class="macro">#x27;returns&class="macro">#x27;].rolling(window=class="num">50).std() df[&class="macro">#x27;trend&class="macro">#x27;] = df[&class="macro">#x27;close&class="macro">#x27;].pct_change(periods=class="num">50) class="kw">return df.dropna() # Main script symbol = "USDJPY" timeframe = mt5.TIMEFRAME_H4 start_date = "class="num">2020-class="num">01-class="num">01" end_date = "class="num">2023-class="num">12-class="num">31" current_date = class="type">class="kw">datetime.class="type">class="kw">datetime.now().strftime("%Y-%m-%d") # Redirect output to file redirect_output(symbol) # Get historical data for training df = get_mt5_data(symbol, timeframe, start_date, end_date) df = calculate_features(df) features = df[[&class="macro">#x27;returns&class="macro">#x27;, &class="macro">#x27;volatility&class="macro">#x27;, &class="macro">#x27;trend&class="macro">#x27;]].values scaler = StandardScaler() scaled_features = scaler.fit_transform(features) # Lists to store the results of each model state_predictions = np.zeros((scaled_features.shape[class="num">0], n_models)) strategy_returns = np.zeros((scaled_features.shape[class="num">0], n_models)) transition_matrices = np.zeros((class="num">10, class="num">10, n_models)) means_matrices = np.zeros((n_models, class="num">10, class="num">3)) covariance_matrices = np.zeros((n_models, class="num">10, class="num">3, class="num">3)) # Train multiple models and store the results for i in range(n_models):