用Python和MQL5进行投资组合优化(基础篇)
📘

用Python和MQL5进行投资组合优化(基础篇)

第 1/3 篇

◍ 用 Python 搭一条 MT5 组合优化通道

想在 MT5 里做多标的组合优化,光靠策略测试器的内置遗传算法往往不够灵活。把 Python 接进来,可以用 scipy、cvxpy 这类库先算权重边界,再回灌到 MQL5 做执行层验证。 这条路的起点是数据互通:MT5 终端通过 Python API 暴露了历史 K 线和账户状态,2025 年 2 月发布的接口版本已支持 625 种内置指标序列的直接拉取。也就是说,组合里塞进十几二十个外汇或贵金属品种,延迟仍在可接受范围。 外汇与贵金属杠杆高、跳空频繁,组合回测结果只代表历史概率,实盘仍可能偏离。建议先在 Python 里跑通均值-方差有效前沿,再把权重写进 EA 的仓位数组做样本外检验。

「两套优化器的实现路径」

本系列给出两套投资组合优化程序,分别走 Python 与 MQL5 两条技术线,目标都是在控制回撤的前提下提高配置效率。 Python 方案依赖 MT5 终端的数据接口,把行情拉出来后用 pandas 做清洗、Numpy 做矩阵运算,再用 cvxpy 求解约束优化,最后用 Matplotlib 画出权重与边界。 MQL5 方案不离开交易终端,直接调用平台原生函数完成历史读取与资产计算,适合不想切环境的实盘用户。 两者都处在量化金融与平台工程的交叉点上,能否跑出优势取决于历史样本与约束设定,外汇与贵金属品种自带高杠杆高风险,回测结论只代表历史概率。

组合优化到底解决什么痛点

传统手动配仓最大的坑不是算不准收益,而是人会被近期波动带偏,追高杀低。MQL5 里的投资组合优化器把历史样本、品种相关性和风险约束全部量化,用遗传算法或穷举去搜一组权重,使回测夏普倾向更高。 实际在 MT5 里跑优化,你能直接看到同一策略在不同手数分配下的净值曲线差异。比如对 EURUSD 与 XAUUSD 做 200 根日线样本优化,风险上限设 2%,最优解往往把黄金权重压到 30% 以下——因为两者危机期相关性会跳升,硬配平反而放大回撤。 这类工具的价值不在「算出一个圣杯」,而是把情绪决策替换成可复现的数据路径。外汇和贵金属杠杆高、跳空频繁,用优化器做压力情景模拟,比拍脑袋调仓更抗极端日。

◍ 用 Python 先验证策略再上 MT5

想快速试错一个交易想法,Python 是性价比很高的入口:社区资源多、跑得不算慢,而且自带对接 MT5 的官方库,能拉历史数据也能下单。实操上建议先用 Python 做一致性验证,把账户比例、交易符号都换几组跑一遍,再决定要不要落到实盘环境。 下面这段脚本是拉数+组合优化的核心骨架,先贴出来逐行拆: # Import necessary libraries import MetaTrader5 as mt5 import pandas as pd import numpy as np import cvxpy as cp import matplotlib.pyplot as plt from datetime import datetime # 引入 MT5 接口、pandas 做表、numpy 算矩阵、cvxpy 做凸优化、matplotlib 画图、datetime 管时间 def get_mt5_data(symbols, from_date, to_date): if not mt5.initialize(): print("Error: Could not connect to MetaTrader 5") mt5.shutdown() return None # 定义拉数函数;先初始化 MT5,连不上就报错退出 data = {} for symbol in symbols: rates = mt5.copy_rates_range(symbol, mt5.TIMEFRAME_D1, from_date, to_date) # 对每个品种用日线范围拉 rates if rates is not None: df = pd.DataFrame(rates) df['time'] = pd.to_datetime(df['time'], unit='s') df.set_index('time', inplace=True) df.drop(['tick_volume', 'spread', 'real_volume'], axis=1, inplace=True) # 转 DataFrame、时间轴化、丢无关列 df['return'] = df['close'].pct_change().fillna(0) data[symbol] = df mt5.shutdown() return data # 算日收益率填进字典,关连接返回 def optimize_portfolio(data): symbols = list(data.keys()) n_assets = len(symbols) min_length = min(len(data[symbol]) for symbol in symbols) # 优化函数:取品种数、对齐最短长度 returns = np.zeros((min_length, n_assets)) for i, symbol in enumerate(symbols): df = data[symbol].iloc[:min_length] returns[:, i] = df['return'].values # 截齐收益矩阵 cov_matrix = np.cov(returns, rowvar=False) expected_returns = np.mean(returns, axis=0) # 协方差与期望收益 weights = cp.Variable(n_assets) risk = cp.quad_form(weights, cov_matrix) objective = cp.Maximize(expected_returns @ weights - 0.5 * risk) # 以期望收益减半风险为最大化目标 constraints = [cp.sum(weights) == 1, weights >= 0] prob = cp.Problem(objective, constraints) prob.solve() # 权重和为1且不准做空,解优化 print("\nOptimization Results:") for i, symbol in enumerate(symbols): print(f"{symbol}: {weights.value[i]}") # 打印各品种权重 原脚本跑出来的权重结果指向单一重仓 Amazon,但当你往主函数里塞一个双均线交叉过滤条件后,权重分布只有小幅变动,结论大体不变。 这背后是因为 AMZN 在那段样本期内走出强趋势,优化器自然倾向给它高权重。你得盯紧趋势衰竭的时点,外汇和贵金属这类高波动品种更要把仓位风控前置,否则单品种暴露会吃掉利润。

