Реализация фактора Януса в MQL5
Введение
Каждый трейдер знает, что рыночные цены следуют одной из двух общих моделей. Цены либо формируют тренды, либо движутся горизонтально. В результате стратегии участников рынка могут быть в значительной степени сведены либо к следованию тренду, либо к развороту. Фактор Януса — это теория поведения рынка, которая отражает эту двойственность. В статье мы раскроем его основы, а также продемонстрируем реализацию индикаторов, облегчающих этот метод анализа.
Обратная связь
Согласно теории Януса, рынки управляются взаимодействием между ценами и тем, как на них реагируют трейдеры. Андерсон сравнил это взаимодействие с системой обратной связи. Подробнее о ней можно узнать в Википедии. Когда рынки находятся в тренде, участники рынка демонстрируют уверенность, позволяя своей прибыли расти. При восходящем тренде более высокие цены подтверждают ожидания участников относительно тренда, что, в свою очередь, подстегивает покупки. Это приводит к еще большему росту цен.
Рисунок 1 иллюстрирует положительную обратную связь при восходящем тренде
При нисходящем тренде падающие цены могут вселить страх в трейдеров, заставляя их продавать, чтобы минимизировать убытки. Увеличение продаж оказывает давление на цены, толкая их дальше вниз. Таким образом, рыночные тренды являются примером положительной обратной связи. Движение цен будут ускоряться, пока не изменится реакция трейдеров на цену.
На рисунке 2 показана положительная обратная связь при нисходящем тренде
Отрицательная обратная связь наблюдается, когда трейдеры мало доверяют рынку, поэтому они предпочитают фиксировать прибыль сразу после любого движения цены. Ранняя фиксация прибыли ослабляет рыночный импульс, ограничивая величину движения цены. Добавьте к этому постоянную борьбу между быками и медведями, и вы получите стабилизацию цен.
Рис. 3. Отрицательная обратная связь на рынке
Движение капитала
Важным элементом этого понятия обратной связи являются типы символов, которые трейдеры предпочитают, когда преобладают разные условия. Анализ акций, проведенный Андерсоном, показывает, что при восходящем тренде трейдеры отдавали предпочтение акциям, показавшим относительно лучшие результаты, в то же время избавляясь от своих менее успешных аналогов. При нисходящем тренде акции с низкими доходами потеряли наибольшую стоимость, поскольку трейдеры нацеливались на них для фиксации прибыли.
В отсутствие тренда было немного различий между более сильными и более слабыми акциями, поскольку трейдеры выбирали определенные ценовые уровни для входа и выхода. Учитывая это, он предположил, что, анализируя относительную эффективность набора акций, можно будет обнаружить периоды отрицательной и положительной обратной связи.
Расчеты Януса
Для определения производительности рассчитываются периодические доходы. На основе доходности набора изучаемых акций выводится среднее значение, которое будет использоваться в качестве базовой меры. Ее можно назвать эталонным доходом. Совокупность оцениваемых акций называется индексом.
В книге Фактор Януса - Руководство по диалектике рынка для следующих за трендом Андерсон описывает различные показатели акций, рассчитанные на основе доходности, которые он использует, чтобы получить представление о поведении рынка.
Библиотека janus.mqh
Данные и подпрограммы, общие для всех вычислений, связанных с Janus, содержатся в janus.mqh. Включаемый файл содержит объявления для трех пользовательских типов:
Класс CSymboldata обрабатывает данные символов и связанные с ними операции.
//+------------------------------------------------------------------+ //|Class which manage the single Symbol | //+------------------------------------------------------------------+ class CSymbolData { private: string m_name; // name of the symbol ENUM_TIMEFRAMES m_timeframe;// timeframe int m_length; // length for copy rates MqlRates m_rates[]; // store rates datetime m_first; // first date on server or local history datetime SetFirstDate(void) { datetime first_date=-1; if((datetime)SymbolInfoInteger(m_name,SYMBOL_TIME)>0) first_date=(datetime)SeriesInfoInteger(m_name,m_timeframe,SERIES_FIRSTDATE); //--- if(first_date==WRONG_VALUE || first_date==0) { if(TerminalInfoInteger(TERMINAL_CONNECTED)) { while(!SeriesInfoInteger(m_name,m_timeframe,SERIES_SERVER_FIRSTDATE,first_date) && !IsStopped()) Sleep(10); } } //--- #ifdef DEBUG Print(m_name," FirstDate ",first_date); #endif return first_date; } public: CSymbolData(string name,ENUM_TIMEFRAMES tf=PERIOD_CURRENT) { m_name = name; m_length = 0; m_timeframe = tf; SymbolSelect(m_name,true); } ~CSymbolData(void) { ArrayFree(m_rates); } datetime GetFirstDate(void) { m_first = SetFirstDate(); return m_first; } string GetName(void) { return m_name; } int GetLength(void) { return m_length; } void SetLength(const int set_length) { if(set_length>0) { m_length=set_length; ArrayResize(m_rates,m_length,m_length); ArraySetAsSeries(m_rates,true); } } bool Update(void) { int copied = CopyRates(m_name,m_timeframe,0,m_length,m_rates); #ifdef DEBUG Print("copied ", copied, " requested ", m_length); #endif //-- return copied == m_length; }; MqlRates GetRateAtPos(const int i) { if(i<0 || i>=m_length) { #ifdef DEBUG Print("Array out of range ",i,".Size of array is ",m_length); #endif return (i<0)?m_rates[0]:m_rates[m_length-1]; } return m_rates[i]; } };CSymbolCollection представляет собой контейнер объектов CSymboldata, который представляет набор анализируемых символов. Код для обоих классов был адаптирован из листинга кодовой базы MQL5. Были внесены изменения в направление индексации базовых буферов. Следует отметить, что все классы реализуют формат индексации справа налево с нулевым индексом, указывающим на последний бар.
Чтобы продемонстрировать методы фактора Януса, мы применим его к анализу валютных пар. Первоначально Андерсон разработал этот метод для анализа акций, но доступность данных по символам акций у большинства брокеров непостоянна.
//+------------------------------------------------------------------+ //| Class that mange the collection of symbols | //+------------------------------------------------------------------+ class CSymbolCollection { private: int m_calculation_length; // global length ENUM_TIMEFRAMES m_collection_timeframe; // timeframe of data string m_raw_symbols; // delimited symbol list datetime m_synced_first; // synced first bar opentime for all symbols bool m_synced; // flag of whether all symbols are synchronized CSymbolData *m_collection[]; // Collection of Symbol Pointer int m_collection_length; // Collection of Symbol Length //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool CheckSymbolBars(const string __sym) { int bars=-1; bars=iBarShift(__sym,m_collection_timeframe,m_synced_first)+1;//SeriesInfoInteger(__sym,PERIOD_CURRENT,SERIES_BARS_COUNT); #ifdef DEBUG Print("Bars found in history for ",__sym," ",bars); #endif if(bars>=m_calculation_length) return(true); //--- return(SyncSymbol(__sym)); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool SyncSymbol(const string __sym) { //--- load data step by step bool downloaded=false; datetime times[1]; int bars=-1; /* if(MQLInfoInteger(MQL_PROGRAM_TYPE)==PROGRAM_INDICATOR) { #ifdef DEBUG Print(" cannot download ",__sym," history from an indicator"); #endif return(downloaded); }*/ #ifdef DEBUG Print(" downloading ",__sym," history"); #endif while(!IsStopped() && !downloaded && TerminalInfoInteger(TERMINAL_CONNECTED)) { //--- while(!SeriesInfoInteger(__sym,m_collection_timeframe,SERIES_SYNCHRONIZED) && !IsStopped()) Sleep(5); //--- bars=Bars(__sym,PERIOD_CURRENT); if(bars>=m_calculation_length) { downloaded=true; break; } //--- copying of next part forces data loading if(CopyTime(__sym,m_collection_timeframe,m_calculation_length-1,1,times)==1) { downloaded=true; break; } //--- Sleep(5); } #ifdef DEBUG if(downloaded) Print(bars," ",__sym," bars downloaded "); else Print("Downloading ",__sym," bars failed"); #endif return(downloaded); } public: CSymbolCollection(const ENUM_TIMEFRAMES tf=PERIOD_CURRENT) { m_raw_symbols=""; m_collection_length = 0; m_calculation_length = -1; m_synced_first=0; m_synced=false; m_collection_timeframe=tf; } ~CSymbolCollection(void) { for(int i=0; i<m_collection_length; i++) { if(CheckPointer(m_collection[i])==POINTER_DYNAMIC) delete m_collection[i]; } } //+------------------------------------------------------------------+ //|return the set timeframe for bars stored in the collection | //+------------------------------------------------------------------+ ENUM_TIMEFRAMES GetTimeFrame(void) { return(m_collection_timeframe); } //+------------------------------------------------------------------+ //|Checks the history available and syncs it across all symbols | //+------------------------------------------------------------------+ bool CheckHistory(const int size) { if(size<=0) return(false); int available=iBarShift(NULL,m_collection_timeframe,m_synced_first)+1; if(available<size) m_calculation_length=available; else m_calculation_length=size; #ifdef DEBUG Print("synced first date is ", m_synced_first); Print("Proposed size of history ",m_calculation_length); #endif if(m_calculation_length<=0) return(false); ResetLastError(); for(int i=0; i<m_collection_length; i++) { m_synced=CheckSymbolBars(m_collection[i].GetName()); if(!m_synced) { Print("Not Enough history data for ", m_collection[i].GetName(), " > ", GetLastError()); return(m_synced); } m_collection[i].SetLength(m_calculation_length); } return m_synced; } //+------------------------------------------------------------------+ //| Add a symbol by name to the collection | //+------------------------------------------------------------------+ int Add(string name) { CSymbolData *ref = new CSymbolData(name,m_collection_timeframe); datetime f=ref.GetFirstDate(); int found=GetIndex(name); if(f==WRONG_VALUE || found>-1) { #ifdef DEBUG if(f==WRONG_VALUE) Print("Failed to retrieve information for symbol ",name,". Symbol removed from collection"); if(found>-1) Print("Symbol ",name,"already part of collection"); #endif delete ref; return(m_collection_length); } ArrayResize(m_collection, m_collection_length + 1,1); m_collection[m_collection_length] = ref; //--- if(f>m_synced_first) m_synced_first=f; //--- return(++m_collection_length); } //+------------------------------------------------------------------+ //|Return symbol name | //+------------------------------------------------------------------+ string GetSymbolNameAtPos(int pos) { return m_collection[pos].GetName(); } //+------------------------------------------------------------------+ //|return index of symbol | //+------------------------------------------------------------------+ int GetIndex(const string symbol_name) { for(int i=0; i<m_collection_length; i++) { if(symbol_name==m_collection[i].GetName()) return(i); } //---fail return(-1); } //+------------------------------------------------------------------+ //| Return Collection length | //+------------------------------------------------------------------+ int GetCollectionLength(void) { return m_collection_length; } //+------------------------------------------------------------------+ //| Update every currency rates | //+------------------------------------------------------------------+ bool Update(void) { int i; for(i = 0; i < m_collection_length; i++) { bool res = m_collection[i].Update(); if(res==false) { Print("missing data on " + m_collection[i].GetName()); return false; } } return true; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ int GetHistoryBarsLength(void) { return m_calculation_length; } //+------------------------------------------------------------------+ //| Return MqlRates of currency at position | //+------------------------------------------------------------------+ MqlRates GetRateAtPos(int pos, int i) { return m_collection[pos].GetRateAtPos(i); } //+------------------------------------------------------------------+ //| Return Open price of currency at position | //+------------------------------------------------------------------+ double GetOpenAtPos(int pos, int i) { return m_collection[pos].GetRateAtPos(i).open; } //+------------------------------------------------------------------+ //| Return Close price of currency at position | //+------------------------------------------------------------------+ double GetCloseAtPos(int pos, int i) { return m_collection[pos].GetRateAtPos(i).close; } //+------------------------------------------------------------------+ //| Return High price of currency at position | //+------------------------------------------------------------------+ double GetHighAtPos(int pos, int i) { return m_collection[pos].GetRateAtPos(i).high; } //+------------------------------------------------------------------+ //| Return Low price of currency at position | //+------------------------------------------------------------------+ double GetLowAtPos(int pos, int i) { return m_collection[pos].GetRateAtPos(i).low; } //+------------------------------------------------------------------+ //| Return Median price of currency at position | //+------------------------------------------------------------------+ double GetMedianAtPos(int pos, int i) { return (GetHighAtPos(pos,i) + GetLowAtPos(pos, i))/2; } //+------------------------------------------------------------------+ //| Return Typical price of currency at position | //+------------------------------------------------------------------+ double GetTypicalAtPos(int pos, int i) { return (GetHighAtPos(pos,i) + GetLowAtPos(pos, i) + GetCloseAtPos(pos,i))/3; } //+------------------------------------------------------------------+ //| Return Weighted price of currency at position | //+------------------------------------------------------------------+ double GetWeightedAtPos(int pos, int i) { return (GetHighAtPos(pos,i) + GetLowAtPos(pos, i) + GetCloseAtPos(pos,i) * 2)/4; } }; //+------------------------------------------------------------------+
Код начинается с трех пользовательских перечислений, описанных ниже.
#include<Math\Stat\Math.mqh> #include <Arrays\ArrayObj.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ enum ENUM_INDEX_TYPE { INDEX_FOREX_MAJORS=0,//forex majors only list INDEX_CUSTOM,//custom symbol list }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ enum ENUM_PRICE { CLOSE=0,//close price MEDIAN,//median price TYPICAL,//typical price WEIGHTED//weighted price }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ enum ENUM_DIFF_TYPE { DIFF_PERCENT=0,//percent difference DIFF_LOG//log difference };
Перечисление | Детали | Настройки |
---|---|---|
ENUM_INDEX_TYPE | Тип символов, включенных в анализируемую коллекцию | INDEX_FOREX - создать коллекцию символов, состоящую из всех основных форекс-валют. Набор из 28 символов. INDEX_CUSTOM - указать набор символов. |
ENUM_PRICE | разрешить выбор ценового ряда для использования в расчетах. Информация о ценах хранится в виде рядов с последними ценами с индексом 0. | CLOSE - цена закрытия MEDIAN - медианная цена - high+low/2 TYPICAL - типичная цена - high+low+close/3 WEIGHTED - взвешенная цена high+low+close+close/4 |
ENUM_DIFF_TYPE | Различные методы дифференцирования, которые можно применить к выбранному ценовому ряду. | DIFF_LOG - использовать логарифмическую разность DIFF_PERCENT - использовать разность в процентах |
Чтобы использовать класс CJanus, необходимо освоить пять методов. После создания объекта CJanus и перед вызовом любого метода сначала должна быть вызвана функция-член Initialize. Метод делает две важные вещи - инициализирует объект CSymbolCollection и заполняет его выбранными символами, которые составят наш индекс. Затем он проверяет и синхронизирует бары истории символов в коллекции.
//+------------------------------------------------------------------+ //|Janus class for calculating janus family of indicators. | //+------------------------------------------------------------------+ class CJanus { private: CSymbolCollection* m_symbol_list; //object container of symbols ENUM_PRICE m_price_type; //applied price for calculations ENUM_DIFF_TYPE m_diff_type; //method of differencing applied ENUM_INDEX_TYPE m_index_type; //type of index int m_hist_size; // synchronized size of history across all selected symbols in collection ENUM_TIMEFRAMES m_list_timeframe; //timeframe for bars to be used in calculations //---private methods double market_return(const uint barshift, const uint symbolshift); void market_offense_defense(const uint barshift,const uint symbolshift,const uint rs_period,double& out_offense,double& out_defense); double rs(const uint barshift,const uint symbolshift,const uint rs_period,uint lag=0); double rs_off_def(const uint barshift, const uint symbolshift,const uint lag,const double median,const double index_offense, const double index_defense, double &array[]); //--- public: //constructor CJanus(void):m_symbol_list(NULL), m_price_type(WRONG_VALUE), m_diff_type(WRONG_VALUE), m_index_type(WRONG_VALUE), m_list_timeframe(WRONG_VALUE), m_hist_size(0) { } // destructor ~CJanus(void) { if(CheckPointer(m_symbol_list)==POINTER_DYNAMIC) delete m_symbol_list; } //public methods bool Initialize(const ENUM_PRICE set_price_type, const ENUM_DIFF_TYPE set_diff_type, const ENUM_INDEX_TYPE set_index_type, ENUM_TIMEFRAMES set_timeframe,const int history_size, const string symbol_list); bool Update(void); int HistorySize(void); string GetSymbolAt(const int sym_ind); int GetSymbolsTotal(void); double CalculateReturn(const uint barshift, const string symbol__); double CalculateBenchMarkReturn(const uint barshift); double CalculateSummedIndexReturn(const uint barshift); void CalculateBenchMarkOffenseDefense(const uint barshift,const uint rs_period,double& out_offense,double& out_defense); void CalculateMarketOffenseDefense(const uint barshift,const string symbol__,const uint rs_period,double& out_offense,double& out_defense); double CalculateRelativeStrength(const uint barshift,const string symbol__,const uint rs_period,uint lag=0); void CalculateRelativeStrengthLeaderLaggard(const uint barshift,const uint rs_period,const double rs_percent_top,double& leader,double& laggard); double CalculateRelativeStrengthSpread(const uint barshift,const uint rs_period,const double rs_percent_top); double CalculateRelativeStrengthSpreadChange(const uint barshift,const uint rs_period,const double rs_percent_top); };
Входные параметры метода Initialize() поясняются в таблице ниже.
Параметры | Детали | Тип данных |
---|---|---|
set_price_type | Перечисление, устанавливающее базовый ценовой ряд, который будет использоваться для всех расчетов | ENUM_PRICE |
set_diff_type | Метод дифференцирования, применяемый к ценовому ряду | ENUM_DIFF_TYPE |
set_index_type | Тип коллекции символов - либо пользовательский, либо только основные валюты форекс. Если выбран пользовательский, символы должны быть указаны в параметрах symbol_list | ENUM_INDEX_TYPE |
set_timeframe | Таймфрейм баров, которые будут использоваться в расчетах | ENUM_TIMEFRAME |
history_size | Максимальное количество баров, которое должно быть запрошено. Поскольку мы имеем дело с несколькими символами, это способ гарантировать, что все символы в коллекции имеют одинаковое количество доступных исторических данных баров. | integer |
symbol_list | Если для set_index_type установлено значение INDEX_CUSTOM, пользователь должен указать разделенный запятыми список символов, которые составят набор символов для анализа. | string |
- Метод Update() обновляет данные символа для получения последних котировок.
- Метод HistorySize() возвращает количество доступных баров истории. Обратите внимание, что это число может быть меньше указанного при вызове метода Initialize. Это связано с тем, что история по всем символам может различаться, поэтому функция возвращает синхронизированный размер истории.
- GetSymbolsTotals() возвращает количество символов, которые были добавлены и подтверждены как доступные в терминале.
- GetSymbolAt() получает имя символа по индексу.
Остальные методы с префиксом Calculate выполняют вычисления и возвращают одно или два значения. Все они имеют в качестве первого входного параметра сдвиг бара, на котором будет выполняться расчет.
Каждый метод будет описан по мере реализации различных индикаторов.
Измерение производительности
Как уже упоминалось, производительность измеряется путем расчета дохода. Для нашей реализации у нас есть возможность выбрать применяемую цену и метод, используемый для расчета прибыли. Вызов метода CalculateReturns() вычисляет доходность для заданного сдвига бара и символа.
//+------------------------------------------------------------------+ //|private method that calculates the returns | //+------------------------------------------------------------------+ double CJanus::market_return(const uint barshift, const uint symbolshift) { double curr,prev; curr=0; prev=1.e-60; switch(m_price_type) { case CLOSE: curr=m_symbol_list.GetCloseAtPos(symbolshift,barshift); prev=m_symbol_list.GetCloseAtPos(symbolshift,barshift+1); break; case MEDIAN: curr=m_symbol_list.GetMedianAtPos(symbolshift,barshift); prev=m_symbol_list.GetMedianAtPos(symbolshift,barshift+1); break; case TYPICAL: curr=m_symbol_list.GetTypicalAtPos(symbolshift,barshift); prev=m_symbol_list.GetTypicalAtPos(symbolshift,barshift+1); break; case WEIGHTED: curr=m_symbol_list.GetWeightedAtPos(symbolshift,barshift); prev=m_symbol_list.GetWeightedAtPos(symbolshift,barshift+1); break; default: return WRONG_VALUE; } if(prev==0) return(WRONG_VALUE); switch(m_diff_type) { case DIFF_PERCENT: return(((curr-prev)/prev)*100); case DIFF_LOG: return(MathLog(curr/prev)); default: return(WRONG_VALUE); } } //+------------------------------------------------------------------+ //|public method to calculate returns for single bar | //+------------------------------------------------------------------+ double CJanus::CalculateReturn(const uint barshift, const string symbol_) { int sshift=m_symbol_list.GetIndex(symbol_); if(sshift>-1) return(market_return(barshift,sshift)); else return(WRONG_VALUE); }
Код индикатора IndexReturns, приведенный ниже, отображает доходность символа графика, а также эталонную доходность.
//+------------------------------------------------------------------+ //| IndexReturns.mq5 | //| Copyright 2023, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Software Corp." #property link "https://www.mql5.com" #property version "1.00" #property indicator_separate_window #property indicator_buffers 2 #property indicator_plots 2 //--- plot IndexReturns #property indicator_label1 "IndexReturns" #property indicator_type1 DRAW_LINE #property indicator_color1 clrRed #property indicator_style1 STYLE_SOLID #property indicator_width1 1 //--- plot Returns #property indicator_label2 "Returns" #property indicator_type2 DRAW_LINE #property indicator_color2 clrBlue #property indicator_style2 STYLE_SOLID #property indicator_width2 1 #include<Janus.mqh> //inputs input ENUM_PRICE AppliedPrice=CLOSE; input ENUM_DIFF_TYPE AppliedDiffType=DIFF_LOG; input ENUM_INDEX_TYPE SelectIndexType=INDEX_FOREX_MAJORS; input string BenchMarkSymbols=""; input int MaxBars = 300; //--- indicator buffers double IndexReturnsBuffer[]; double ReturnsBuffer[]; CJanus *janus; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- indicator buffers mapping SetIndexBuffer(0,IndexReturnsBuffer,INDICATOR_DATA); SetIndexBuffer(1,ReturnsBuffer,INDICATOR_DATA); ArraySetAsSeries(IndexReturnsBuffer,true); ArraySetAsSeries(ReturnsBuffer,true); //--- PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,0.0); PlotIndexSetDouble(1,PLOT_EMPTY_VALUE,0.0); PlotIndexSetString(1,PLOT_LABEL,_Symbol+" Returns"); //--- IndicatorSetInteger(INDICATOR_DIGITS,8); IndicatorSetString(INDICATOR_SHORTNAME,"IndexReturns("+_Symbol+")"); //--- janus=new CJanus(); //--- if(!janus.Initialize(AppliedPrice,AppliedDiffType,SelectIndexType,PERIOD_CURRENT,MaxBars,BenchMarkSymbols)) return(INIT_FAILED); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //|Custom indicator deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- switch(reason) { case REASON_INITFAILED: ChartIndicatorDelete(ChartID(),ChartWindowFind(),"IndexReturns("+_Symbol+")"); break; default: break; } //--- if(CheckPointer(janus)==POINTER_DYNAMIC) delete janus; } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- int limit; if(prev_calculated<=0) { limit=janus.HistorySize()-2; PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,rates_total-limit+1); PlotIndexSetInteger(1,PLOT_DRAW_BEGIN,rates_total-limit+1); } else limit=rates_total-prev_calculated; //--- if(!janus.Update()) return(prev_calculated); for(int bar=limit;bar>=1;bar--) { ReturnsBuffer[bar]=janus.CalculateReturn(bar,_Symbol); IndexReturnsBuffer[bar]=janus.CalculateBenchMarkReturn(bar); } //--- return value of prev_calculated for next call return(rates_total); } //+------------------------------------------------------------------+
Индикатор IndexReturns
Эталон
Чтобы рассчитать эталонную доходность, мы используем индивидуальную доходность символов для каждого соответствующего момента времени, чтобы получить медианное значение. Этот расчет производится с помощью метода CalculateBenchmarkReturns().
//+------------------------------------------------------------------+ //|public method to calculate index returns | //+------------------------------------------------------------------+ double CJanus::CalculateBenchMarkReturn(const uint barshift) { double sorted[]; int size=m_symbol_list.GetCollectionLength(); if(size<=0) return(WRONG_VALUE); ArrayResize(sorted,size); for(int i=0; i<size; i++) { sorted[i]=market_return(barshift,i); } if(!ArraySort(sorted)) { Print("sorting error ",__LINE__," ",__FUNCTION__); return(0); } return(MathMedian(sorted)); }
Атака и защита
Чтобы лучше понять относительную производительность, Андерсон придумал для символа понятие очков атаки (offense) и защиты (defense). Очки атаки — это производительность, достигнутая, когда эталонная доходность выше средней.
Очки защиты рассчитываются аналогичным образом с использованием доходности, полученной, когда эталонная доходность ниже средней. Это значение показывает, насколько хорошо символ держится, когда рыночные цены колеблются.
Чтобы рассчитать эти значения, эталонные доходности делятся на две части: эталонные доходности атаки и защиты. Они рассчитываются путем выбора подходящей длины окна, на основе которой рассчитывается средний ориентир.
В классе CJanus медианное значение используется как среднее по указанному окну. Эталонная доходность атаки достигается путем накопления разницы между достигнутой доходностью и средней эталонной доходностью, при этом достигнутая доходность выше средней или равна ей.
Эталонная доходность защиты рассчитывается аналогичным образом с использованием эталонных значений, меньших, чем среднее значение окна.
//+------------------------------------------------------------------+ //|public method to calculate Index offense and defense scores | //+------------------------------------------------------------------+ void CJanus::CalculateBenchMarkOffenseDefense(const uint barshift,const uint rs_period,double& out_offense,double& out_defense) { out_defense=out_offense=WRONG_VALUE; double index[],sorted[],median,i_offense,i_defense; median=i_offense=i_defense=0; ArrayResize(index,rs_period); ArrayResize(sorted,rs_period); int begin=0; for(int i=0; i<(int)rs_period; i++) { index[i]=CalculateBenchMarkReturn(barshift+i); if(i>=begin) sorted[i-begin]=index[i]; } if(!ArraySort(sorted)) { Print("sorting error ",__LINE__," ",__FUNCTION__); return; } median=MathMedian(sorted); i_offense=1.e-30; i_defense=-1.e-30; for(int i=begin; i<(int)rs_period; i++) { if(index[i]>=median) i_offense+=index[i]-median; else i_defense+=index[i]-median; } if(i_offense<0 || i_defense>0) { #ifdef DEBUG Print("error invalid figures ","Offensive ",i_offense," Defensive ",i_defense); #endif return; } out_offense=i_offense; out_defense=i_defense; return; }
После того, как были рассчитаны эталонные доходности атаки и защиты, они используются для расчета очков атаки и защиты символа. В том же окне, которое используется для эталонных показателей, каждый раз, когда эталонная доходность больше или равна среднему значению окна, достигнутые доходности складываются вместе. Затем эта сумма выражается в виде доли соответствующего эталонного показателя атаки и умножается на 100.
Аналогичные вычисления выполняются с использованием эталонной доходности защиты. Метод CalculateSymbolOffenseDefense() реализует подсчет как очков атаки, так и очков защиты.
//+------------------------------------------------------------------+ //|public method to calculate market offense and defense | //+------------------------------------------------------------------+ void CJanus::CalculateSymbolOffenseDefense(const uint barshift,const string symbol__,const uint rs_period,double &out_offense,double &out_defense) { out_defense=out_offense=0.0; int sshift=m_symbol_list.GetIndex(symbol__); if(sshift>-1) market_offense_defense(barshift,sshift,rs_period,out_offense,out_defense); else return; }
//+------------------------------------------------------------------+ //|private method that calculates market defense and offense values | //+------------------------------------------------------------------+ void CJanus::market_offense_defense(const uint barshift,const uint symbolshift,const uint rs_period,double& out_offense,double& out_defense) { out_defense=out_offense=0.0; double index[],sorted[],median,i_offense,i_defense; median=i_offense=i_defense=0; ArrayResize(index,rs_period); ArrayResize(sorted,rs_period); int begin=0; for(int i=0; i<(int)rs_period; i++) { index[i]=CalculateBenchMarkReturn(barshift+i); if(i>=begin) sorted[i-begin]=index[i]; } if(!ArraySort(sorted)) { Print("sorting error ",__LINE__," ",__FUNCTION__); return; } median=MathMedian(sorted); i_offense=1.e-30; i_defense=-1.e-30; for(int i=begin; i<(int)rs_period; i++) { if(index[i]>=median) i_offense+=index[i]-median; else i_defense+=index[i]-median; } if(i_offense<0 || i_defense>0) { #ifdef DEBUG Print("error invalid figures ","Offensive ",i_offense," Defensive ",i_defense); #endif return; } double m_offense,m_defense; m_offense=m_defense=0; for(int i=0; i<(int)rs_period; i++) { if(index[i]>=median) m_offense+=market_return(barshift+i,symbolshift); else m_defense+=market_return(barshift+i,symbolshift); } out_defense= (m_defense/i_defense) * 100; out_offense= (m_offense/i_offense) * 100; }
Нанося на график очки атаки и защиты, Андерсон заметил закономерности, которые возникали в зависимости от преобладающих рыночных условий. Во время положительной обратной связи очки были сильно разбросаны со значительной разницей между символами с лучшей и худшей производительностью. В периоды отрицательной обратной связи разница между лучшими и худшими исполнителями сокращалась. Андерсон назвал это расширением и сужением индекса.
В приведенном ниже коде описывается скрипт OffensiveDefensiveScatterPlot. Он имеет те же входные данные, что и приведенный выше индикатор, и рисует графики очков атаки и защиты в виде анимации.
//+------------------------------------------------------------------+ //| OffensiveDefensiveScatterPlot.mq5 | //| Copyright 2023, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Software Corp." #property link "https://www.mql5.com" #property version "1.00" #property script_show_inputs #include <Graphics\Graphic.mqh> #include<Janus.mqh> input ENUM_PRICE AppliedPrice=CLOSE; input ENUM_DIFF_TYPE AppliedDiffType=DIFF_LOG; input ENUM_INDEX_TYPE SelectIndexType=INDEX_FOREX_MAJORS; input uint AppliedPeriod = 25; input string BenchMarkSymbols=""; input int MaxBars = 50; CJanus janus; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- if(!janus.Initialize(AppliedPrice,AppliedDiffType,SelectIndexType,PERIOD_CURRENT,MaxBars,BenchMarkSymbols)) { Print("error with janus object"); return; } //--- janus.Update(); //--- double y[]; double x[]; int size=janus.GetSymbolsTotal(); ArrayResize(x,size); ArrayResize(y,size); long chart=0; string name="OffenseDefense"; for(int k=MaxBars-(int)AppliedPeriod-1; k>=0; k--) { for(int i=0; i<size; i++) { string ssy=janus.GetSymbolAt(i); janus.CalculateSymbolOffenseDefense(k,ssy,AppliedPeriod,y[i],x[i]); } CGraphic graphic; if(ObjectFind(chart,name)<0) graphic.Create(chart,name,0,0,0,780,380); else graphic.Attach(chart,name); //--- graphic.CurveAdd(x,y,ColorToARGB(clrBlue),CURVE_POINTS,"DefensiveOffensive "); //--- graphic.CurvePlotAll(); //--- graphic.Update(); Sleep(1*1000); graphic.Destroy(); ChartRedraw(); } ChartSetInteger(0,CHART_SHOW,true); } //+------------------------------------------------------------------+
Ниже приведена диаграмма, полученная с помощью скрипта
Относительная сила
Когда наступательные и защитные оценки символа связаны с помощью приведенной ниже формулы, мы получаем относительную силу символа.
Вывод этой формулы задокументирован в книге Андерсона. Относительная сила вычисляется методом CalculateRelativeStrength().
//+------------------------------------------------------------------+ //|public method to calculate relative strength | //+------------------------------------------------------------------+ double CJanus::CalculateRelativeStrength(const uint barshift,const string symbol__,const uint rs_period,uint lag=0) { int sshift=m_symbol_list.GetIndex(symbol__); if(sshift>-1) return(rs(barshift,sshift,rs_period,lag)); else return(WRONG_VALUE); }
//+------------------------------------------------------------------+ //|private method that calculates the relative strength | //+------------------------------------------------------------------+ double CJanus::rs(const uint barshift,const uint symbolshift,const uint rs_period,uint lag=0) { if(lag>=rs_period) return(WRONG_VALUE); double index[],sorted[],median,i_offense,i_defense; median=i_offense=i_defense=0; ArrayResize(index,rs_period); ArrayResize(sorted,rs_period); int begin=(int)lag; for(int i=0; i<(int)rs_period; i++) { index[i]=CalculateBenchMarkReturn(barshift+i); if(i>=begin) sorted[i-begin]=index[i]; } if(!ArraySort(sorted)) { Print("sorting error ",__LINE__," ",__FUNCTION__); return(EMPTY_VALUE); } median=MathMedian(sorted); i_offense=1.e-30; i_defense=-1.e-30; for(int i=begin; i<(int)rs_period; i++) { if(index[i]>=median) i_offense+=index[i]-median; else i_defense+=index[i]-median; } if(i_offense<0 || i_defense>0) { #ifdef DEBUG Print("error invalid figures ","Offensive ",i_offense," Defensive ",i_defense); #endif return(WRONG_VALUE); } return(rs_off_def(barshift,symbolshift,lag,median,i_offense,i_defense,index)); }
Приведенный ниже код индикатора отображает относительную силу символа графика.
//+------------------------------------------------------------------+ //| RelativeStrength.mq5 | //| Copyright 2023, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Software Corp." #property link "https://www.mql5.com" #property version "1.00" #property indicator_separate_window #property indicator_buffers 1 #property indicator_plots 1 //--- plot RelativeStrength #property indicator_label1 "RelativeStrength" #property indicator_type1 DRAW_LINE #property indicator_color1 clrRed #property indicator_style1 STYLE_SOLID #property indicator_width1 1 #include<Janus.mqh> //inputs input ENUM_PRICE AppliedPrice=CLOSE; input ENUM_DIFF_TYPE AppliedDiffType=DIFF_LOG; input ENUM_INDEX_TYPE SelectIndexType=INDEX_FOREX_MAJORS; input uint AppliedPeriod = 7; input string BenchMarkSymbols=""; input int MaxBars = 300; //--- indicator buffers double RelativeStrengthBuffer[]; //--- CJanus *janus; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- indicator buffers mapping SetIndexBuffer(0,RelativeStrengthBuffer,INDICATOR_DATA); //--- ArraySetAsSeries(RelativeStrengthBuffer,true); //--- PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,0.0); IndicatorSetString(INDICATOR_SHORTNAME,"RS("+_Symbol+")("+string(AppliedPeriod)+")"); //--- janus=new CJanus(); //--- if(!janus.Initialize(AppliedPrice,AppliedDiffType,SelectIndexType,PERIOD_CURRENT,MaxBars,BenchMarkSymbols)) return(INIT_FAILED); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //|Custom indicator deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- switch(reason) { case REASON_INITFAILED: ChartIndicatorDelete(ChartID(),ChartWindowFind(),"RS("+_Symbol+")("+string(AppliedPeriod)+")"); break; default: break; } //--- if(CheckPointer(janus)==POINTER_DYNAMIC) delete janus; } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[],p const long &tick_volume[], const long &volume[], const int &spread[]) { //--- int limit; if(prev_calculated<=0) { limit=janus.HistorySize()-int(AppliedPeriod+2); PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,rates_total-limit+1); } else limit=rates_total-prev_calculated; //--- if(!janus.Update()) return(prev_calculated); for(int i=limit;i>=1;i--) { RelativeStrengthBuffer[i]=janus.CalculateRelativeStrength(i,_Symbol,AppliedPeriod); } //--- return value of prev_calculated for next call return(rates_total); } //+------------------------------------------------------------------+
Индикатор относительной силы
Используя значения относительной силы, мы получаем более четкое представление о расширении и сжатии, происходящих с течением времени. Это показано ниже на графике индикатора самых высоких и самых низких значений относительной силы.
Код индикатора показан ниже.
//+------------------------------------------------------------------+ //| RelativeStrenghtBestWorst.mq5 | //| Copyright 2023, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Software Corp." #property link "https://www.mql5.com" #property version "1.00" #property indicator_separate_window #property indicator_buffers 2 #property indicator_plots 2 //--- plot Upper #property indicator_label1 "Upper" #property indicator_type1 DRAW_LINE #property indicator_color1 clrBlue #property indicator_style1 STYLE_SOLID #property indicator_width1 1 //--- plot Lower #property indicator_label2 "Lower" #property indicator_type2 DRAW_LINE #property indicator_color2 clrRed #property indicator_style2 STYLE_SOLID #property indicator_width2 1 #include<Janus.mqh> //inputs input ENUM_PRICE AppliedPrice=CLOSE; input ENUM_DIFF_TYPE AppliedDiffType=DIFF_LOG; input ENUM_INDEX_TYPE SelectIndexType=INDEX_FOREX_MAJORS; input uint AppliedPeriod = 25; input string BenchMarkSymbols=""; input int MaxBars = 300; //--- indicator buffers double UpperBuffer[]; double LowerBuffer[]; double rsv[]; CJanus *janus; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- indicator buffers mapping SetIndexBuffer(0,UpperBuffer,INDICATOR_DATA); SetIndexBuffer(1,LowerBuffer,INDICATOR_DATA); //--- PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,0.0); PlotIndexSetDouble(1,PLOT_EMPTY_VALUE,0.0); //--- ArraySetAsSeries(UpperBuffer,true); ArraySetAsSeries(LowerBuffer,true); //--- IndicatorSetString(INDICATOR_SHORTNAME,"RS_Upperlower("+_Symbol+")("+string(AppliedPeriod)+")"); //--- janus=new CJanus(); //--- if(!janus.Initialize(AppliedPrice,AppliedDiffType,SelectIndexType,PERIOD_CURRENT,MaxBars,BenchMarkSymbols)) return(INIT_FAILED); ArrayResize(rsv,janus.GetSymbolsTotal()); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //|Custom indicator deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- switch(reason) { case REASON_INITFAILED: ChartIndicatorDelete(ChartID(),ChartWindowFind(),"RS_Upperlower("+_Symbol+")("+string(AppliedPeriod)+")"); break; default: break; } //--- if(CheckPointer(janus)==POINTER_DYNAMIC) delete janus; } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- int limit; if(prev_calculated<=0) { limit=janus.HistorySize()-int(AppliedPeriod+2); PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,rates_total-limit+1); } else limit=rates_total-prev_calculated; //--- if(!janus.Update()) return(prev_calculated); for(int i=limit;i>=1;i--) { for(int k=0;k<ArraySize(rsv);k++) { string sym=janus.GetSymbolAt(k); rsv[k]= janus.CalculateRelativeStrength(i,sym,AppliedPeriod); } ArraySort(rsv); UpperBuffer[i]=rsv[ArraySize(rsv)-1]; LowerBuffer[i]=rsv[0]; } //--- return value of prev_calculated for next call return(rates_total); } //+------------------------------------------------------------------+
Лидеры и отстающие по относительной силе
Когда относительная производительность лучших и худших исполнителей усредняется, мы получаем относительную силу лидеров и относительную силу отстающих соответственно. Эти значения указывают на лучшее время для торговли в зависимости от состояния обратной связи. Относительная сила (rs) лидеров и отстающих рассчитывается путем указания количества лучших и худших исполнителей для расчета среднего значения.
//+------------------------------------------------------------------+ //|public method to calculate relative strength leaders and laggards | //+------------------------------------------------------------------+ void CJanus::CalculateRelativeStrengthLeaderLaggard(const uint barshift,const uint rs_period,const double rs_percent_top,double& leader,double& laggard) { leader=laggard=0; uint lag=rs_period; double sorted[]; int iwork[],k,n,isub; k=isub=-1; int size=m_symbol_list.GetCollectionLength(); ArrayResize(sorted,size); ArrayResize(iwork,size); for(int i=0; i<size; i++) { sorted[i]=rs(barshift,uint(i),rs_period,lag); iwork[i]=i; } MathQuickSortAscending(sorted,iwork,0,size-1); k=(int)(rs_percent_top*(size+1))-1; if(k<0) k=0; n=k+1; while(k>=0) { isub=iwork[k]; for(uint i=0; i<lag; i++) { laggard+=market_return(barshift+i,isub); } isub=iwork[size-1-k]; for(uint i=0; i<lag; i++) { leader+=market_return(barshift+i,isub); } --k; } leader/=n*lag; laggard/=n*lag; return; }
Входной параметр rs_percent_top для метода CalculateRelativeStrengthLeadersLaggards() обозначает долю символов, используемых для расчета усредненной относительной силы. Например, установка rs_percent_top на 0,1 означает, что верхние и нижние 10% символов будут использоваться при расчете лидеров и отстающих.
Ниже скриншот индикатора, отображающего лидеров и отстающих (Leaders and Laggards)
Разброс
Когда на рынках господствует положительная обратная связь, самые слабые и самые сильные ценные бумаги расходятся в зависимости от направления тренда. При восходящем тренде трейдеры отдают предпочтение более сильным ценным бумагам, а при нисходящем тренде продают более слабые ценные бумаги.
Если цены застряли в диапазоне, разница между самыми слабыми и самыми сильными ценными бумагами относительно невелика, то есть происходит конвергенция. Чтобы распознать эти изменения, мы вычисляем среднюю разницу в относительной силе между самой слабой и самой сильной ценными бумагами. Это то, что измеряет разброс относительной силы.
Разброс относительной силы реализован в методе CalculateRelativeStrengthSpread().
//+------------------------------------------------------------------+ //|public method to calculate the relative strength spread. | //+------------------------------------------------------------------+ double CJanus::CalculateRelativeStrengthSpread(const uint barshift,const uint rs_period,const double rs_percent_top) { double sorted[],width,div; int k=0; int size=m_symbol_list.GetCollectionLength(); width=div=0; ArrayResize(sorted,size); for(int i=0; i<size; i++) { sorted[i]=rs(barshift,uint(i),rs_period); } if(!ArraySort(sorted)) { Print("sorting error ",__LINE__," ",__FUNCTION__); return(WRONG_VALUE); } k=(int)(rs_percent_top*(size+1))-1; if(k<0) k=0; double n=double(k+1); while(k>=0) { width+=sorted[size-1-k]-sorted[k]; --k; } return(width/=n); }
Индикатор разброса относительной силы
Комбинация индикаторов разброса и лидеров/отстающих может быть использована для разработки конкретных правил торговли. По мнению Андерсона, наилучшие возможности открываются при торговле символами, которые находятся вблизи или на верхней и нижней границах относительной силы рассматриваемого индекса.
//+------------------------------------------------------------------+ //|public method to calculate relative strength leaders and laggards | //+------------------------------------------------------------------+ void CJanus::CalculateRelativeStrengthLeaderLaggard(const uint barshift,const uint rs_period,const double rs_percent_top,double& leader,double& laggard) { leader=laggard=0; uint lag=1; double sorted[]; int iwork[],k,n,isub; k=isub=-1; int size=m_symbol_list.GetCollectionLength(); ArrayResize(sorted,size); ArrayResize(iwork,size); for(int i=0; i<size; i++) { sorted[i]=rs(barshift,uint(i),rs_period,lag); iwork[i]=i; } MathQuickSortAscending(sorted,iwork,0,size-1); k=(int)(rs_percent_top*(size+1))-1; if(k<0) k=0; n=k+1; while(k>=0) { isub=iwork[k]; for(uint i=0; i<lag; i++) { laggard+=market_return(barshift+i,isub); } isub=iwork[size-1-k]; for(uint i=0; i<lag; i++) { leader+=market_return(barshift+i,isub); } --k; } leader/=n*lag; laggard/=n*lag; return; }
Когда тренд спреда растет в сочетании с лидерами rs, это указывает на положительную обратную связь, а также на чистую покупку сильными исполнителями. Подходящее время для покупки символов, демонстрирующих наибольшую относительную силу. Если вместо этого растут отстающие rs, то нацельтесь на более слабые символы для продажи. В любой из этих ситуаций может быть целесообразно объединить эти правила с эффективным методом входа, который будет применяться к выбранному(ым) символу(ам).
Другой вариант — использовать индикатор разброса в качестве фильтра риска. Когда есть положительная обратная связь, рынок менее рискован, а отрицательная обратная связь является индикатором возможных изменчивых условий. В такое время лучше оставаться вне рынка.
Заключение
Хотя анализ форекс-символов может быть не лучшим применением фактора Януса, весь смысл статьи в том, чтобы познакомить читателей с методом. Более подробную информацию о нем можно найти в книге Андерсона, ссылка на которую приведена ниже.Прилагаемый zip-файл содержит весь код инструментов, упомянутых в статье. В таблице ниже перечислены все инструменты.
Чтобы индикаторы работали, пользователи должны сначала убедиться, что исторические данные для всех символов, составляющих индекс, доступны.
Имя файла | Тип | Описание |
---|---|---|
Mql5/include/Janus.mqh | Включаемый файл | Включаемый файл с определениями классов CSymbolData, CSymbolCollection и CJanus |
Mql5/scripts/OffensiveDefensiveScatterPlot.mq5 | Скрипт | Скрипт, рисующий анимированный точечный график очков атаки/защиты |
Mql5/indicators/IndexReturns.mq5 | Индикатор | Код индикатора, отображающего символы и эталонную доходность |
Mql5/indicators/RelativeStrengthBestWorst.mq5 | Индикатор | Индикатор, показывающий график самых высоких и самых низких значений относительной силы. |
Mql5/indicators/RelativeStrength.mq5 | Индикатор | Индикатор относительной силы символа |
Mql5/indicators/RelativeStrengthSpread.mq5 | Индикатор | Индикатор разброса относительной силы |
Mql5/indicatorr/RssLeaderlaggards.mq5 | Индикатор | Индикатор, отображающий лидеров и отстающих по относительной силе |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/12328
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования