PythonとMQL5による多銘柄分析(前編):NASDAQ集積回路メーカー
投資家がポートフォリオを多様化する方法はたくさんあります。また、ポートフォリオの最適化度を評価する基準として使用できる指標も複数存在します。ただし、個々の投資家がこれらの多様な選択肢をすべて十分に検討し、大きな決定を下すための時間やリソースを持つことは、現実的には困難です。本連載では、複数の銘柄を同時に取引する際に直面する多岐にわたる選択肢について詳しく解説します。目標は、どの戦略を維持するか、どの戦略があなたに適さないかを判断するお手伝いをすることです。
取引戦略の概要
このディスカッションでは、密接に関連する株式のバスケットを選定し、ビジネスサイクルにおいて集積回路を設計・販売する5つの企業に焦点を当てました。選定した企業は、Broadcom、Cisco、Intel、NVIDIA、Comcastです。これらの企業はすべて全米証券業協会自動取引システム(NASDAQ)取引所に上場しています。NASDAQは1971年に設立され、現在では取引量で米国最大の取引所となっています。
集積回路は現代社会に欠かせない存在であり、WebサイトをホストするMetaQuotesサーバーから、この記事を閲覧しているデバイスに至るまで、あらゆる場面で活用されています。これらのデバイスの多くは、選定した企業の技術によって支えられています。興味深いことに、世界初の集積回路であるIntel 4004は、NASDAQ設立と同じ1971年にIntelによって開発されました。Intel 4004は約2,600個のトランジスタを搭載していましたが、現在のチップは数十億個のトランジスタを搭載しており、その進化の規模は計り知れません。
集積回路への世界的な需要が増加する中、この市場で賢くエクスポージャーを得ることを目指しています。この5社の株式バスケットを基に、ポートフォリオリターンを最大化する方法を検討しました。単純に資本を均等配分する伝統的なアプローチでは、現代の市場の変動性には対応しきれません。そのため、どの株式を買うべきか売るべきか、さらに最適な取引数量を導き出すモデルを構築しました。このモデルは、保有データを活用してポジションサイズと数量をアルゴリズム的に決定します。
方法論の概要
まず、MetaTrader 5 Pythonライブラリを活用し、MetaTrader 5端末からバスケット内の5銘柄についてそれぞれ100,000行のM市場データを取得しました。このデータを通常の価格データからパーセント変化に変換した上で、市場リターンデータに対して探索的データ分析をおこないました。
分析の結果、5銘柄間の相関は弱いことが判明しました。さらに、箱ひげ図分析では、各銘柄の平均リターンが0に近いことが明らかとなりました。リターンを重ねてプロットすると、NVIDIAの株式が最も大きな変動を示していることが確認できました。また、5銘柄の間でペアプロットを作成しましたが、明確な関係性は確認されませんでした。
次に、SciPyライブラリを使用してポートフォリオ内の各銘柄に最適な重みを計算しました。これらの重みはすべて-1から1の範囲に設定されています。ポートフォリオの重みが0未満の場合、アルゴリズムは売りを指示し、0を超える場合は買いを推奨します。
ポートフォリオの最適な重みを算出した後、このデータを取引アプリケーションに統合しました。このアプリケーションは、各市場において最適なポジションが常に維持されるよう設計されており、エンドユーザーが指定した利益水準に達するとオープンポジションを自動でクローズします。
データの取得
はじめに、必要なライブラリをインポートしましょう。
#Import the libraries we need import pandas as pd import numpy as np import seaborn as sns import matplotlib.pyplot as plt import MetaTrader5 as mt5 from scipy.optimize import minimize
次に、MetaTrader 5端末を初期化します。
#Initialize the terminal
mt5.initialize()
取引したい株式のバスケットを定義します。
#Now let us fetch the data we need on chip manufacturing stocks #Broadcom, Cisco, Comcast, Intel, NVIDIA stocks = ["AVGO.NAS","CSCO.NAS","CMCSA.NAS","INTC.NAS","NVDA.NAS"]
市場データを格納するデータフレームを作成します。
#Let us create a data frame to store our stock returns amount = 100000 returns = pd.DataFrame(columns=stocks,index=np.arange(0,amount))
マーケットデータを取得します。
#Fetch the stock returns for stock in stocks: temp = pd.DataFrame(mt5.copy_rates_from_pos(stock,mt5.TIMEFRAME_M1,0,amount)) returns[[stock]] = temp[["close"]].pct_change()
データを書式設定します。
#Format the data set
returns.dropna(inplace=True)
returns.reset_index(inplace=True,drop=True)
returns
最後に、データを100倍してパーセンテージとして保存します。
#Convert the returns to percentages returns = returns * 100 returns
探索的データ分析
システム内の変数間の関係を視覚的に確認できることもあります。データの相関レベルを分析し、活用できる線形結合があるかどうかを見てみましょう。残念ながら、相関レベルは印象的ではなく、今のところ、利用できる線形依存関係はないようです。
#Let's analyze if there is any correlation in the data sns.heatmap(returns.corr(),annot=True)
図1:相関ヒートマップ
データのペアワイズ散布図を分析してみましょう。大規模なデータセットを扱う場合、重要な関係が簡単に見逃されてしまう可能性があります。ペアワイズ散布図は、このような事態が発生する可能性を最小限に抑えます。残念ながら、散布図によって明らかになったデータには、簡単に観察できる関係はありませんでした。
#Let's create pair plots of our data sns.pairplot(returns)
図2:ペア散布図の一部
データで観察されたリターンをプロットすると、NVIDIAのリターンが最も変動が大きいことがわかります。
#Lets also visualize our returns
returns.plot()
図3:市場リターンのプロット
市場リターンを箱ひげ図にすると、平均市場リターンが0であることがよくわかります。
#Let's try creating box-plots sns.boxplot(returns)
図4:市場リターンを箱ひげ図で可視化する
ポートフォリオの最適化
これで、各銘柄の資本配分の最適の重みを計算する準備が整いました。最初は、重みをランダムに割り当てます。さらに、最適化アルゴリズムの進捗状況を保存するためのデータ構造も作成します。
#Define random weights that add up to 1 weights = np.array([1,0.5,0,0.5,-1]) #Create a data structure to store the progress of the algorithm evaluation_history = []
最適化手順の目的関数は、与えられた重みの下でのポートフォリオのリターンとなります。なお、ポートフォリオのリターンは、資産リターンの幾何平均を使って計算されます。算術平均よりも幾何平均を採用することにしたのは、正負の値を扱う場合、平均を計算するのはもはや些細な作業ではないからです。この問題に気軽に取り組み、算術平均を使えば、ポートフォリオのリターンは簡単に0と計算できたでしょう。最適化アルゴリズムに返す前にポートフォリオのリターンに負の1を乗じることで、最大化問題に最小化アルゴリズムを使用することができます。
#Let us now get ready to maximize our returns #First we need to define the cost function def cost_function(x): #First we need to calculate the portfolio returns with the suggested weights portfolio_returns = np.dot(returns,x) geom_mean = ((np.prod( 1 + portfolio_returns ) ** (1.0/99999.0)) - 1) #Let's keep track of how our algorithm is performing evaluation_history.append(-geom_mean) return(-geom_mean)
ここで、すべての重みの合計が1になることを保証する制約を定義しましょう。SciPyのいくつかの最適化手続きだけが等式制約をサポートしていることに注意してください。等式制約は、この関数が0と等しくなることをSciPyモジュールに知らせます。したがって、重みの絶対値と1の差は0になるようにします。
#Now we need to define our constraints def l1_norm_constraint(x): return(((np.sum(np.abs(x))) - 1)) constraints = ({'type':'eq','fun':l1_norm_constraint})
重みはすべて-1から1の間にします。これは、アルゴリズムの境界を定義することによって強制できます。
#Now we need to define the bounds for our weights bounds = [(-1,1)] * 5
最適化プロシージャを実行します。
#Perform the optimization results = minimize(cost_function,weights,method="SLSQP",bounds=bounds,constraints=constraints)
最適化プロシージャの結果です。
results
success:True
status:0
fun:0.0024308603411499208
x: [ 3.931e-01 1.138e-01 -5.991e-02 7.744e-02 -3.557e-01]
nit:23
jac: [ 3.851e-04 2.506e-05 -3.083e-04 -6.868e-05 -3.186e-04]
nfev:158
njev:23
算出した最適な係数値を保存しておきましょう。
optimal_weights = results.x optimal_weights
また、この手順で得られた最適点を保存しておく必要があります。
optima_y = min(evaluation_history)
optima_x = evaluation_history.index(optima_y)
inputs = np.arange(0,len(evaluation_history))
最適化アルゴリズムのパフォーマンス履歴を可視化してみましょう。プロットからわかるように、アルゴリズムは最初の50回の反復で苦戦しているように見えます。しかし、ポートフォリオのリターンを最大化する最適なポイントを見つけることができたようです。
plt.scatter(inputs,evaluation_history) plt.plot(optima_x,optima_y,'s',color='r') plt.axvline(x=optima_x,ls='--',color='red') plt.axhline(y=optima_y,ls='--',color='red') plt.title("Maximizing Returns")
図5:SLSQP最適化アルゴリズムのパフォーマンス
重みの絶対値が1になることを確認しましょう。言い換えると、L1ノルム制約に違反していないことを検証します。
#Validate the weights add up to 1 np.sum(np.abs(optimal_weights))
最適係数を直感的に解釈する方法があります。10ポジションを建てたいと仮定すると、まず係数を10倍します。次に、1による整数除算を実行し、小数点以下の桁数を削除します。残った整数は、各市場で建てるべきポジションの数と解釈できます。データは、リターンを最大化するために、Broadcomのロングポジションを3つ、Ciscoのロングポジションを1つ、Comcastのショートポジションを1つ、NVIDIAのショートポジションを4つ持ち、Intelのポジションを持たないことを示唆しているようです。
#Here's an intuitive way of understanding the data #If we can only open 10 positions, our best bet may be #3 buy positions in Broadcom #1 buy position in Cisco #1 sell position sell position in Comcast #No positions in Intel #4 sell postions in NVIDIA (optimal_weights * 10) // 1
MQL5での実装
それでは、MQL5で取引戦略を実行してみましょう。まず、アプリケーションで使用するグローバル変数を定義することから始めましょう。
//+------------------------------------------------------------------+ //| NASDAQ IC AI.mq5 | //| Gamuchirai Zororo Ndawana | //| https://www.mql5.com/ja/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Zororo Ndawana" #property link "https://www.mql5.com/ja/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int rsi_handler,bb_handler; double bid,ask; int optimal_weights[5] = {3,1,-1,0,-4}; string stocks[5] = {"AVGO.NAS","CSCO.NAS","CMCSA.NAS","INTC.NAS","NVDA.NAS"}; vector current_close = vector::Zeros(1); vector rsi_buffer = vector::Zeros(1); vector bb_high_buffer = vector::Zeros(1); vector bb_mid_buffer = vector::Zeros(1); vector bb_low_buffer = vector::Zeros(1);
ポジションの管理に役立つ取引ライブラリをインポートします。
//+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade/Trade.mqh> CTrade Trade;
プログラムのエンドユーザーは、私たちがコントロールできるようにした入力を通じて、エキスパートアドバイザー(EA)の動作を調整することができます。
//+------------------------------------------------------------------+ //| User inputs | //+------------------------------------------------------------------+ input double profit_target = 1.0; //At this profit level, our position will be closed input int rsi_period = 20; //Adjust the RSI period input int bb_period = 20; //Adjust the Bollinger Bands period input double trade_size = 0.3; //How big should our trades be?
取引アルゴリズムが初めて設定されるときは常に、前回計算した5つの銘柄すべてが利用可能であることを確認する必要があります。そうでない場合は、初期化プロシージャを中止します。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Validate that all the symbols we need are available if(!validate_symbol()) { return(INIT_FAILED); } //--- Everything went fine return(INIT_SUCCEEDED); }
プログラムがチャートから削除された場合、もう使っていないリソースを解放しなければなりません。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Release resources we no longer need release_resources(); }
更新された価格を受け取るたびに、まずグローバルに定義された変数に現在のビッドとアスクを保存し、取引機会をチェックし、最後に準備していた利益を取りたいと思います。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Update market data update_market_data(); //--- Check for a trade oppurtunity in each symbol check_trade_symbols(); //--- Check if we have an oppurtunity to take ourt profits check_profits(); }
テーブルから利益を取り除く関数は、バスケットにあるすべての銘柄を繰り返し処理します。銘柄が見つかったら、その市場でポジションを持っているかどうかをチェックします。未決済のポジションがあると仮定して、利益がユーザーが定義した利益目標を上回ったかどうかをチェックし、上回った場合はポジションを決済します。そうでなければ、次に進みます。
//+------------------------------------------------------------------+ //| Check for opportunities to collect our profits | //+------------------------------------------------------------------+ void check_profits(void) { for(int i =0; i < 5; i++) { if(SymbolSelect(stocks[i],true)) { if(PositionSelect(stocks[i])) { if(PositionGetDouble(POSITION_PROFIT) > profit_target) { Trade.PositionClose(stocks[i]); } } } } }
更新された価格を受け取るときはいつでも、グローバルにスコープされた変数に格納します。
//+------------------------------------------------------------------+ //| Update markte data | //+------------------------------------------------------------------+ void update_market_data(void) { ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); }
EAが使用されていないときはいつでも、エンドユーザーのエクスペリエンスを向上させるために、不要になったリソースを解放します。
//+-------------------------------------------------------------------+ //| Release the resources we no longer need | //+-------------------------------------------------------------------+ void release_resources(void) { ExpertRemove(); } //+------------------------------------------------------------------+
初期化時に、必要な銘柄がすべて揃っているかどうかをチェックしました。以下の関数がその役割を担っています。銘柄の配列にあるすべての銘柄を繰り返し処理します。銘柄の選択に失敗した場合、この関数はfalseを返し、初期化手順を停止します。そうでなければ、この関数はtrueを返します。
//+------------------------------------------------------------------+ //| Validate that all the symbols we need are available | //+------------------------------------------------------------------+ bool validate_symbol(void) { for(int i=0; i < 5; i++) { //--- We failed to add one of the necessary symbols to the Market Watch window! if(!SymbolSelect(stocks[i],true)) { Comment("Failed to add ",stocks[i]," to the market watch. Ensure the symbol is available."); return(false); } } //--- Everything went fine return(true); }
この部門は、当社のポートフォリオにおけるポジションの開設と管理のプロセスを調整する役割を担っています。配列内のすべての銘柄を繰り返しチェックし、その市場にポジションがあるかどうか、またその市場にポジションを持つべきかどうかをチェックします。ポジションを持つべきなのにポジションがない場合、この関数はその市場で露出する機会をチェックするプロセスを開始します。それ以外の場合、この関数は何もしません。
//+------------------------------------------------------------------+ //| Check if we have any trade opportunities | //+------------------------------------------------------------------+ void check_trade_symbols(void) { //--- Loop through all the symbols we have for(int i=0;i < 5;i++) { //--- Select that symbol and check how many positons we have open if(SymbolSelect(stocks[i],true)) { //--- If we have no positions in that symbol, optimize the portfolio if((PositionsTotal() == 0) && (optimal_weights[i] != 0)) { optimize_portfolio(stocks[i],optimal_weights[i]); } } } }
ポートフォリオを最適化する関数は、2つのパラメータ、つまり、対象銘柄とその銘柄に帰属する重みをります。重みが正の場合、この関数は重みパラメータが満たされるまで、その市場でロングポジションを取る手続きを開始します。
//+------------------------------------------------------------------+ //| Optimize our portfolio | //+------------------------------------------------------------------+ void optimize_portfolio(string symbol,int weight) { //--- If the weight is less than 0, check if we have any oppurtunities to sell that stock if(weight < 0) { if(SymbolSelect(symbol,true)) { //--- If we have oppurtunities to sell, act on it if(check_sell(symbol, weight)) { Trade.Sell(trade_size,symbol,bid,0,0,"NASDAQ IC AI"); } } } //--- Otherwise buy else { if(SymbolSelect(symbol,true)) { //--- If we have oppurtunities to buy, act on it if(check_buy(symbol,weight)) { Trade.Buy(trade_size,symbol,ask,0,0,"NASDAQ IC AI"); } } } }
ここで、ロングポジションを持つための条件を定義しなければなりません。エントリーのタイミングは、テクニカル分析とプライスアクションの組み合わせに頼ります。価格水準がボリンジャーバンドの上限バンドを上回り、RSI水準が70を上回り、より高い時間軸での値動きが強気である場合にのみ、ロングポジションをエントリーします。同様に、これは高確率の設定であり、安全に利益目標を達成することができると考えます。最後に、その市場でのポジションの総数が、最適配分レベルを超えないことが最終条件です。条件が満たされれば、trueを返し、optimize_portfolio関数にロングポジションを持つ権限を与えます。
//+------------------------------------------------------------------+ //| Check for oppurtunities to buy | //+------------------------------------------------------------------+ bool check_buy(string symbol, int weight) { //--- Ensure we have selected the right symbol SymbolSelect(symbol,true); //--- Load the indicators on the symbol bb_handler = iBands(symbol,PERIOD_CURRENT,bb_period,0,1,PRICE_CLOSE); rsi_handler = iRSI(symbol,PERIOD_CURRENT,rsi_period,PRICE_CLOSE); //--- Validate the indicators if((bb_handler == INVALID_HANDLE) || (rsi_handler == INVALID_HANDLE)) { //--- Something went wrong return(false); } //--- Load indicator readings into the buffers bb_high_buffer.CopyIndicatorBuffer(bb_handler,1,0,1); rsi_buffer.CopyIndicatorBuffer(rsi_handler,0,0,1); current_close.CopyRates(symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,0,1); //--- Validate that we have a valid buy oppurtunity if((bb_high_buffer[0] < current_close[0]) && (rsi_buffer[0] > 70)) { return(false); } //--- Do we allready have enough positions if(PositionsTotal() >= weight) { return(false); } //--- We can open a position return(true); }
check_sell関数はcheck_buy関数と同じように機能しますが、重みにマイナス1を掛けることで、市場で開いているポジションの数を簡単にカウントできるようにします。この関数は、価格がボリンジャーバンドの安値の下にあり、RSIの数値が30未満であることを確認します。これら3つの条件が満たされた場合、さらに高い時間枠のプライスアクションがショートポジションをエントリーできることを確認する必要があります。
//+------------------------------------------------------------------+ //| Check for oppurtunities to sell | //+------------------------------------------------------------------+ bool check_sell(string symbol, int weight) { //--- Ensure we have selected the right symbol SymbolSelect(symbol,true); //--- Negate the weight weight = weight * -1; //--- Load the indicators on the symbol bb_handler = iBands(symbol,PERIOD_CURRENT,bb_period,0,1,PRICE_CLOSE); rsi_handler = iRSI(symbol,PERIOD_CURRENT,rsi_period,PRICE_CLOSE); //--- Validate the indicators if((bb_handler == INVALID_HANDLE) || (rsi_handler == INVALID_HANDLE)) { //--- Something went wrong return(false); } //--- Load indicator readings into the buffers bb_low_buffer.CopyIndicatorBuffer(bb_handler,2,0,1); rsi_buffer.CopyIndicatorBuffer(rsi_handler,0,0,1); current_close.CopyRates(symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,0,1); //--- Validate that we have a valid sell oppurtunity if(!((bb_low_buffer[0] > current_close[0]) && (rsi_buffer[0] < 30))) { return(false); } //--- Do we have enough trades allready open? if(PositionsTotal() >= weight) { //--- We have a valid sell setup return(false); } //--- We can go ahead and open a position return(true); }
図6:アルゴリズムのフォワードテスト
結論
このディスカッションでは、AIを活用してポジションサイズと資本配分をアルゴリズム的に決定する方法を示しました。ポートフォリオには、最適化可能なさまざまな側面があります。たとえば、リスク(分散)、業界ベンチマークとのパフォーマンス相関(ベータ)、リスク調整後のリターンなどが挙げられます。しかし、この例ではモデルをシンプルに保ち、リターンの最大化のみに焦点を当てました。連載を進める中で、これらの重要な指標を順次検討していく予定です。ただし、このシンプルな例を通じて、ポートフォリオ最適化の基礎となる主要なアイデアを把握できるようにしています。複雑な最適化手法を導入する場合でも、ここで説明した基本的な考え方は変わらないため、読者は引き続き自信を持って問題に取り組めるでしょう。このディスカッションで提供した情報が、常に成功を保証するものではないことを強調しておきます。しかし、アルゴリズムを活用して複数の銘柄を取引することに真剣に取り組む場合、本手法は検討する価値があります。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/15909
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索