MQL5 / C++
# Import necessary libraries
class="kw">import MetaTrader5 as mt5
class="kw">import pandas as pd
class="kw">import numpy as np
class="kw">import cvxpy as cp
class="kw">import matplotlib.pyplot as plt
from class="type">class="kw">datetime class="kw">import class="type">class="kw">datetime
# Function to obtain historical data from MT5
def get_mt5_data(symbols, from_date, to_date):
    # Establish connection with MetaTrader class="num">5
    if not mt5.initialize():
        print("Error: Could not connect to MetaTrader class="num">5")
        mt5.shutdown()
        class="kw">return None
    
    data = {}
    
    for symbol in symbols:
        # Get historical price data
        rates = mt5.copy_rates_range(symbol, mt5.TIMEFRAME_D1, from_date, to_date)
        
        if rates is not None:
            # Convert to Pandas DataFrame
            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)
            df.drop([&class="macro">#x27;tick_volume&class="macro">#x27;, &class="macro">#x27;spread&class="macro">#x27;, &class="macro">#x27;real_volume&class="macro">#x27;], axis=class="num">1, inplace=True)
            
            # Calculate daily returns
            df[&class="macro">#x27;class="kw">return&class="macro">#x27;] = df[&class="macro">#x27;close&class="macro">#x27;].pct_change().fillna(class="num">0)
            
            # Save in the data dictionary
            data[symbol] = df
    
    # Close the connection with MetaTrader class="num">5
    mt5.shutdown()
    
    class="kw">return data
# Function to optimize the portfolio
def optimize_portfolio(data):
    symbols = list(data.keys())
    n_assets = len(symbols)
    
    # Find the minimum data length among all assets
    min_length = min(len(data[symbol]) for symbol in symbols)
    
    # Adjust and normalize returns
    returns = np.zeros((min_length, n_assets))
    for i, symbol in enumerate(symbols):
        # Adjust data length
        df = data[symbol].iloc[:min_length]
        returns[:, i] = df[&class="macro">#x27;class="kw">return&class="macro">#x27;].values
    
    # Calculate covariance matrix and expected returns
    cov_matrix = np.cov(returns, rowvar=False)
    expected_returns = np.mean(returns, axis=class="num">0)
    
    # Optimization variables
    weights = cp.Variable(n_assets)
    risk = cp.quad_form(weights, cov_matrix)
    objective = cp.Maximize(expected_returns @ weights - class="num">0.5 * risk)
    
    # Constraints
    constraints = [cp.sum(weights) == class="num">1, weights >= class="num">0]
    
    # Solve the optimization problem
    prob = cp.Problem(objective, constraints)
    prob.solve()
    
    # Display optimization results
    print("\nOptimization Results:")
    for i, symbol in enumerate(symbols):
        print(f"{symbol}: {weights.value[i]}")
    
    # Calculate minimum variance and expected class="kw">return for the portfolio

「权重解出来后怎么看与接策略」

优化跑完会回吐两个硬数字:组合期望收益约 0.0025,最小方差约 0.0205。这组值基于 2023 全年 EURUSD、GBPUSD、#AMZN、#AAPL 的日线收益协方差,外汇与股票混算,样本外表现可能漂移,外汇/贵金属本身高风险,权重不能直接当仓位建议。 实际解出的权重里 #AMZN 占 1.0,EURUSD 与 GBPUSD 都在 1e-23 量级(近似 0),#AAPL 为 -1.3e-23(近似 0 空头)。说明在该协方差结构下, solver 把风险预算几乎全压到单一美股,外汇对组合边际贡献被吃净,这种权重分布倾向在汇率低波动期出现。 把权重交回可视化函数就能直接出柱状图,横轴资产、纵轴权重,一眼看出集中度。代码里 plt.bar(symbols, weights) 跑的就是这事,MT5 取数后本地 matplotlib 渲染即可。 光有权重不够,原文接着把收益序列接进双均线过滤:短窗 12、长窗 26,算 SMA_50 / SMA_200(命名虽写 50/200,窗口参数实际是 12/26),signal 在短均上穿长均时置 1,position 用前一日 signal 平移避免未来函数。adjusted_return = return * position 就是把组合收益按策略开关折算。外汇品种叠加这套过滤,可能剔除部分震荡期假突破。

MQL5 / C++
min_variance = cp.sqrt(cp.quad_form(weights.value, cov_matrix)).value
expected_return_portfolio = expected_returns @ weights.value

print(f"\nExpected portfolio class="kw">return: {expected_return_portfolio:.4f}")
print(f"Minimum portfolio variance: {min_variance:.4f}")

class="kw">return symbols, weights.value
# Function to visualize results
def visualize_results(symbols, weights):
    # Plot weights of each asset in the portfolio
    plt.figure(figsize=(class="num">10, class="num">6))
    plt.bar(symbols, weights, class="type">color=&class="macro">#x27;blue&class="macro">#x27;)
    plt.xlabel(&class="macro">#x27;Assets&class="macro">#x27;)
    plt.ylabel(&class="macro">#x27;Weights&class="macro">#x27;)
    plt.title(&class="macro">#x27;Asset Weights in Optimized Portfolio&class="macro">#x27;)
    plt.show()
# Execute the main script
if __name__ == "__main__":
    # Define parameters
    symbols = ["EURUSD", "GBPUSD", "class="macro">#AMZN", "class="macro">#AAPL"]   # Asset symbols
    from_date = class="type">class="kw">datetime(class="num">2023, class="num">1, class="num">1)   # Start date
    to_date = class="type">class="kw">datetime(class="num">2023, class="num">12, class="num">31)   # End date

    # Get historical data from MT5
    print(f"Obtaining historical data from {from_date} to {to_date}...")
    data = get_mt5_data(symbols, from_date, to_date)

    if data:
        # Optimize the portfolio
        symbols, weights = optimize_portfolio(data)

        # Visualize the results
        visualize_results(symbols, weights)
# Function to apply the moving average crossover strategy
def apply_sma_strategy(data, short_window=class="num">12, long_window=class="num">26):
    for symbol, df in data.items():
        df[&class="macro">#x27;SMA_50&class="macro">#x27;] = df[&class="macro">#x27;close&class="macro">#x27;].rolling(window=short_window).mean()
        df[&class="macro">#x27;SMA_200&class="macro">#x27;] = df[&class="macro">#x27;close&class="macro">#x27;].rolling(window=long_window).mean()
        df[&class="macro">#x27;signal&class="macro">#x27;] = class="num">0
        df.loc[df.index[short_window:], &class="macro">#x27;signal&class="macro">#x27;] = np.where(
            df.loc[df.index[short_window:], &class="macro">#x27;SMA_50&class="macro">#x27;] > df.loc[df.index[short_window:], &class="macro">#x27;SMA_200&class="macro">#x27;], class="num">1, class="num">0
        )
        df[&class="macro">#x27;position&class="macro">#x27;] = df[&class="macro">#x27;signal&class="macro">#x27;].shift(class="num">1).fillna(class="num">0)
    class="kw">return data
# Function to adjust returns according to the strategy
def adjust_returns(data):
    for symbol, df in data.items():
        df[&class="macro">#x27;adjusted_return&class="macro">#x27;] = df[&class="macro">#x27;class="kw">return&class="macro">#x27;] * df[&class="macro">#x27;position&class="macro">#x27;]
    class="kw">return data
        # Apply the moving average crossover strategy
        data = apply_sma_strategy(data)

回测跑完后的组合数值

把策略逻辑套进 adjust_returns 之后,直接拉 2023 全年历史:从 2023-01-01 00:00:00 到 2023-12-31 00:00:00 这段窗口,外汇与个股的优化结果差异很极端。 EURUSD 的优化值落在 -5.669e-25,GBPUSD 则是 5.495e-23,量级都贴近机器零;而标记里的 AMZN 给出 1.0、AAPL 为 -5.595e-23,说明个股与货币对在同一个目标函数下权重分配完全不在一个尺度。 最终组合期望收益 0.0006、最小方差 0.0151——这组数字只是该次优化输出,不代表任何实盘胜率,外汇与贵金属交易本身杠杆高、回撤可能远超方差估计,只能当作参数敏感性的一次快照去 MT5 复算。

MQL5 / C++
    # Adjust returns according to the strategy
    data = adjust_returns(data)
Obtaining historical data from class="num">2023-class="num">01-class="num">01 class="num">00:class="num">00:class="num">00 to class="num">2023-class="num">12-class="num">31 class="num">00:class="num">00:class="num">00...
Optimization Results:
EURUSD: -class="num">5.669275045708089e-25
GBPUSD: class="num">5.494697501444607e-23
class="macro">#AMZN: class="num">1.0
class="macro">#AAPL: -class="num">5.59465620602481e-23
Expected portfolio class="kw">return: class="num">0.0006
Minimum portfolio variance: class="num">0.0151

常见问题

可以,用Python搭一条通道把品种数据和约束传过去,先验证策略再上实盘,避免盲调参数。
看回测后的组合数值和波动情况,权重只是参考,需结合品种相关性人工确认再接策略。
小布可以读取你的组合数值并标注偏离较大的权重和解,帮你快速筛掉明显不合理的配置。
直接上容易过拟合且难排查,Python先跑能低成本试错,确认逻辑稳了再迁移更省事。
优先看最大回撤、品种间相关性和权重集中度,这三样异常基本代表组合有隐患。