
使用Python与MQL5进行多个交易品种分析(第二部分):主成分分析在投资组合优化中的应用
管理投资组合所面临的总风险是一项艰巨的任务,我们交易社区的每个成员都必须面对。鉴于现代投资者可选择的大量投资机会,如今在不断扩展的市场中,如何全面分析并决定适当的资产配置?在我们上一次的讨论中,我展示了如何使用SciPy来最大化投资组合的总回报。今天,我将专注于如何控制您手头可能拥有的任何投资组合的风险/方差。我们可以使用许多模型来控制投资组合的风险。我们可以使用统计学中的一种流行工具——主成分分析(PCA),来有效管理投资组合的总方差。
对于希望出售EA的社区成员,本文将展示如何为终端用户提供无缝的体验。我们的交易应用程序将同时具备灵活性和稳健性。我将向您展示如何创建交易应用程序,使您的客户能够轻松切换高、中、低风险交易模式。而PCA算法将在后台为您的终端用户承担繁重的工作。
方法论概述
在本文中,我们将管理一个包含10种加密货币的投资组合。加密货币是一种存在于一种特殊网络——区块链网络上的数字资产。区块链技术是一种安全协议,使得网络中的任何成员几乎不可能进行欺诈交易。因此,鉴于底层网络的强度,区块链技术的首个流行用途之一就是数字货币。然而,这些数字资产以波动性极高而闻名,投资者在投资加密资产时很难充分管理风险水平。幸运的是,我们可以使用PCA等统计方法来管理投资加密货币时我们希望暴露的总方差。主成分分析(PCA)是一种来自多元统计学中因子分析分支的技术。PCA已在许多领域得到应用,从图像分析到机器学习。在图像分析中,PCA通常用于数据压缩等任务,而在机器学习中,其主要用于降维。
PCA的核心思想是为数据集中的每一列找到一个系数值。如果每一列都乘以其系数,我们得到的新列应该最大化数据集的方差。如果我们成功完成了这一过程,就成功找到了第一个主成分。
第二个主成分将与第一个正交,这意味着它们完全不相关,同时最大化数据集的总方差。这种模式会一直持续,直到我们创建了足够多的成分,以解释原始数据集中的所有方差。
我将向您展示,每个主成分如何分别映射到我们可以用作交易应用程序设置的离散风险水平。这将帮助您在几分钟内智能管理风险设置。
开始
我相信,对于可能是第一次接触该算法的读者来说,算法运行的可视化演示将大有裨益。我将使用图1中MQL5的标识图片,并首先将其转换为黑白模式。这种黑白滤镜将使我们更容易应用并直观地看到PCA算法对我们的数据做了什么处理。
图1:我们将使用上述MQL5标识来演示PCA算法的工作原理
现在有了可供操作的指导图,让我们加载Python库。
#Let's go import pandas as pd import numpy as np import MetaTrader5 as mt5 import seaborn as sns import matplotlib.pyplot as plt from sklearn.decomposition import PCA from skimage import io, color
读取图像并将其转换为黑白模式。
# Load and preprocess the image image = io.imread('mql5.png') # Replace with your image path image_gray = image_gray = color.rgb2gray(image) # Convert to grayscale
将图像展平为二维,并对5个不同成分级别的目标应用PCA。原始图像位于图2的最左侧。第一个主成分最大化输入数据的方差。正如我们在标记为“1个成分”的图像中所看到的,原始图像的整体结构在一定程度上得以保留。尽管图像中心的“MQL5”文本已不再可读,但我们仍可以合理地推断出图像有一个黑色背景,中间有一个类似白色的结构。
当使用5个主成分时,图像中的文本是可读的。然而,像背景中设计的浅灰色图标这样的细微细节丢失了,需要更多的成分才能恢复。
通过本次实践,您将直观地理解PCA算法的核心目标:生成原始输入数据的紧凑且互不相关的特征表示。这一任务是通过创建输入数据的连续线性组合来实现的,这些组合最大化输入数据的方差。
# Flatten the image h, w = image_gray.shape image_flattened = image_gray.reshape(h, w) # Apply PCA with different numbers of components n_components_list = [1,5,20,50,100] # Number of components to keep fig, axes = plt.subplots(1, len(n_components_list) + 1, figsize=(20, 10)) axes[0].imshow(image_gray, cmap='gray') axes[0].set_title("Original Image") axes[0].axis('off') for i, n_components in enumerate(n_components_list): # Initialize PCA and transform the flattened image pca = PCA(n_components=n_components) pca.fit(image_flattened) # Transform and inverse transform the image transformed_image = pca.transform(image_flattened) reconstructed_image = pca.inverse_transform(transformed_image).reshape(h, w) # Plot the reconstructed image axes[i + 1].imshow(reconstructed_image, cmap='gray') axes[i + 1].set_title(f"{n_components} Components") axes[i + 1].axis('off') plt.tight_layout() plt.show()
图2:前两个主成分对MQL5标识的降维重构效果
图2:后三个主成分对MQL5标识的降维重构效果
我们对图像本身并不感兴趣,而是更关注最大化或最小化投资组合的方差。我们可以通过将包含投资组合中每个资产收益率的数据集传递给我们的PCA算法来实现这一目标。
获取我们的市场数据
在开始之前,首先我们需要确保我们的MetaTrader 5终端已启动。
mt5.initialize()
现在列出我们投资组合中将持有的所有资产。
#List of cryptocurrencies we wish to invest crypto = [ "BCHUSD", #BitcoinCash "EOSUSD", #EOS "BTCUSD", #Bitcoin "ETHUSD", #Etherum "ADAUSD", #Cardano "XRPUSD", #Ripple "UNIUSD", #Monero "DOGUSD", #Dogecoin "LTCUSD", #Litecoin "SOLUSD" #Solana ]
我们想要获取6年的每日市场数据。
fetch = 365 * 6
我们的模型将预测未来30天的走势。
look_ahead = 30
创建一个数据结构来存储我们的加密货币收益率。
data = pd.DataFrame(columns=crypto,index=range(fetch))
获取我们列表中每个交易品种的市场报价。
for i in range(0,len(crypto)): data[crypto[i]] = pd.DataFrame(mt5.copy_rates_from_pos(crypto[i],mt5.TIMEFRAME_M1,0,fetch)).loc[:,"close"]
让我们看看我们的市场数据。
data
图4:可视化我们收集到的一些市场数据,记录一组加密货币6年的历史价格
探索性数据分析
交易加密货币可能具有挑战性,因为这些品种的波动性很大。图5帮助我们可视化一组10种加密货币在6年期间(从2018年到2024年)的表现。这些资产之间的价差会变化,并且可能不容易被人为直观地计算或有效使用。
data_plotting = data.iloc[:,:]/data.iloc[0,:]
sns.lineplot(data_plotting)
图5:我们可能投资的10种不同加密货币的表现
各加密货币对投资组合的风险贡献,可通过计算每个交易品种收益率的滚动标准差进行可视化呈现。从图6我们可以看到,高波动性时期似乎会聚集在一起。然而,部分市场比其他市场表现出更剧烈的波动性。
plt.plot(data_plotting.iloc[:,0].rolling(14).std()) plt.plot(data_plotting.iloc[:,1].rolling(14).std()) plt.plot(data_plotting.iloc[:,2].rolling(14).std()) plt.plot(data_plotting.iloc[:,3].rolling(14).std()) plt.plot(data_plotting.iloc[:,4].rolling(14).std()) plt.plot(data_plotting.iloc[:,5].rolling(14).std()) plt.plot(data_plotting.iloc[:,6].rolling(14).std()) plt.plot(data_plotting.iloc[:,7].rolling(14).std()) plt.plot(data_plotting.iloc[:,8].rolling(14).std()) plt.plot(data_plotting.iloc[:,9].rolling(14).std()) plt.legend(crypto) plt.title("The Risk Associated With Each of our Cryptocurrencies") plt.xlabel("Time") plt.ylabel("Standard Deviation")
图6:我们投资组合中每种加密货币的标准差
PCA算法将帮助我们有意识地最小化或最大化我们对图6中可视化投资组合总方差的暴露。在10种资产中人工手动筛选出能使投资组合方差最大化的资产组合,几乎是一项无法独立完成的任务。当我们分析同一资产类别中的资产时,一定程度的强相关性是意料之中的。特别有趣的是以下几对资产之间的相关性水平:
- EOSUSD & XRPUSD
- DOGUSD & BCHUSD
#Correlation analysis
sns.heatmap(data.corr(),annot=True)
图7:可视化我们10种加密货币投资组合的相关性水平
计算两周内的价格水平变化。
#Calculate the change over 2 weeks data = data.diff(14).dropna().reset_index(drop=True)
现在拟合缩放器。
scaler = RobustScaler() scaled_data = pd.DataFrame(scaler.fit_transform(data), columns=data.columns)
在我们的每日市场回报上拟合scikit-learn的PCA对象。
pca = PCA() pca.fit(scaled_data)
现在让我们分析第一个主成分。系数的符号告诉我们是否应该在每个市场中持有长仓或短仓。正系数告诉我们做多,负系数告诉我们做空。因此,为了最大化我们投资组合的方差,PCA算法建议在我们选择的所有市场中做空。
对于希望构建剥头皮策略的交易者来说,第一个主成分是最优选择。
#High risk strategy pca.components_[0]
此外,每个主成分还为我们提供了最优的资金分配比例,这些比例将相应地最大化或最小化方差。然而,为了获取这些信息,我们必须对主成分进行一些转换。
我们将主成分除以其L1范数。L1范数是主成分中绝对值的总和。通过将各主成分载荷得分除以其总和,我们即可确定为使投资组合方差最大化应分配给各资产的最优比例。
在计算了成分中每种加密货币的比例之后,我们可以将资产分配比例乘以想要开仓的数量,以获得在每个市场中应该开仓数量的近似值。计算总和是为了证明,如果我们将资产分配比例乘以100,总仓位将加起来等于100。
#High risk asset allocations can be estimated from the first principal component high_risk_asset_allocation = pca.components_[0] / np.linalg.norm(pca.components_[0],1) np.sum(high_risk_asset_allocation * 100)
例如,如果我们想按照高风险策略开10个仓位,主成分建议我们在BCHUSD(比特币现金)上卖出1个仓位。系数的小数部分可以被解释为一个略小的仓位规模。但在投资组合中精确考量这一点可能会耗费大量时间。依据分配比例的整数部分可能更简单。
high_risk_asset_allocation * 10
现在让我们转向中等风险的主成分。
#Mid risk strategy pca.components_[len(crypto)//2]
最后,我们的低风险主成分建议一种完全不同的交易策略。
#Low risk strategy pca.components_[len(crypto)-1]
让我们将这些主成分保存到文本文件中,以便可以在MetaTrader 5交易应用程序中使用这些发现。
np.savetxt("LOW RISK COMPONENTS.txt",pca.components_[len(crypto)-1]) np.savetxt("MID RISK COMPONENTS.txt",pca.components_[len(crypto)//2]) np.savetxt("HIGH RISK COMPONENTS.txt",pca.components_[0])
在MQL5中实现
我们现在可以开始实现MetaTrader 5交易应用程序。
我们的交易应用程序将遵循最优资产分配的发现。我们将根据用户当前选择的风险设置,在每个市场中只开1笔交易。交易应用程序将使用移动平均线和随机振荡器来确定入场时机。
如果主成分建议我们在市场中做多,那么我们将等待价格水平升至该市场的移动平均线读数之上,并且随机振荡器高于50。一旦两个条件都满足,我们将建立多头仓位,并相应地使用ATR设置我们的获利和止损水平。以这种方式确定市场的入场时机,有望随着时间的推移为我们提供更稳定和一致的入场信号。
开始前,我们首先将定义一个自定义枚举器,以便终端用户能够控制交易应用程序的风险参数。
//+------------------------------------------------------------------+ //| PCA For Portfolio Optimization.mq5 | //| Gamuchirai Zororo Ndawana | //| https://www.mql5.com/en/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Zororo Ndawana" #property link "https://www.mql5.com/en/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| Custom enumerations | //+------------------------------------------------------------------+ enum risk_level { High=0, Mid=1, Low=2 };
加载我们需要的库。
//+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade Trade;
定义在任何情况都不会改变的常数值。
//+------------------------------------------------------------------+ //| Constants values | //+------------------------------------------------------------------+ const double high_risk_components[] = {#include "HIGH RISK COMPONENTS.txt"}; const double mid_risk_components[] = {#include "MID RISK COMPONENTS.txt"}; const double low_risk_components[] = {#include "LOW RISK COMPONENTS.txt"}; const string crypto[] = {"BCHUSD","EOSUSD","BTCUSD","ETHUSD","ADAUSD","XRPUSD","UNIUSD","DOGUSD","LTCUSD","SOLUSD"};
我们将在整个程序中使用的全局变量。
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ double current_risk_settings[]; double vol,bid,ask; int atr_handler; int stoch_handler; int ma_handler; double atr_reading[],ma_reading[],stoch_reading[];
我们将允许最终用户动态控制交易账户的风险设置和期望的交易手数。
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "Risk Levels" input risk_level user_risk = High; //Which risk level should we use? input group "Money Management" input int lot_multiple = 1; //How big should out lot size be?
当加载交易应用程序时,我们首先会根据用户选择的风险设置加载主成分。所有这些都将由“setup”函数处理。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Setup our market data setup(); //--- return(INIT_SUCCEEDED); }
如果不再使用EA,那么我们应该释放那些不再使用的资源。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Release the indicators IndicatorRelease(atr_handler); IndicatorRelease(ma_handler); IndicatorRelease(stoch_handler); }
最后,每当我们接收到更新的价格时,则在每个加密货币市场中维持一个仓位的投资组合。由用户选择的风险设置决定我们将占据的仓位。根据风险设置,我们可能需要占据“买入”或“卖出”仓位,以控制投资组合的方差。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Ensure we always have 10 positions if(PositionsTotal() < 10) { //--- Let's see which market we aren't in for(int i = 0; i < 10; i++) { //--- Check if we can now enter that market if(!PositionSelect(crypto[i])) { check_setup(i); } } } }
配置函数将判断终端用户选择的风险设置等级,并据此加载对应的主成分。
//+------------------------------------------------------------------+ //| Setup our market data | //+------------------------------------------------------------------+ void setup(void) { //--- First let us define the current risk settings switch(user_risk) { //--- The user selected high risk case 0: { ArrayCopy(current_risk_settings,high_risk_components,0,0,WHOLE_ARRAY); Comment("EA in high risk mode"); break; } //--- The user selected mid risk case 1: { ArrayCopy(current_risk_settings,mid_risk_components,0,0,WHOLE_ARRAY); Comment("EA in mid risk mode"); break; } //--- The user selected low risk //--- Low risk is also the default setting for safety! default: { ArrayCopy(current_risk_settings,low_risk_components,0,0,WHOLE_ARRAY); Comment("EA in low risk mode"); break; } } }
鉴于我们正对10个不同交易品种进行主动交易,需要构建一个负责获取各品种相关市场数据的专用函数。以下定义的"update"函数将承担此项任务。每当调用该函数时,它会将指定交易品种当前的买入价和卖出价及基于该市场计算的其他技术指标读数加载至内存。在我们完成数据使用并确定交易动作后,该函数会自动将下一个交易品种的市场数据加载至内存。
//+------------------------------------------------------------------+ //| Update our system varaibles | //+------------------------------------------------------------------+ void update(string market) { //--- Get current prices bid = SymbolInfoDouble(market,SYMBOL_BID); ask = SymbolInfoDouble(market,SYMBOL_ASK); //--- Get current technical readings atr_handler = iATR(market,PERIOD_CURRENT,14); stoch_handler = iStochastic(market,PERIOD_CURRENT,5,3,3,MODE_EMA,STO_CLOSECLOSE); ma_handler = iMA(market,PERIOD_CURRENT,30,0,MODE_EMA,PRICE_CLOSE); //--- Copy buffer CopyBuffer(atr_handler,0,0,1,atr_reading); CopyBuffer(ma_handler,0,0,1,ma_reading); CopyBuffer(stoch_handler,0,0,1,stoch_reading); //--- }
最后,我们需要构建一个风险合规检查函数,用于验证当前是否可按照用户指定的风险参数开立新仓位。若不具备开仓条件,系统将向终端用户提供清晰可理解的解释,说明无法在该市场开仓的具体原因。
//+------------------------------------------------------------------+ //| Open a position if we can | //+------------------------------------------------------------------+ void check_setup(int idx) { //--- The function takes the index of the symbol as its only parameter //--- It will look up the principal component loading of the symbol to determine whether it should buy or sell update(crypto[idx]); vol = lot_multiple * SymbolInfoDouble(crypto[idx],SYMBOL_VOLUME_MIN); if(current_risk_settings[idx] > 0) { if((iClose(crypto[idx],PERIOD_D1,0) > ma_reading[0]) && (stoch_reading[0] > 50)) { Comment("Analyzing: ",crypto[idx],"\nMA: ",ma_reading[0],"\nStoch: ",stoch_reading[0],"\nAction: Buy"); Trade.Buy(vol,crypto[idx],ask,(ask - (atr_reading[0] * 3)),(ask + (atr_reading[0] * 3)),"PCA Risk Optimization"); return; } else { Comment("Waiting for an oppurtunity to BUY: ",crypto[idx]); } } else if(current_risk_settings[idx] < 0) { if((iClose(crypto[idx],PERIOD_D1,0) < ma_reading[0]) && (stoch_reading[0] < 50)) { Comment("Analyzing: ",crypto[idx],"\nClose: ","\nMA: ",ma_reading[0],"\nStoch: ",stoch_reading[0],"\nAction: Sell"); Trade.Sell(vol,crypto[idx],bid,(bid + (atr_reading[0] * 3)),(bid - (atr_reading[0] * 3)),"PCA Risk Optimization"); return; } else { Comment("Waiting for an oppurtunity to SELL: ",crypto[idx]); return; } } Comment("Analyzing: ",crypto[idx],"\nMA: ",ma_reading[0],"\nStoch: ",stoch_reading[0],"\nAction: None"); return; }; //+------------------------------------------------------------------+
图8:在MetaTrader 5中查看我们的EA
图9:调整我们交易应用程序的风险参数
图10:在实时真实市场数据上测试我们的交易应用程序
结论
主成分分析(PCA)通常被视为一个颇具难度的主题,这主要源于其概念理解所需的高阶数学符号体系。然而,希望本文已向您展示了如何以直观易用的方式应用PCA,即使您是初次接触这一主题,也能立即开展实践。
管理投资组合中的风险并非一项简单的任务,数学工具和模型是衡量和管理金融市场中您所面临风险的最优方式。尤其是当您希望参与的市场数量变得很大时。
无论我们有10个交易品种还是100个交易品种,PCA总会向我们展示哪些组合将最大化投资组合的风险,哪些组合将最小化我们的投资组合风险。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/16273

