
Анализ нескольких символов с помощью Python и MQL5 (Часть I): Производители интегральных схем NASDAQ
У инвестора есть много способов диверсифицировать свой портфель. Кроме того, существует множество различных показателей, которые можно использовать в качестве критерия того, насколько хорошо оптимизирован портфель. Маловероятно, что у какого-либо инвестора будет достаточно времени или ресурсов, чтобы тщательно рассмотреть все варианты, прежде чем принять столь важное решение. В этой серии статей мы расскажем вам о множестве возможностей, которые откроются перед вами на пути к одновременной торговле несколькими символами. Наша цель — помочь вам решить, какими стратегиями следует пользоваться, а какие вам не подойдут.
Обзор торговой стратегии
Мы выбрали акции, которые фундаментально связаны друг с другом. Мы выбрали 5 акций компаний, которые разрабатывают и продают интегральные схемы. Это Broadcom, Cisco, Intel, NVIDIA и Comcast. Все 5 компаний котируются на бирже NASDAQ. NASDAQ (National Association of Securities Dealers Automated Quotations, Система автоматической котировки Национальной ассоциации биржевых дилеров) была основана в 1971 году и является крупнейшей биржей в США по объему торгов.
Интегральные схемы стали неотъемлемой частью нашей повседневной жизни. Электронные чипы пронизывают все аспекты современной жизни: от серверов MetaQuotes, на которых размещен этот самый веб-сайт, на котором вы читаете эту статью, до устройства, которое вы используете для чтения этой статьи. Все эти устройства используют технологию, которая, скорее всего, разработана одной из этих 5 компаний. Первая в мире интегральная схема была разработана компанией Intel. Она получила название Intel 4004 и была выпущена в 1971 году, в том же году, когда была основана биржа NASDAQ. В процессоре Intel 4004 было около 2600 транзисторов, что значительно меньше, чем в современных чипах, в которых их число может достигать миллиардов.
Поскольку нас мотивирует глобальный спрос на интегральные схемы, мы хотим разумно выйти на рынок микросхем. Рассмотрев корзину из этих 5 акций, мы покажем, как максимизировать доходность вашего портфеля, разумно распределяя капитал между ними. Традиционный подход равномерного распределения капитала между всеми 5 акциями не будет достаточным на современных нестабильных рынках. Вместо этого мы построим модель, которая будет информировать нас о том, следует ли нам покупать или продавать каждую акцию, а также об оптимальных объемах, которыми нам следует торговать. Другими словами, мы используем имеющиеся у нас данные для алгоритмического определения размера и количества наших позиций.
Обзор методологии
Мы начали с извлечения 100 000 строк рыночных данных M1 для каждой из 5 акций в нашей корзине из терминала MetaTrader 5 с помощью библиотеки MetaTrader 5 Python. После преобразования обычных данных о ценах в процентные изменения мы провели исследовательский анализ данных о доходности рынка.
Мы наблюдали слабую корреляцию между пятью акциями. Более того, блочные диаграммы ясно показали, что средняя доходность каждой акции была близка к 0. Мы также наложили друг на друга доходность каждой акции и четко увидели, что доходность акций NVIDIA оказалась наиболее волатильной. Наконец, мы создали парные графики по всем 5 выбранным нами акциям и, к сожалению, не смогли обнаружить никакой заметной взаимосвязи, из которой можно было бы извлечь пользу.
Затем мы использовали библиотеку SciPy, чтобы найти оптимальные веса для каждой из 5 акций в нашем портфеле. Мы разрешим всем 5 весам находиться в диапазоне от -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 поддерживают ограничения равенства. Ограничения равенства сообщают модулю SciPy, что мы хотели бы, чтобы эта функция была равна 0. Поэтому мы хотим, чтобы разница между абсолютным значением наших весов и 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, чтобы отбросить все десятичные знаки. Оставшиеся целые числа можно интерпретировать как количество позиций, которые нам следует открыть на каждом рынке. Наши данные указывают на то, что нам следует открыть 3 длинные позиции по Broadcom, 1 длинную позицию по Cisco, 1 короткую позицию по Comcast, ни одной позиции по Intel и 4 короткие позиции по NVIDIA, чтобы максимизировать нашу прибыль.
#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/en/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Zororo Ndawana" #property link "https://www.mql5.com/en/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;
Конечный пользователь нашей программы может настраивать поведение советника с помощью входных данных, которые мы позволяем ему контролировать.
//+------------------------------------------------------------------+ //| 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(); }
Всякий раз, когда мы получаем обновленные цены, мы сначала хотели бы сохранить текущие цены bid и ask в наших глобально определенных переменных, проверить наличие торговых возможностей и, наконец, забрать всю имеющуюся у нас прибыль.
//+------------------------------------------------------------------+ //| 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); }
Всякий раз, когда наш советник не используется, мы освобождаем ресурсы, которые ему больше не нужны, чтобы обеспечить удобство работы конечного пользователя.
//+-------------------------------------------------------------------+ //| 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]); } } } }
Функция оптимизации портфеля принимает два параметра: рассматриваемую акцию и веса, присвоенные этой акции. Если веса положительны, функция начнет процедуру открытия длинной позиции на этом рынке до тех пор, пока не будет достигнут параметр веса; для отрицательных весов верно обратное.
//+------------------------------------------------------------------+ //| 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. Если эти три условия выполнены, нам также необходимо убедиться, что поведение цены на более высоких таймфреймах позволяет нам открыть короткую позицию.
//+------------------------------------------------------------------+ //| 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. Форвард-тестирование алгоритма
Заключение
Мы продемонстрировали, как можно алгоритмически определить размер позиции и распределение капитала с помощью ИИ. Существует множество различных аспектов портфеля, которые мы можем оптимизировать, например, риск (дисперсия) портфеля, корреляция нашего портфеля с показателями отраслевого эталона (бета) и доходность портфеля с поправкой на риск. В нашем примере мы сохранили простоту модели и рассматривали только максимизацию прибыли. В дальнейшем мы рассмотрим множество важных показателей в этой серии статей. Однако этот простой пример позволяет нам уловить основные идеи, лежащие в основе оптимизации портфеля, а когда мы перейдем к сложным процедурам оптимизации, читатель сможет с уверенностью приступить к решению проблемы, зная, что основные идеи, которые мы здесь изложили, не изменятся. Хотя мы не можем гарантировать, что информация, содержащаяся в нашем обсуждении, будет постоянно приносить пользу, ее, безусловно, стоит рассмотреть, если вы серьезно настроены торговать несколькими символами с использованием алгоритмов.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/15909





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования