Implementando OLAP na negociação (Parte 3): analisando cotações para desenvolver estratégias de negociação
Neste artigo, continuaremos a ver a aplicação da tecnologia OLAP (On-Line Analytical Processing) na negociação. Nos dois primeiros artigos, descrevemos técnicas gerais para construir classes para coleta e análise de dados multidimensionais, também falamos sobre a visualização dos resultados de análise numa interface gráfica. Ambos os artigos abordavam o processamento de relatórios de negociação vindos de diferentes fontes, como o testador de estratégia, o histórico de negociação on-line, arquivos HTML e CSV (incluindo sinais de negociação MQL5). No entanto, o OLAP pode ser aplicado em outras áreas, nomeadamente na análise de cotações e no desenvolvimento de estratégias de negociação.
Introdução
Lembremo-nos do que foi implementado nos artigos anteriores (para aqueles que não os leram por um motivo ou outro, é bom que o façam). O núcleo estava no arquivo OLAPcube.mqh, que continha:
- todas as principais classes de seletores e agregadores;
- classes de registros de trabalho contendo dados de origem (Record base abstrato e vários descendentes especializados de TradeRecord contendo dados de transação);
- adaptador básico para ler as diferentes fontes de dados (abstratas) e gerar matrizes de registros de trabalho a partir delas;
- adaptador específico para o histórico de negociação da conta HistoryDataAdapter;
- classe base para exibir resultados e sua implementação mais simples (Display, LogDisplay);
- painel de controle único na forma de classe Analyst que vinculava o adaptador, o agregador e a tela;
As coisas específicas relacionadas aos relatórios HTML eram exportadas para o arquivo HTMLcube.mqh, que, em particular, definia as classes de transação do relatório HTML HTMLTradeRecord e do HTMLReportAdapter que as gerava.
Da mesma forma, ao arquivo CSVcube.mqh eram adicionadas as classes para transações a partir dos relatórios CSV CSVTradeRecord, além disso, era acrescentado um adaptador CSVReportAdapter para elas.
Por fim, para simplificar a integração do OLAP aos programas MQL5, foi escrito o arquivo OLAPcore.mqh com a classe-wrapper de toda a funcionalidade OLAP usada em projetos de demonstração, OLAPWrapper.
Como a próxima tarefa do OLAP aborda uma nova área, precisaremos refatorar o código existente, bem como selecionar nele as partes comuns ao histórico de negociação e às cotações, se bem que o ideal seria que fosse a qualquer fonte de dados.
Refatoração
É criado um arquivo novo OLAPCommon.mqh com base no OLAPcube.mqh, arquivo novo esse em que permanecem apenas os tipos básicos. Entre os itens retirados estão, em primeiro lugar, enumerações descrevendo a atribuição complementar dos campos de dados, por exemplo, SELECTORS e TRADE_RECORD_FIELDS. Também são excluídas as classes de seleção e de registro relacionadas à negociação. Certamente, em vez de serem excluídas permanentemente, são transferidas para o novo arquivo OLAPTrades.mqh, criado para trabalhar com histórico e relatórios de negociação.
Além disso, a antiga classe-wrapper OLAPWrapper é movida para o arquivo OLAPCommon.mqh, essa classe agora se torna padrão e, portanto, é renomeada para OLAPEngine. As enumerações de campos de dados devem ser usadas como parâmetro de parametrização (por exemplo, para adaptação de projetos dos artigos 1 e 2, será TRADE_RECORD_FIELDS, consulte detalhes abaixo).
O arquivo OLAPTrades.mqh contém os seguintes tipos (descritos nos artigos 1 e 2):
- enumerações TRADE_SELECTORS (anteriormente SELECTORS), TRADE_RECORD_FIELDS;
- seletores TradeSelector, TypeSelector, SymbolSelector, MagicSelector, ProfitableSelector, DaysRangeSelector;
- classes de registro TradeRecord, CustomTradeRecord, HistoryTradeRecord;
- adaptador HistoryDataAdapter;
- mecanismo OLAPEngineTrade — especialização OLAPEngine<TRADE_RECORD_FIELDS> ;
Observe que o seletor DaysRangeSelector também está aqui, ou seja, tornou-se um seletor regular para analisar o histórico de negociação, enquanto anteriormente estava no arquivo OLAPcore.mqh como um modelo de seletor personalizado.
No final do arquivo, por padrão é criada uma instância do adaptador:
HistoryDataAdapter<RECORD_CLASS> _defaultHistoryAdapter;
bem como uma instância do mecanismo OLAP:
OLAPEngineTrade _defaultEngine;
É conveniente usar esses objetos no código fonte do cliente. Aplicaremos uma abordagem semelhante de fornecimento de objetos prontos em outras áreas complementares (arquivos de cabeçalho), em particular no analisador de cotações que temos planejado.
Os arquivos HTMLcube.mqh e CSVcube.mqh permanecem quase inalterados. Todas as funcionalidades anteriores para analisar o histórico de negociação e os relatórios se mantêm, por isso ao artigo foi anexado o novo EA de teste OLAPRPRT.mq5, que é um análogo do OLAPDEMO.mq5 do primeiro artigo.
Ao usar o arquivo OLAPTrades.mqh como modelo, é fácil criar implementações especializadas de classes OLAP para outros tipos de dados.
Como complicaremos o projeto adicionando novas funcionalidades, todos os aspectos da integração do OLAP com uma interface gráfica serão intencionalmente deixados em segundo plano. Neste artigo, iremos nos concentrar na análise de dados sem visualização de dados (especialmente porque os métodos de visualização podem ser diferentes). Após ler este artigo, aqueles que estejam interessados podem combinar por conta própria o mecanismo atualizado com os recursos do artigo 2, no campo da GUI.
Aprimoramento
No contexto da análise de cotações, podem ser úteis alguns novos métodos de ramificação lógica e acumulação de dados. No entanto, ao arquivo OLAPCommon.mqh adicionaremos as classes destinadas a isso, porque são de natureza básica e, portanto, estarão disponíveis para qualquer “cubo” complementar, bem como as antigas do OLAPTrades.mqh.
Bem, foram adicionados:
- seletor MonthSelector;
- seletor WorkWeekDaySelector;
- agregador VarianceAggregator;
MonthSelector permite agrupar dados por mês. O fato de esse seletor ter estado ausente anteriormente pode ser considerado apenas um engano.
template<typename E> class MonthSelector: public DateTimeSelector<E> { public: MonthSelector(const E f): DateTimeSelector(f, 12) { _typename = typename(this); } virtual bool select(const Record *r, int &index) const { double d = r.get(selector); datetime t = (datetime)d; index = TimeMonth(t) - 1; return true; } virtual string getLabel(const int index) const { static string months[12] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; return months[index]; } };
WorkWeekDaySelector é um análogo de WeekDaySelector, mas apenas divide dados por dias da semana (de 1 a 5). Isso é muito conveniente para analisar mercados cujo horário de negociação exclui fins de semana, já que os indicadores de final de semana são sempre zero e não faz sentido lhes reservar células do hipercubo.
O agregador VarianceAggregator permite calcular a variância de dados e, assim, complementa o agregador de média AverageAggregator. A essência do novo agregador pode ser comparada com o valor do indicador Average True Range (ATR), mas, ao contrário deste último, o agregador pode ser calculado para qualquer intervalo (por exemplo, separadamente por horas do dia ou por dias da semana), bem como para outras fontes de dados (por exemplo, variância da receita no histórico de negociação).
template<typename E> class VarianceAggregator: public Aggregator<E> { protected: int counters[]; double sumx[]; double sumx2[]; public: VarianceAggregator(const E f, const Selector<E> *&s[], const Filter<E> *&t[]): Aggregator(f, s, t) { _typename = typename(this); } virtual void setSelectorBounds(const int length = 0) override { Aggregator<E>::setSelectorBounds(); ArrayResize(counters, ArraySize(totals)); ArrayResize(sumx, ArraySize(totals)); ArrayResize(sumx2, ArraySize(totals)); ArrayInitialize(counters, 0); ArrayInitialize(sumx, 0); ArrayInitialize(sumx2, 0); } virtual void update(const int index, const double value) override { counters[index]++; sumx[index] += value; sumx2[index] += value * value; const int n = counters[index]; const double variance = (sumx2[index] - sumx[index] * sumx[index] / n) / MathMax(n - 1, 1); totals[index] = MathSqrt(variance); } };
Fig. 1 Diagrama: classes de agregadores
Os seletores QuantizationSelector e SerialNumberSelector são derivados do BaseSelector, em vez do TradeSelector, que é mais específico. Além disso, o QuantizationSelector recebe um novo parâmetro de construtor que permite definir a granularidade do seletor. Por padrão, é zero, o que significa que os dados são agrupados de acordo com a coincidência exata do valor do campo correspondente (o campo é especificado no seletor). Por exemplo, no artigo anterior, usamos quantização por tamanho de lote para obter um relatório de lucro dividido por tamanho de lote, além disso, lotes, como 0,01, 0,1 e outros registrados no histórico de negociação, podiam ser usados como células de cubo. Às vezes, é mais conveniente quantificar com um determinado incremento (com um tamanho de célula), permitindo especificar um novo parâmetro de construtor. No código fonte abaixo, os trechos adicionados são marcados com um comentário "+".
template<typename T> class QuantizationSelector: public BaseSelector<T> { protected: Vocabulary<double> quants; uint cell; // + public: QuantizationSelector(const T field, const uint granularity = 0 /* + */): BaseSelector<T>(field), cell(granularity) { _typename = typename(this); } virtual void prepare(const Record *r) override { double value = r.get(selector); if(cell != 0) value = MathSign(value) * MathFloor(MathAbs(value) / cell) * cell; // + quants.add(value); } virtual bool select(const Record *r, int &index) const override { double value = r.get(selector); if(cell != 0) value = MathSign(value) * MathFloor(MathAbs(value) / cell) * cell; // + index = quants.get(value); return (index >= 0); } virtual int getRange() const override { return quants.size(); } virtual string getLabel(const int index) const override { return (string)(float)quants[index]; } };
Adicionalmente, nas classes existentes foram feitas outras melhorias. Em particular, as classes de filtro Filter e FilterRange agora suportam comparações por valor de campo, e não apenas pelo índice da célula correspondendo a esse valor. Isso é conveniente para o usuário, porque o índice das células nem sempre é conhecido antecipadamente. O novo modo de operação é ativado se o seletor retornar um índice igual a -1 (as linhas adicionadas estão marcadas com comentários com '+'):
template<typename E> class Filter { protected: Selector<E> *selector; double filter; public: Filter(Selector<E> &s, const double value): selector(&s), filter(value) { } virtual bool matches(const Record *r) const { int index; if(selector.select(r, index)) { if(index == -1) // + { // + if(dynamic_cast<FilterSelector<E> *>(selector) != NULL) // + { // + return r.get(selector.getField()) == filter; // + } // + } // + else // + { // + if(index == (int)filter) return true; } // + } return false; } Selector<E> *getSelector() const { return selector; } virtual string getTitle() const { return selector.getTitle() + "[" + (string)filter + "]"; } };
Obviamente, precisamos de um seletor que como índice possa retornar -1. Neste caso, ele será o chamado de FilterSelector.
template<typename T> class FilterSelector: public BaseSelector<T> { public: FilterSelector(const T field): BaseSelector(field) { _typename = typename(this); } virtual bool select(const Record *r, int &index) const override { index = -1; return true; } virtual int getRange() const override { return 0; } virtual double getMin() const override { return 0; } virtual double getMax() const override { return 0; } virtual string getLabel(const int index) const override { return EnumToString(selector); } };
Como se pode ver, este seletor, para qualquer registro, retorna true como um sinal de que o registro deve ser processado e como índice devolve -1. Devido a isso, o filtro poderá "entender" que o usuário pede para "peneirar" o registro por valor do campo, em vez de por índice. Veremos abaixo um exemplo de como é usado isso.
Além disso, a exibição do log agora suporta a classificação de cubo multidimensional por valor — anteriormente os cubos multidimensionais não podiam ser classificados. A classificação por rótulos de cubo multidimensional está disponível apenas parcialmente, ela é possível apenas para os seletores que "são capazes" de formatar uniformemente rótulos com strings em ordem lexicográfica. Em particular, o novo seletor por dias da semana fornece rótulos como "1`Monday", "2`Tuesday", "3`Wednesday", "4`Thursday", "5`Friday", enquanto o número do dia presente no primeiro símbolo fornece uma adequada classificação. Caso contrário, para uma implementação adequada, será necessário introduzir funções de comparação de rótulos. Além disso, para os agregadores "sequenciais" especiais IdentityAggregator, ProgressiveTotalAggregator, provavelmente será necessário definir as prioridades dos lados do cubo, já que nesses agregadores ao longo do eixo X sempre há um número de sequência de registro, que pode ser bom no momento de classificar no final e não no início.
Estas não são todas as modificações dos códigos fonte antigos, elas podem ser entendidas em essência comparando os códigos fonte em contexto.
Estendendo o OLAP para uma área de cotações complementar
Com ajuda das classes base de OLAPCommon.mqh e por analogia com OLAPTrades.mqh, criaremos o arquivo OLAPQuotes.mqh com as classes destinadas à análise de cotações. Nele, em primeiro lugar, descrevemos tipos como:
- enumerações QUOTE_SELECTORS, QUOTE_RECORD_FIELDS;
- seletores QuoteSelector, ShapeSelector;
- classes de registros QuotesRecord, CustomQuotesBaseRecord;
- adaptador QuotesDataAdapter;
- OLAPEngineQuotes — especialização OLAPEngine<QUOTE_RECORD_FIELDS>;
A enumeração QUOTE_SELECTORS é definida da seguinte maneira:
enum QUOTE_SELECTORS { SELECTOR_NONE, // none SELECTOR_SHAPE, // type SELECTOR_INDEX, // ordinal number /* below datetime field assumed */ SELECTOR_MONTH, // month-of-year SELECTOR_WEEKDAY, // day-of-week SELECTOR_DAYHOUR, // hour-of-day SELECTOR_HOURMINUTE, // minute-of-hour /* the next require a field as parameter */ SELECTOR_SCALAR, // scalar(field) SELECTOR_QUANTS, // quants(field) SELECTOR_FILTER // filter(field) };
O seletor de forma (shape) diferencia as barras por tipo: altista, baixista e neutra, dependendo da direção do movimento dos preços.
O seletor de índice corresponde à classe SerialNumberSelector, que é definida nas classes base (arquivo OLAPCommon.mqh). Se, no caso de operações de negociação, esses números de sequência forem transações, então, para cotações, serão os números das barras.
O seletor de mês foi descrito acima. Outros seletores são herdados de artigos anteriores.
Os campos de dados nas cotações são descritos usando a seguinte enumeração:
enum QUOTE_RECORD_FIELDS { FIELD_NONE, // none FIELD_INDEX, // index (bar number) FIELD_SHAPE, // type (bearish/flat/bullish) FIELD_DATETIME, // datetime FIELD_PRICE_OPEN, // open price FIELD_PRICE_HIGH, // high price FIELD_PRICE_LOW, // low price FIELD_PRICE_CLOSE, // close price FIELD_PRICE_RANGE_OC,// price range (OC) FIELD_PRICE_RANGE_HL,// price range (HL) FIELD_SPREAD, // spread FIELD_TICK_VOLUME, // tick volume FIELD_REAL_VOLUME, // real volume FIELD_CUSTOM1, // custom 1 FIELD_CUSTOM2, // custom 2 FIELD_CUSTOM3, // custom 3 FIELD_CUSTOM4, // custom 4 QUOTE_RECORD_FIELDS_LAST };
O objetivo de cada um deve ficar claro nos nomes e nos comentários.
As duas das enumerações acima são colocadas em macros:
#define SELECTORS QUOTE_SELECTORS #define ENUM_FIELDS QUOTE_RECORD_FIELDS
Observe que definições de macro semelhantes, como SELECTORS e ENUM_FIELDS, estão disponíveis em todos os arquivos de cabeçalho “complementares”, no nosso caso, existem dois (OLAPTrades.mqh, OLAPQuotes.mqh, para o histórico de operações de negociação e para o de cotações), mas pode haver mais. Assim, em qualquer projeto usando OLAP, agora é possível analisar apenas uma área complementar por vez (incluindo, por exemplo, OLAPTrades.mqh ou OLAPQuotes.mqh, mas não duas ao mesmo tempo). Para analisar cruzadamente cubos diferentes, será necessário fazer mais uma pequena refatoração. Isso é deixado para estudo independente, uma vez que as tarefas de análise paralela de vários metacubos são mais específicas e raras.
O seletor pai para cotações é uma especialização BaseSelector contendo os campos QUOTE_RECORD_FIELDS:
class QuoteSelector: public BaseSelector<QUOTE_RECORD_FIELDS> { public: QuoteSelector(const QUOTE_RECORD_FIELDS field): BaseSelector(field) { } };
O seletor de tipo de barra (altista ou baixista) ShapeSelector é implementado da seguinte maneira:
class ShapeSelector: public QuoteSelector { public: ShapeSelector(): QuoteSelector(FIELD_SHAPE) { _typename = typename(this); } virtual bool select(const Record *r, int &index) const { index = (int)r.get(selector); index += 1; // shift from -1, 0, +1 to [0..2] return index >= getMin() && index <= getMax(); } virtual int getRange() const { return 3; // 0 through 2 } virtual string getLabel(const int index) const { const static string types[3] = {"bearish", "flat", "bullish"}; return types[index]; } };
São reservados 3 valores para os tipos: -1, movimento descendente; 0, movimento lateral; +1, movimento ascendente. Os índices de células estão no intervalo de 0 a 2, inclusive. O preenchimento do campo com o valor real do tipo de uma barra específica é fornecido abaixo na classe QuotesRecord.
Fig. 2 Diagrama: classes de seletores
A classe do registro que armazena informações sobre uma barra específica deve ser entendida de maneira intuitiva:
class QuotesRecord: public Record { protected: static int counter; // number of bars void fillByQuotes(const MqlRates &rate) { set(FIELD_INDEX, counter++); set(FIELD_SHAPE, rate.close > rate.open ? +1 : (rate.close < rate.open ? -1 : 0)); set(FIELD_DATETIME, (double)rate.time); set(FIELD_PRICE_OPEN, rate.open); set(FIELD_PRICE_HIGH, rate.high); set(FIELD_PRICE_LOW, rate.low); set(FIELD_PRICE_CLOSE, rate.close); set(FIELD_PRICE_RANGE_OC, (rate.close - rate.open) / _Point); set(FIELD_PRICE_RANGE_HL, (rate.high - rate.low) * MathSign(rate.close - rate.open) / _Point); set(FIELD_SPREAD, (double)rate.spread); set(FIELD_TICK_VOLUME, (double)rate.tick_volume); set(FIELD_REAL_VOLUME, (double)rate.real_volume); } public: QuotesRecord(): Record(QUOTE_RECORD_FIELDS_LAST) { } QuotesRecord(const MqlRates &rate): Record(QUOTE_RECORD_FIELDS_LAST) { fillByQuotes(rate); } static int getRecordCount() { return counter; } static void reset() { counter = 0; } virtual string legend(const int index) const override { if(index >= 0 && index < QUOTE_RECORD_FIELDS_LAST) { return EnumToString((QUOTE_RECORD_FIELDS)index); } return "unknown"; } };
Todas as informações vêm da estrutura MqlRates. A criação de instâncias de classe será mostrada posteriormente na implementação do adaptador.
Nesta mesma classe, é definida a atribuição acrescentada no campo (inteiro, real, data), o que é necessário, uma vez que todos os campos de registro são só tecnicamente armazenados numa matriz do tipo double.
class QuotesRecord: public Record { protected: const static char datatypes[QUOTE_RECORD_FIELDS_LAST]; public: ... static char datatype(const int index) { return datatypes[index]; } }; const static char QuotesRecord::datatypes[QUOTE_RECORD_FIELDS_LAST] = { 0, // none 'i', // index, serial number 'i', // type (-1 down/0/+1 up) 't', // datetime 'd', // open price 'd', // high price 'd', // low price 'd', // close price 'd', // range OC 'd', // range HL 'i', // spread 'i', // tick 'i', // real 'd', // custom 1 'd', // custom 2 'd', // custom 3 'd' // custom 4 };
A presença de um sinalizador para a especialização de campo nos permite adaptar a entrada/saída de dados na interface do usuário, o que será mostrado abaixo.
Para suportar o preenchimento de campos personalizados, existe uma classe intermediária cujo objetivo principal é chamar fillCustomFields a partir da classe de usuário que a classe base especifica com ajuda de um modelo (assim, no momento de chamar o construtor CustomQuotesBaseRecord, nosso objeto personalizado já é criado e preenchido com campos padrão que geralmente são necessários para o cálculo de campos personalizados):
template<typename T> class CustomQuotesBaseRecord: public T { public: CustomQuotesBaseRecord(const MqlRates &rate): T(rate) { fillCustomFields(); } };
Ele é usado no adaptador de cotações:
template<typename T> class QuotesDataAdapter: public DataAdapter { private: int size; int cursor; public: QuotesDataAdapter() { reset(); } virtual void reset() override { size = MathMin(Bars(_Symbol, _Period), TerminalInfoInteger(TERMINAL_MAXBARS)); cursor = size - 1; T::reset(); } virtual int reservedSize() { return size; } virtual Record *getNext() { if(cursor >= 0) { MqlRates rate[1]; if(CopyRates(_Symbol, _Period, cursor, 1, rate) > 0) { cursor--; return new CustomQuotesBaseRecord<T>(rate[0]); } Print(__FILE__, " ", __LINE__, " ", GetLastError()); return NULL; } return NULL; } };
Observe que as barras são distribuídas de mais antigas para mais novas, ou seja, em ordem cronológica. Em particular, isso significa que a indexação (campo FIELD_INDEX) é realizada como numa matriz regular, e não em séries temporais.
Por fim, o mecanismo OLAP para cotações é o seguinte:
class OLAPEngineQuotes: public OLAPEngine<QUOTE_SELECTORS,QUOTE_RECORD_FIELDS> { protected: virtual Selector<QUOTE_RECORD_FIELDS> *createSelector(const QUOTE_SELECTORS selector, const QUOTE_RECORD_FIELDS field) override { switch(selector) { case SELECTOR_SHAPE: return new ShapeSelector(); case SELECTOR_INDEX: return new SerialNumberSelector<QUOTE_RECORD_FIELDS,QuotesRecord>(FIELD_INDEX); case SELECTOR_MONTH: return new MonthSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME); case SELECTOR_WEEKDAY: return new WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME); case SELECTOR_DAYHOUR: return new DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME); case SELECTOR_HOURMINUTE: return new DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME); case SELECTOR_SCALAR: return field != FIELD_NONE ? new BaseSelector<QUOTE_RECORD_FIELDS>(field) : NULL; case SELECTOR_QUANTS: return field != FIELD_NONE ? new QuantizationSelector<QUOTE_RECORD_FIELDS>(field, QuantGranularity) : NULL; case SELECTOR_FILTER: return field != FIELD_NONE ? new FilterSelector<QUOTE_RECORD_FIELDS>(field) : NULL; } return NULL; } virtual void initialize() override { Print("Bars read: ", QuotesRecord::getRecordCount()); } public: OLAPEngineQuotes(): OLAPEngine() {} OLAPEngineQuotes(DataAdapter *ptr): OLAPEngine(ptr) {} };
Na classe base OLAPEngine, discutida no primeiro artigo, permanecem todas as funções principais, mas com um nome diferente, OLAPWrapper. Aqui, apenas precisamos criar seletores específicos para as cotações.
Forneceremos instâncias padrão do adaptador e do mecanismo OLAP padrão na forma de objetos prontos:
QuotesDataAdapter<RECORD_CLASS> _defaultQuotesAdapter; OLAPEngineQuotes _defaultEngine;
Com base nas classes criadas para duas áreas de análise complementares (OLAPTrades.mqh, OLAPQuotes.mqh), é fácil estender a funcionalidade OLAP para outros aplicativos, como processar resultados de otimização ou dados obtidos de fontes externas.
Fig. 3 Diagrama: classes de controle OLAP
Expert Advisor para análise de cotações OLAP
Tudo está pronto para usarmos as classes criadas. Escrevemos o EA não-negociante OLAPQTS.mq5. Sua estrutura será semelhante à do OLAPRPRT.mq5, e será útil na análise dos relatórios de negociação.
A classe CustomQuotesRecord, herdada de QuotesRecord, é projetada para mostrar o cálculo/preenchimento de campos personalizados. Usaremos alguns campos personalizados para identificar nas cotações padrões que serão úteis para a construção de estratégias de negociação. Todos esses campos são preenchidos no método fillCustomFields, mas os descreveremos com mais detalhes abaixo.
class CustomQuotesRecord: public QuotesRecord { public: CustomQuotesRecord(): QuotesRecord() {} CustomQuotesRecord(const MqlRates &rate): QuotesRecord(rate) { } virtual void fillCustomFields() override { // ... } virtual string legend(const int index) const override { // ... return QuotesRecord::legend(index); } };
Para que o adaptador "reconheça" nossa classe de registro CustomQuotesRecord e crie instâncias dela, é necessário definir a macro a seguir antes de ativar o OLAPQuotes.mqh:
// this line plugs our class into default adapter in OLAPQuotes.mqh #define RECORD_CLASS CustomQuotesRecord #include <OLAP/OLAPQuotes.mqh>
Para controlar o EA, são destinados parâmetros de entrada semelhantes aos que estavam no projeto de análise do histórico de negociação. Como nesse caso, é permitido acumular dados em três dimensões do metacubo, para o qual está disponível uma escolha de seletores ao longo dos eixos X, Y, Z. Também é possível especificar um filtro por um valor ou um intervalo de dois valores. Por fim, o usuário deve selecionar o tipo de agregador (lembre-se de que alguns agregadores exigem a especificação de um campo de agregação, outros implicam um campo específico) e, opcionalmente, o tipo de classificação.
sinput string X = "————— X axis —————"; // · X · input SELECTORS SelectorX = DEFAULT_SELECTOR_TYPE; // · SelectorX input ENUM_FIELDS FieldX = DEFAULT_SELECTOR_FIELD /* field does matter only for some selectors */; // · FieldX sinput string Y = "————— Y axis —————"; // · Y · input SELECTORS SelectorY = SELECTOR_NONE; // · SelectorY input ENUM_FIELDS FieldY = FIELD_NONE; // · FieldY sinput string Z = "————— Z axis —————"; // · Z · input SELECTORS SelectorZ = SELECTOR_NONE; // · SelectorZ input ENUM_FIELDS FieldZ = FIELD_NONE; // · FieldZ sinput string F = "————— Filter —————"; // · F · input SELECTORS _Filter1 = SELECTOR_NONE; // · Filter1 input ENUM_FIELDS _Filter1Field = FIELD_NONE; // · Filter1Field input string _Filter1value1 = ""; // · Filter1value1 input string _Filter1value2 = ""; // · Filter1value2 sinput string A = "————— Aggregator —————"; // · A · input AGGREGATORS _AggregatorType = DEFAULT_AGGREGATOR_TYPE; // · AggregatorType input ENUM_FIELDS _AggregatorField = DEFAULT_AGGREGATOR_FIELD; // · AggregatorField input SORT_BY _SortBy = SORT_BY_NONE; // · SortBy
Todos os seletores e seus campos são reduzidos a matrizes para facilitar a subsequente transferência ao mecanismo:
SELECTORS _selectorArray[4]; ENUM_FIELDS _selectorField[4]; int OnInit() { _selectorArray[0] = SelectorX; _selectorArray[1] = SelectorY; _selectorArray[2] = SelectorZ; _selectorArray[3] = _Filter1; _selectorField[0] = FieldX; _selectorField[1] = FieldY; _selectorField[2] = FieldZ; _selectorField[3] = _Filter1Field; _defaultEngine.setAdapter(&_defaultQuotesAdapter); EventSetTimer(1); return INIT_SUCCEEDED; }
Como podemos ver, o EA usa as instâncias padrão do mecanismo e o adaptador de cotações. Devido à sua especificidade, o EA deve processar os dados uma vez para os parâmetros inseridos. Para fazer isso, além de ser iniciado nos finais de semana quando não há ticks, no manipulador OnInit inicia um temporizador.
A inicialização direta do processamento no OnTimer fica assim:
LogDisplay _display(11, _Digits); void OnTimer() { EventKillTimer(); double Filter1value1 = 0, Filter1value2 = 0; if(CustomQuotesRecord::datatype(_Filter1Field) == 't') { Filter1value1 = (double)StringToTime(_Filter1value1); Filter1value2 = (double)StringToTime(_Filter1value2); } else { Filter1value1 = StringToDouble(_Filter1value1); Filter1value2 = StringToDouble(_Filter1value2); } _defaultQuotesAdapter.reset(); _defaultEngine.process(_selectorArray, _selectorField, _AggregatorType, _AggregatorField, _display, _SortBy, Filter1value1, Filter1value2); }
Ao analisar cotações, precisaremos de um filtro por datas. Devido a isso, os valores para os filtros são especificados nos parâmetros de entrada na forma de strings e, dependendo do tipo de campo ao qual é colocado o filtro, elas (as strings) são interpretadas como um número ou data (no formato usual AAAA.MM.DD). No exemplo do primeiro artigo, sempre foram inseridos valores numéricos, o que é inconveniente para o usuário final no caso de datas.
Todos os parâmetros de entrada preparados são transferidos ao método process do mecanismo OLAP e, em seguida, ocorre o trabalho sem intervenção do usuário. Após isso no log do EA são exibidos os resultados usando uma instância do LogDisplay.
Testando a análise de cotações OLAP
Para nos familiarizarmos com as ferramentas criadas, realizaremos vários estudos de cotações simples.
Abrimos o gráfico EURUSD D1 e colocamos nele o EA OLAPQTS. Deixamos todos os parâmetros com valores padrão, implicando escolher o seletor type no eixo X e o agregador COUNT. Somente alteramos as configurações do filtro: no parâmetro Filter1, definimos a variante "filter(field)", no Filter1Field, datetime, enquanto no Filter1Value1 e no Filter1Value2, "2019.01.01" e "2020.01.01", respectivamente. Assim, limitamos o intervalo de cálculo a 2019.
Como resultado da inicialização do EA, obtemos algo assim:
OLAPQTS (EURUSD,D1) Bars read: 12626 OLAPQTS (EURUSD,D1) CountAggregator<QUOTE_RECORD_FIELDS> FIELD_NONE [3] OLAPQTS (EURUSD,D1) Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0]; OLAPQTS (EURUSD,D1) Selectors: 1 OLAPQTS (EURUSD,D1) X: ShapeSelector(FIELD_SHAPE) [3] OLAPQTS (EURUSD,D1) Processed records: 259 OLAPQTS (EURUSD,D1) 134.00000: bearish OLAPQTS (EURUSD,D1) 0.00000: flat OLAPQTS (EURUSD,D1) 125.00000: bullish
Pode-se ver no registro que foram analisadas 12 626 barras (todo o histórico disponível do EURUSD D1), mas apenas 259 delas atendem a condição do filtro, e 134 são baixistas e 125, altistas.
Se mudarmos o período gráfico para H1, obtemos uma estimativa das barras horárias:
OLAPQTS (EURUSD,H1) Bars read: 137574 OLAPQTS (EURUSD,H1) CountAggregator<QUOTE_RECORD_FIELDS> FIELD_NONE [3] OLAPQTS (EURUSD,H1) Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0]; OLAPQTS (EURUSD,H1) Selectors: 1 OLAPQTS (EURUSD,H1) X: ShapeSelector(FIELD_SHAPE) [3] OLAPQTS (EURUSD,H1) Processed records: 6196 OLAPQTS (EURUSD,H1) 3051.00000: bearish OLAPQTS (EURUSD,H1) 55.00000: flat OLAPQTS (EURUSD,H1) 3090.00000: bullish
Em seguida, tentamos analisar os spreads. Uma das peculiaridades do MetaTrader 5 é que se pode armazenar e exibir o spread mínimo na estrutura MqlRates e na janela de dados. Do ponto de vista do teste de ideias de negociação, dada abordagem pode ser perigosa, porque fornece estimativas do lucro otimistas demais. Seria melhor justificada se também os spreads máximos estivessem disponíveis no histórico. Obviamente, se necessário, é possível usar o histórico de ticks, mas ainda assim o modo barras é mais econômico. Tentemos avaliar spreads reais por horas do dia.
No mesmo gráfico EURUSD H1, manteremos o filtro anterior para 2019 e inseriremos as seguintes configurações de EA. Seletor X — "hour-of-day", agregador — "AVERAGE", campo do agregador — "spread". Aqui está um exemplo de resultado:
OLAPQTS (EURUSD,H1) Bars read: 137574 OLAPQTS (EURUSD,H1) AverageAggregator<QUOTE_RECORD_FIELDS> FIELD_SPREAD [24] OLAPQTS (EURUSD,H1) Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0]; OLAPQTS (EURUSD,H1) Selectors: 1 OLAPQTS (EURUSD,H1) X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24] OLAPQTS (EURUSD,H1) Processed records: 6196 OLAPQTS (EURUSD,H1) 4.71984: 00 OLAPQTS (EURUSD,H1) 3.19066: 01 OLAPQTS (EURUSD,H1) 3.72763: 02 OLAPQTS (EURUSD,H1) 4.19455: 03 OLAPQTS (EURUSD,H1) 4.38132: 04 OLAPQTS (EURUSD,H1) 4.28794: 05 OLAPQTS (EURUSD,H1) 3.93050: 06 OLAPQTS (EURUSD,H1) 4.01158: 07 OLAPQTS (EURUSD,H1) 4.39768: 08 OLAPQTS (EURUSD,H1) 4.68340: 09 OLAPQTS (EURUSD,H1) 4.68340: 10 OLAPQTS (EURUSD,H1) 4.64479: 11 OLAPQTS (EURUSD,H1) 4.57915: 12 OLAPQTS (EURUSD,H1) 4.62934: 13 OLAPQTS (EURUSD,H1) 4.64865: 14 OLAPQTS (EURUSD,H1) 4.61390: 15 OLAPQTS (EURUSD,H1) 4.62162: 16 OLAPQTS (EURUSD,H1) 4.50579: 17 OLAPQTS (EURUSD,H1) 4.56757: 18 OLAPQTS (EURUSD,H1) 4.61004: 19 OLAPQTS (EURUSD,H1) 4.59459: 20 OLAPQTS (EURUSD,H1) 4.67054: 21 OLAPQTS (EURUSD,H1) 4.50775: 22 OLAPQTS (EURUSD,H1) 3.57312: 23
Para cada hora do dia, é indicado o valor médio do spread, mas ele é enganoso porque é uma média do spread mínimo. Para obter uma imagem mais adequada, mudamos para o período gráfico M1, assim, a análise será feita com o máximo detalhamento possível do histórico (se não usarmos ticks).
OLAPQTS (EURUSD,M1) Bars read: 1000000 OLAPQTS (EURUSD,M1) AverageAggregator<QUOTE_RECORD_FIELDS> FIELD_SPREAD [24] OLAPQTS (EURUSD,M1) Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0]; OLAPQTS (EURUSD,M1) Selectors: 1 OLAPQTS (EURUSD,M1) X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24] OLAPQTS (EURUSD,M1) Processed records: 371475 OLAPQTS (EURUSD,M1) 14.05653: 00 OLAPQTS (EURUSD,M1) 6.63397: 01 OLAPQTS (EURUSD,M1) 6.00707: 02 OLAPQTS (EURUSD,M1) 5.72516: 03 OLAPQTS (EURUSD,M1) 5.72575: 04 OLAPQTS (EURUSD,M1) 5.77588: 05 OLAPQTS (EURUSD,M1) 5.82541: 06 OLAPQTS (EURUSD,M1) 5.82560: 07 OLAPQTS (EURUSD,M1) 5.77979: 08 OLAPQTS (EURUSD,M1) 5.44876: 09 OLAPQTS (EURUSD,M1) 5.32619: 10 OLAPQTS (EURUSD,M1) 5.32966: 11 OLAPQTS (EURUSD,M1) 5.32096: 12 OLAPQTS (EURUSD,M1) 5.32117: 13 OLAPQTS (EURUSD,M1) 5.29633: 14 OLAPQTS (EURUSD,M1) 5.21140: 15 OLAPQTS (EURUSD,M1) 5.17084: 16 OLAPQTS (EURUSD,M1) 5.12794: 17 OLAPQTS (EURUSD,M1) 5.27576: 18 OLAPQTS (EURUSD,M1) 5.48078: 19 OLAPQTS (EURUSD,M1) 5.60175: 20 OLAPQTS (EURUSD,M1) 5.70999: 21 OLAPQTS (EURUSD,M1) 5.87404: 22 OLAPQTS (EURUSD,M1) 6.94555: 23
Aqui a imagem é mais realista, pois, em algumas horas, o spread mínimo médio aumenta em 2-3 vezes. Porém, para tornar a análise ainda mais rigorosa, podemos consultar o valor máximo, usando o agregador "MAX". Apesar de os valores obtidos ainda serem o máximo de _mínimo_, não se deve esquecer que eles são construídos em barras de minutos a cada hora e, portanto, descrevem bem as condições de entrada e saída para negociação de curto prazo.
OLAPQTS (EURUSD,M1) Bars read: 1000000 OLAPQTS (EURUSD,M1) MaxAggregator<QUOTE_RECORD_FIELDS> FIELD_SPREAD [24] OLAPQTS (EURUSD,M1) Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0]; OLAPQTS (EURUSD,M1) Selectors: 1 OLAPQTS (EURUSD,M1) X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24] OLAPQTS (EURUSD,M1) Processed records: 371475 OLAPQTS (EURUSD,M1) 157.00000: 00 OLAPQTS (EURUSD,M1) 31.00000: 01 OLAPQTS (EURUSD,M1) 12.00000: 02 OLAPQTS (EURUSD,M1) 12.00000: 03 OLAPQTS (EURUSD,M1) 13.00000: 04 OLAPQTS (EURUSD,M1) 11.00000: 05 OLAPQTS (EURUSD,M1) 12.00000: 06 OLAPQTS (EURUSD,M1) 12.00000: 07 OLAPQTS (EURUSD,M1) 11.00000: 08 OLAPQTS (EURUSD,M1) 11.00000: 09 OLAPQTS (EURUSD,M1) 12.00000: 10 OLAPQTS (EURUSD,M1) 13.00000: 11 OLAPQTS (EURUSD,M1) 12.00000: 12 OLAPQTS (EURUSD,M1) 13.00000: 13 OLAPQTS (EURUSD,M1) 12.00000: 14 OLAPQTS (EURUSD,M1) 14.00000: 15 OLAPQTS (EURUSD,M1) 16.00000: 16 OLAPQTS (EURUSD,M1) 14.00000: 17 OLAPQTS (EURUSD,M1) 15.00000: 18 OLAPQTS (EURUSD,M1) 21.00000: 19 OLAPQTS (EURUSD,M1) 17.00000: 20 OLAPQTS (EURUSD,M1) 25.00000: 21 OLAPQTS (EURUSD,M1) 31.00000: 22 OLAPQTS (EURUSD,M1) 70.00000: 23
A diferença é considerável, pois começamos com um spread em torno de 4 pontos e terminamos com dezenas ou até centenas à meia-noite.
Avaliamos a variância do spread e, ao mesmo tempo, verificamos como funciona o novo agregador, selecionando "DEVIATION".
OLAPQTS (EURUSD,M1) Bars read: 1000000 OLAPQTS (EURUSD,M1) VarianceAggregator<QUOTE_RECORD_FIELDS> FIELD_SPREAD [24] OLAPQTS (EURUSD,M1) Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0]; OLAPQTS (EURUSD,M1) Selectors: 1 OLAPQTS (EURUSD,M1) X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24] OLAPQTS (EURUSD,M1) Processed records: 371475 OLAPQTS (EURUSD,M1) 9.13767: 00 OLAPQTS (EURUSD,M1) 3.12974: 01 OLAPQTS (EURUSD,M1) 2.72293: 02 OLAPQTS (EURUSD,M1) 2.70965: 03 OLAPQTS (EURUSD,M1) 2.68758: 04 OLAPQTS (EURUSD,M1) 2.64350: 05 OLAPQTS (EURUSD,M1) 2.64158: 06 OLAPQTS (EURUSD,M1) 2.64934: 07 OLAPQTS (EURUSD,M1) 2.62854: 08 OLAPQTS (EURUSD,M1) 2.72117: 09 OLAPQTS (EURUSD,M1) 2.80259: 10 OLAPQTS (EURUSD,M1) 2.79681: 11 OLAPQTS (EURUSD,M1) 2.80850: 12 OLAPQTS (EURUSD,M1) 2.81435: 13 OLAPQTS (EURUSD,M1) 2.83489: 14 OLAPQTS (EURUSD,M1) 2.90745: 15 OLAPQTS (EURUSD,M1) 2.95804: 16 OLAPQTS (EURUSD,M1) 2.96799: 17 OLAPQTS (EURUSD,M1) 2.88021: 18 OLAPQTS (EURUSD,M1) 2.76605: 19 OLAPQTS (EURUSD,M1) 2.72036: 20 OLAPQTS (EURUSD,M1) 2.85615: 21 OLAPQTS (EURUSD,M1) 2.94224: 22 OLAPQTS (EURUSD,M1) 4.60560: 23
Esses valores representam um único desvio padrão, a partir do qual é possível configurar filtros em estratégias de escalpelamento ou robôs que dependem de impulsos de volatilidade.
Verificamos o preenchimento do campo com a faixa de movimentos de preços na barra, o trabalho de quantização para um determinado tamanho de célula e a classificação.
Para fazer isso, mudamos de volta para EURUSD D1, mantendo o filtro anterior para 2019. Nos parâmetros, indicamos:
- QuantGranularity=100 (pontos de 5 dígitos)
- SelectorX=quants
- FieldX=price range (OC)
- Aggregator=COUNT
- SortBy=value (descending)
Obtemos o resultado:
OLAPQTS (EURUSD,D1) Bars read: 12627 OLAPQTS (EURUSD,D1) CountAggregator<QUOTE_RECORD_FIELDS> FIELD_NONE [20] OLAPQTS (EURUSD,D1) Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0]; OLAPQTS (EURUSD,D1) Selectors: 1 OLAPQTS (EURUSD,D1) X: QuantizationSelector<QUOTE_RECORD_FIELDS>(FIELD_PRICE_RANGE_OC) [20] OLAPQTS (EURUSD,D1) Processed records: 259 OLAPQTS (EURUSD,D1) [value] [title] OLAPQTS (EURUSD,D1) [ 0] 72.00000 "0.0" OLAPQTS (EURUSD,D1) [ 1] 27.00000 "100.0" OLAPQTS (EURUSD,D1) [ 2] 24.00000 "-100.0" OLAPQTS (EURUSD,D1) [ 3] 24.00000 "-200.0" OLAPQTS (EURUSD,D1) [ 4] 21.00000 "200.0" OLAPQTS (EURUSD,D1) [ 5] 17.00000 "-300.0" OLAPQTS (EURUSD,D1) [ 6] 16.00000 "300.0" OLAPQTS (EURUSD,D1) [ 7] 12.00000 "-400.0" OLAPQTS (EURUSD,D1) [ 8] 8.00000 "500.0" OLAPQTS (EURUSD,D1) [ 9] 8.00000 "400.0" OLAPQTS (EURUSD,D1) [10] 6.00000 "-700.0" OLAPQTS (EURUSD,D1) [11] 6.00000 "-500.0" OLAPQTS (EURUSD,D1) [12] 6.00000 "700.0" OLAPQTS (EURUSD,D1) [13] 4.00000 "-600.0" OLAPQTS (EURUSD,D1) [14] 2.00000 "600.0" OLAPQTS (EURUSD,D1) [15] 2.00000 "1000.0" OLAPQTS (EURUSD,D1) [16] 1.00000 "-800.0" OLAPQTS (EURUSD,D1) [17] 1.00000 "-1100.0" OLAPQTS (EURUSD,D1) [18] 1.00000 "900.0" OLAPQTS (EURUSD,D1) [19] 1.00000 "-1000.0"
Como esperado, o maior número de barras (72) cai na faixa zero, ou seja, a mudança de preço não excede 100 pontos. Alterações de ± 100 e ± 200 pontos são as próximas em termos de "popularidade" e assim por diante.
Mas tudo isso é apenas uma demonstração do desempenho do OLAP ao analisar cotações. É hora de avançar para a criação de estratégias de negociação usando OLAP.
Projetando estratégias de negociação baseadas na análise de cotações OLAP. Parte 1
Tentaremos descobrir se há padrões relacionados aos ciclos intradiários e intrasemanais. Se, durante determinadas horas ou dias da semana, os movimentos de preços prevalecentes forem, em média, assimétricos, poderemos usar isso para abrir negócios. Para detectar tal ciclicidade, será necessário usar os seletores "hour-of-day" e "day-of-week". Os seletores podem ser usados sequencialmente (um de cada vez) ou ambos simultaneamente (cada um ao longo de seu próprio eixo). A segunda opção é melhor no sentido em que permitirá criar seções de dados mais precisas que levem em conta dois fatores (ciclos) de uma só vez. Embora para o programa seja irrelevante quais seletores colocar nos eixos X e Y, os resultados mostrados ao usuário são transpostos.
Como o intervalo de índices dos seletores em questão é, respectivamente, 24 (horas num dia) e 5 (dias numa semana útil), o tamanho do cubo é 120. Em princípio, podemos conectar ciclos sazonais dentro do ano, escolhendo o seletor "month-of-year" ao longo do eixo Z. Mas, por simplicidade, nos restringimos a cubos bidimensionais.
A mudança de preço dentro da barra é apresentada em dois campos: FIELD_PRICE_RANGE_OC e FIELD_PRICE_RANGE_HL. O primeiro indica a diferença de pontos entre os preços Open e Close, enquanto o segundo, a amplitude entre High e Low. Usaremos o primeiro como fonte de estatísticas sobre potenciais transações. Resta decidir quais estatísticas considerar, ou seja, qual agregador aplicar.
Curiosamente, neste caso, pode ser útil o agregador ProfitFactorAggregator. Lembremo-nos de que ele resume separadamente os valores positivos e negativos de um determinado campo para todos os registros e, em seguida, retorna seu quociente: divide o positivo pelo módulo negativo obtido. Assim, se em alguma célula do hipercubo prevalecerem incrementos positivos nos preços, obteremos um fator de lucro visivelmente acima de 1, e já se prevalecerem incrementos negativos, o fator de lucro será visivelmente menor que 1. Em outras palavras, todos os valores diferentes de 1 apontam boas condições para abrir uma transação longa ou uma curta. Quando o fator de lucro é maior que 1, as compras são rentáveis, já quando o fator de lucro é menor que 1, as vendas são lucrativas. O fator de lucro real das vendas é o inverso do valor calculado.
Realizemos uma análise com base no EURUSD H1. Escolhemos os parâmetros de entrada:
- SelectorX=hour-of-day
- SelectorY=day-of-week
- Filter1=field
- Filter1Field=datetime
- Filter1Value1=2019.01.01
- Filter1Value2=2020.01.01
- AggregatorType=Profit Factor
- AggregatorField=price range (OC)
- SortBy=value (descending)
Não nos interessa uma lista completa de 120 linhas contendo resultados. Fornecemos apenas os valores iniciais e finais que indicam as opções de compra e venda mais lucrativas (graças à classificação incluída, elas aparecem no início e no final).
OLAPQTS (EURUSD,H1) Bars read: 137597 OLAPQTS (EURUSD,H1) ProfitFactorAggregator<QUOTE_RECORD_FIELDS> FIELD_PRICE_RANGE_OC [120] OLAPQTS (EURUSD,H1) Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0]; OLAPQTS (EURUSD,H1) Selectors: 2 OLAPQTS (EURUSD,H1) X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24] OLAPQTS (EURUSD,H1) Y: WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [5] OLAPQTS (EURUSD,H1) Processed records: 6196 OLAPQTS (EURUSD,H1) [value] [title] OLAPQTS (EURUSD,H1) [ 0] 5.85417 "00; 1`Monday" OLAPQTS (EURUSD,H1) [ 1] 5.79204 "00; 5`Friday" OLAPQTS (EURUSD,H1) [ 2] 5.25194 "00; 4`Thursday" OLAPQTS (EURUSD,H1) [ 3] 4.10104 "01; 4`Thursday" OLAPQTS (EURUSD,H1) [ 4] 4.00463 "01; 2`Tuesday" OLAPQTS (EURUSD,H1) [ 5] 2.93725 "01; 3`Wednesday" OLAPQTS (EURUSD,H1) [ 6] 2.50000 "00; 3`Wednesday" OLAPQTS (EURUSD,H1) [ 7] 2.44557 "15; 1`Monday" OLAPQTS (EURUSD,H1) [ 8] 2.43496 "04; 5`Friday" OLAPQTS (EURUSD,H1) [ 9] 2.36278 "20; 3`Wednesday" OLAPQTS (EURUSD,H1) [ 10] 2.33917 "04; 4`Thursday" ... OLAPQTS (EURUSD,H1) [110] 0.49096 "09; 3`Wednesday" OLAPQTS (EURUSD,H1) [111] 0.48241 "13; 4`Thursday" OLAPQTS (EURUSD,H1) [112] 0.45891 "19; 4`Thursday" OLAPQTS (EURUSD,H1) [113] 0.45807 "19; 3`Wednesday" OLAPQTS (EURUSD,H1) [114] 0.44993 "14; 3`Wednesday" OLAPQTS (EURUSD,H1) [115] 0.44513 "23; 4`Thursday" OLAPQTS (EURUSD,H1) [116] 0.42693 "23; 1`Monday" OLAPQTS (EURUSD,H1) [117] 0.37026 "10; 1`Monday" OLAPQTS (EURUSD,H1) [118] 0.34662 "23; 3`Wednesday" OLAPQTS (EURUSD,H1) [119] 0.19705 "23; 5`Friday"
Observe que em cada valor são exibidos os rótulos das duas dimensões X e Y - hora e dia da semana.
Os valores obtidos não estão totalmente corretos, porque é considerado o spread. Para resolver este problema, podemos usar campos personalizados. Por exemplo, para estimar o potencial impacto dos spreads, salvaremos no primeiro campo personalizado o intervalo da barra menos o spread. Para o segundo campo, calcularemos a orientação da barra menos os spreads.
virtual void fillCustomFields() override { const double newBarRange = get(FIELD_PRICE_RANGE_OC); const double spread = get(FIELD_SPREAD); set(FIELD_CUSTOM1, MathSign(newBarRange) * (MathAbs(newBarRange) - spread)); set(FIELD_CUSTOM2, MathSign(newBarRange) * MathSign(MathAbs(newBarRange) - spread)); // ... }
Escolhemos o campo personalizado número 1 como o campo do agregador e obtemos os seguintes resultados:
OLAPQTS (EURUSD,H1) Bars read: 137598 OLAPQTS (EURUSD,H1) ProfitFactorAggregator<QUOTE_RECORD_FIELDS> FIELD_CUSTOM1 [120] OLAPQTS (EURUSD,H1) Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0]; OLAPQTS (EURUSD,H1) Selectors: 2 OLAPQTS (EURUSD,H1) X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24] OLAPQTS (EURUSD,H1) Y: WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [5] OLAPQTS (EURUSD,H1) Processed records: 6196 OLAPQTS (EURUSD,H1) [value] [title] OLAPQTS (EURUSD,H1) [ 0] 6.34239 "00; 5`Friday" OLAPQTS (EURUSD,H1) [ 1] 5.63981 "00; 1`Monday" OLAPQTS (EURUSD,H1) [ 2] 5.15044 "00; 4`Thursday" OLAPQTS (EURUSD,H1) [ 3] 4.41176 "01; 2`Tuesday" OLAPQTS (EURUSD,H1) [ 4] 4.18052 "01; 4`Thursday" OLAPQTS (EURUSD,H1) [ 5] 3.04167 "01; 3`Wednesday" OLAPQTS (EURUSD,H1) [ 6] 2.60000 "00; 3`Wednesday" OLAPQTS (EURUSD,H1) [ 7] 2.53118 "15; 1`Monday" OLAPQTS (EURUSD,H1) [ 8] 2.50118 "04; 5`Friday" OLAPQTS (EURUSD,H1) [ 9] 2.47716 "04; 4`Thursday" OLAPQTS (EURUSD,H1) [ 10] 2.46208 "20; 3`Wednesday" OLAPQTS (EURUSD,H1) [ 11] 2.20858 "03; 5`Friday" OLAPQTS (EURUSD,H1) [ 12] 2.11964 "03; 1`Monday" OLAPQTS (EURUSD,H1) [ 13] 2.11123 "19; 2`Tuesday" OLAPQTS (EURUSD,H1) [ 14] 2.10998 "01; 1`Monday" OLAPQTS (EURUSD,H1) [ 15] 2.07638 "10; 4`Thursday" OLAPQTS (EURUSD,H1) [ 16] 1.95498 "09; 5`Friday" ... OLAPQTS (EURUSD,H1) [105] 0.59029 "11; 5`Friday" OLAPQTS (EURUSD,H1) [106] 0.55008 "14; 5`Friday" OLAPQTS (EURUSD,H1) [107] 0.54643 "13; 3`Wednesday" OLAPQTS (EURUSD,H1) [108] 0.50484 "09; 3`Wednesday" OLAPQTS (EURUSD,H1) [109] 0.50000 "22; 1`Monday" OLAPQTS (EURUSD,H1) [110] 0.49744 "06; 2`Tuesday" OLAPQTS (EURUSD,H1) [111] 0.46686 "13; 4`Thursday" OLAPQTS (EURUSD,H1) [112] 0.44753 "19; 3`Wednesday" OLAPQTS (EURUSD,H1) [113] 0.44499 "19; 4`Thursday" OLAPQTS (EURUSD,H1) [114] 0.43838 "14; 3`Wednesday" OLAPQTS (EURUSD,H1) [115] 0.41290 "23; 4`Thursday" OLAPQTS (EURUSD,H1) [116] 0.39770 "23; 1`Monday" OLAPQTS (EURUSD,H1) [117] 0.35586 "10; 1`Monday" OLAPQTS (EURUSD,H1) [118] 0.34721 "23; 3`Wednesday" OLAPQTS (EURUSD,H1) [119] 0.18769 "23; 5`Friday"
Eles mostram que as operações devem gerar lucro na quinta-feira em particular: compras às 0, 1 e 4 da manhã e vendas às 19 e 23 horas. Na sexta-feira, recomenda-se comprar às 0, 3, 4, 9 da manhã e vender às 11, 14 e 23. É verdade que a venda às 23h de sexta-feira pode ser arriscada devido ao fechamento das negociações e a um possível gap numa direção desfavorável (mas, a propósito, a análise de gaps também pode ser facilmente automatizada usando campos personalizados). Daqui para frente, 2 e mais são considerados um fator de lucro aceitável (para, 0.5 e menos, respectivamente). Na prática, os indicadores geralmente são piores que os teóricos, por isso, é preciso deixar uma margem de segurança.
Faz sentido considerar o fator de lucro não apenas em termos de amplitude, mas também no que diz respeito ao número de candles altistas e baixistas. Para fazer isso, é necessário selecionar como campo agregador o tipo (formulário) da barra. Um ou dois candles de tamanhos extraordinários gera, às vezes, algumas das sumas lucrativas. Esse tipo de aumentos repentinos se tornam visíveis se compararmos o fator de lucro segundo o tamanho dos candles com o fator de lucro segundo a quantidade de candles em ambas as direções.
De um modo geral, não somos obrigados a analisar no mesmo período gráfico selecionado no seletor inferior para o campo de data. Em particular, agora usamos "hour-of-day" no período gráfico H1. É permitido usar qualquer período gráfico menor ou igual ao seletor inferior no campo de data. Por exemplo, podemos fazer uma análise semelhante no M15, preservando o agrupamento por horas com a ajuda do seletor "hour-of-day". Assim, encontraremos o fator de lucro para as barras de quinze minutos. No entanto, neste caso, nossa estratégia de negociação requer que seja indicado com exatidão o momento de entrada no mercado durante uma hora, para fazer isso, seria bom poder analisar quais as maneiras mais prováveis de o candle se formar a cada hora (em outras palavras, entender após que tipo de contra-movimentos é que é formado o corpo principal da barra). Nos comentários do código fonte OLAPQTS, é apresentado um exemplo da "digitalização" das caudas das barras.
Outra maneira de identificar barras estáveis de "compra" e de "venda" por horas e dias da semana é usar o ProgressiveTotalAggregator. Para ele, seria necessário definir o seletor "ordinal number" (passagem por todas as barras) para o eixo X, seletores "hour-of-day" e "day-of-week" para os eixos Y e Z, o campo anterior para agregação — "custom 1". Como resultado, obteríamos curvas reais de saldos de negociação para cada barra horária. No entanto, não é muito conveniente enviar esses dados para o log e analisá-los, portanto esse método é mais adequado quando anexado um "display" gráfico. Mas, como isso complicaria a implementação, por enquanto, conformamo-nos com o log.
Criaremos um EA SingleBar que executará transações de acordo com os ciclos encontrados usando a análise OLAP. Os principais parâmetros de entrada fornecerão negociação conforme o horário previsto:
input string BuyHours = ""; input string SellHours = ""; input uint ActiveDayOfWeek = 0;
Os parâmetros de string BuyHours e SellHours aceitam listas de horas nas quais é necessário comprar e vender, respectivamente. As horas em cada lista são separadas por vírgulas. O parâmetro ActiveDayOfWeek define o dia da semana (valores de 1 a 5, isto é, de segunda-feira a sexta-feira). No estágio de teste, somente testaremos hipóteses para cada dia separadamente, embora no futuro o EA deva manter um horário que combine todos os dias da semana. Se ActiveDayOfWeek estiver definido como 0, o EA negociará todos os dias no mesmo horário, mas, para fazer isso, primeiro precisaremos realizar uma análise OLAP para uma única dimensão "hour-of-day", redefinindo "day-of-week" no eixo Y. Quem desejar pode verificar essa estratégia por conta própria.
As configurações são lidas e verificadas no OnInit:
int buyHours[], sellHours[]; int parseHours(const string &data, int &result[]) { string str[]; const int n = StringSplit(data, ',', str); ArrayResize(result, n); for(int i = 0; i < n; i++) { result[i] = (int)StringToInteger(str[i]); } return n; } int OnInit() { const int trend = parseHours(BuyHours, buyHours); const int reverse = parseHours(SellHours, sellHours); return trend > 0 || reverse > 0 ? INIT_SUCCEEDED : INIT_PARAMETERS_INCORRECT; }
No manipulador OnTick, verificaremos as listas de horários de negociação e definiremos a variável especial mode como +1, ou -1 se numa delas for encontrada a hora atual. Se a hora não for encontrada em nenhum lugar, mode será 0, o que significará fechar as posições existentes (se houver) e não abrir novas. Se ainda não houver ordens e mode não for igual a zero, será aberta uma nova posição. Se a posição já existir e corresponder à direção do horário, ela será mantida. Se a direção da posição e do sinal não corresponderem, ocorrerá uma reversão. Somente uma posição poderá ser aberta por vez.
template<typename T> int ArrayFind(const T &array[], const T value) { const int n = ArraySize(array); for(int i = 0; i < n; i++) { if(array[i] == value) return i; } return -1; } void OnTick() { MqlTick tick; if(!SymbolInfoTick(_Symbol, tick)) return; const int h = TimeHour(TimeCurrent()); int mode = 0; if(ArrayFind(buyHours, h) > -1) { mode = +1; } else if(ArrayFind(sellHours, h) > -1) { mode = -1; } if(ActiveDayOfWeek != 0 && ActiveDayOfWeek != _TimeDayOfWeek()) mode = 0; // skip all days except specified // pick up existing orders (if any) const int direction = CurrentOrderDirection(); if(mode == 0) { if(direction != 0) { OrdersCloseAll(); } return; } if(direction != 0) // there exist open orders { if(mode == direction) // keep direction { return; // existing trade goes on } OrdersCloseAll(); } const int type = mode > 0 ? OP_BUY : OP_SELL; const double p = type == OP_BUY ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID); OrderSend(_Symbol, type, Lot, p, 100, 0, 0); }
A negociação é realizada apenas com base na abertura de barras. Isso é estabelecido na estratégia de negociação. As funções auxiliares ArrayFind, CurrentOrderDirection, OrdersCloseAll são apresentadas abaixo. Todos eles, como o próprio EA, usam a biblioteca MT4Orders para um trabalho simplificado com a API de negociação. O MT4Bridge/MT4Time.mqh anexado também é usado para trabalhar com datas.
int CurrentOrderDirection(const string symbol = NULL) { for(int i = OrdersTotal() - 1; i >= 0; i--) { if(OrderSelect(i, SELECT_BY_POS)) { if(OrderType() <= OP_SELL && (symbol == NULL || symbol == OrderSymbol())) { return OrderType() == OP_BUY ? +1 : -1; } } } return 0; } void OrdersCloseAll(const string symbol = NULL, const int type = -1) // OP_BUY or OP_SELL { for(int i = OrdersTotal() - 1; i >= 0; i--) { if(OrderSelect(i, SELECT_BY_POS)) { if(OrderType() <= OP_SELL && (type == -1 || OrderType() == type) && (symbol == NULL || symbol == OrderSymbol())) { OrderClose(OrderTicket(), OrderLots(), OrderType() == OP_BUY ? SymbolInfoDouble(OrderSymbol(), SYMBOL_BID) : SymbolInfoDouble(OrderSymbol(), SYMBOL_ASK), 100); } } } }
O código fonte completo está anexado ao artigo. Entre outras coisas, que são omitidas no texto do artigo por questões de concisão, há, por exemplo, um cálculo teórico do fator de lucro usando a mesma lógica do mecanismo OLAP. Isso permite comparar seu valor com o valor prático do fator de lucro emitido pelo testador. Esses dois valores geralmente são semelhantes, mas não correspondem exatamente. Obviamente, um fator de lucro teórico só faz sentido se nas configurações do Expert Advisor for selecionado um horário de negociação unidirecional — compras (BuyHours) ou vendas (SellHours). Caso contrário, os dois modos se sobrepõem, e o fator de lucro teórico tende a um valor próximo de 1. Além disso, lembramos que o fator de lucro favorável para vendas em cálculos teóricos é indicado por valores inferiores a 1, pois é o inverso do fator de lucro normal. Por exemplo, um fator de lucro teórico para vendas de 0,5 significa um fator de lucro prático de testador igual a 2. Para o modo de compra, os valores de fator de lucro teórico e prático têm a mesma interpretação: valores acima de 1 são lucro e menos que 1 é perda.
Testaremos o trabalho do EA do SingleBar em 2019 no EURUSD H1. Definimos os valores encontrados do horário de negociação para sexta-feira:
- BuyHours=0,4,3,9
- SellHours=23,14,11
- ActiveDayOfWeek=5
Em princípio, a ordem das horas não é importante, mas aqui estão em ordem decrescente de lucratividade esperada. Como resultado do teste, obtemos o relatório:
Fig. 4 Relatório do EA SingleBar - trading de sextas-feiras 2019, EURUSD H1
Os indicadores são bons, mas isso não surpreende, já que este ano é o ano em que realizada a análise. Deslocamos o início dos testes para o início de 2018 e vemos há quanto tempo estão funcionando as tendências encontradas.
Fig. 5 Relatório do EA SingleBar - trading de sextas-feiras 2019, no intervalo 2018-2019, EURUSD H1
Embora os indicadores tenham piorado, é claro que as mesmas “regras” têm estado funcionando desde meados de 2018 e, portanto, podem ser encontradas anteriormente usando a análise OLAP para negociação “no futuro atual”. No entanto, buscar o período de análise ideal e esclarecer a duração dos padrões encontrados é um grande tópico separado. Em certo sentido, a análise OLAP requer a mesma otimização que a otimização de EAs. Teoricamente, seria possível implementar uma abordagem na qual o OLAP fosse incorporado a um EA executado no testador com base em diferentes partes do histórico, tendo em conta um deslocamento das datas de início e diferentes durações, e em seguida chamar um teste com base num período 'forward' para cada uma. Esta abordagem é conhecida como Cluster Walk-Forward e é usada com EAs habituais, mas o MetaTrader não a suporta totalmente (no momento da escrita do artigo, apenas é possível iniciar automaticamente os testes 'forward', mas será necessário implementar a mudança e o redimensionamento da janela de otimização independentemente em MQL5 ou com ferramentas de terceiros, como scripts de shell).
Em geral, o OLAP deve ser considerado como uma ferramenta de pesquisa que identifica áreas para um desenvolvimento mais completo usando outros meios, incluindo o uso de uma abordagem tradicional para otimizar EAs com base nos resultados da análise. A seguir, veremos como o mecanismo OLAP pode ser incorporado ao EA e usado em tempo real - tanto no testador quanto online.
Verificamos a estratégia de negociação atual por mais alguns dias. Mostraremos deliberadamente dias bons e ruins.
Fig. 6.a Relatório do EA SingleBar - trading de terças-feiras 2018-2019, segundo análise de 2019, EURUSD H1
Fig. 6.b Relatório do EA SingleBar - trading de quartas-feiras 2018-2019, segundo análise de 2019, EURUSD H1
Fig. 6.c Relatório do EA SingleBar - trading de quintas-feiras 2018-2019, segundo análise de 2019, EURUSD H1
O comportamento ambíguo da negociação em diferentes dias da semana, como esperado, demonstra a falta de soluções universais e requer refinamento.
Vamos ver quais horários de negociação podem ser encontrados se analisarmos as cotações por um período mais longo, por exemplo, de 2015 a 2019 e depois negociarmos no modo 'forward' em 2019.
OLAPQTS (EURUSD,H1) Bars read: 137606 OLAPQTS (EURUSD,H1) ProfitFactorAggregator<QUOTE_RECORD_FIELDS> FIELD_CUSTOM3 [120] OLAPQTS (EURUSD,H1) Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1420070400.0 ... 1546300800.0]; OLAPQTS (EURUSD,H1) Selectors: 2 OLAPQTS (EURUSD,H1) X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24] OLAPQTS (EURUSD,H1) Y: WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [5] OLAPQTS (EURUSD,H1) Processed records: 24832 OLAPQTS (EURUSD,H1) [value] [title] OLAPQTS (EURUSD,H1) [ 0] 2.04053 "01; 3`Wednesday" OLAPQTS (EURUSD,H1) [ 1] 1.78702 "01; 4`Thursday" OLAPQTS (EURUSD,H1) [ 2] 1.75055 "15; 1`Monday" OLAPQTS (EURUSD,H1) [ 3] 1.71793 "00; 1`Monday" OLAPQTS (EURUSD,H1) [ 4] 1.69210 "00; 4`Thursday" OLAPQTS (EURUSD,H1) [ 5] 1.64361 "04; 3`Wednesday" OLAPQTS (EURUSD,H1) [ 6] 1.63956 "20; 3`Wednesday" OLAPQTS (EURUSD,H1) [ 7] 1.62157 "05; 3`Wednesday" OLAPQTS (EURUSD,H1) [ 8] 1.53032 "00; 3`Wednesday" OLAPQTS (EURUSD,H1) [ 9] 1.49733 "16; 1`Monday" OLAPQTS (EURUSD,H1) [ 10] 1.48539 "01; 5`Friday" ... OLAPQTS (EURUSD,H1) [109] 0.74241 "16; 5`Friday" OLAPQTS (EURUSD,H1) [110] 0.70346 "13; 3`Wednesday" OLAPQTS (EURUSD,H1) [111] 0.68990 "23; 2`Tuesday" OLAPQTS (EURUSD,H1) [112] 0.66238 "23; 4`Thursday" OLAPQTS (EURUSD,H1) [113] 0.66176 "14; 4`Thursday" OLAPQTS (EURUSD,H1) [114] 0.62968 "13; 1`Monday" OLAPQTS (EURUSD,H1) [115] 0.62585 "23; 5`Friday" OLAPQTS (EURUSD,H1) [116] 0.60150 "14; 5`Friday" OLAPQTS (EURUSD,H1) [117] 0.55621 "11; 2`Tuesday" OLAPQTS (EURUSD,H1) [118] 0.54919 "23; 3`Wednesday" OLAPQTS (EURUSD,H1) [119] 0.49804 "11; 3`Wednesday"
Como vemos, a medida que o período aumenta, a rentabilidade de cada hora tomada individualmente diminui, a generalização em algum momento começa a se opor à busca de padrões, excedendo sua vida útil. Quarta-feira parece ser o dia mais lucrativo, mas mesmo não se comporta de maneira estável no teste 'forward', por exemplo, para configurações:
- BuyHours=1,4,20,5,0
- SellHours=11,23,13
- ActiveDayOfWeek=3
obtemos o relatório:
Fig. 7 Relatório do EA SingleBar - trading de quartas-feiras 2015-2020, segundo análise excluindo 2019, EURUSD H1
Para resolver esse problema, é necessária uma técnica mais versátil, na qual o OLAP seja apenas uma de entre um conjunto de ferramentas. Além disso, faz sentido procurar padrões mais complexos (multifatoriais). Vamos tentar criar outra estratégia de negociação, que levará em conta não apenas o ciclo do tempo, mas também a direção da barra anterior.
Projetando estratégias de negociação baseadas na análise de cotações OLAP. Parte II
Pode-se assumir que a direção de cada barra depende, em certa medida, da direção da anterior. Essa dependência provavelmente tem um caráter cíclico semelhante, conectado por flutuações intradia e intra-semana, que descobrimos na seção anterior. Em outras palavras, ao realizar uma análise OLAP, é necessário não apenas acumular o tamanho e a direção das barras pelas horas e dias da semana, mas também levar em consideração as características da barra anterior. Para isso, usamos os campos personalizados restantes.
No terceiro campo personalizado, calculamos a covariância "assimétrica" de duas barras adjacentes. A covariância comum, como produto das faixas de movimentos de preços dentro das barras, levando em consideração a direção (mais - aumento, menos - diminuição), não possui valor preditivo especial, uma vez que o valor de covariância obtido para as barras anteriores e seguintes são equivalentes. No entanto, as decisões de negociação são efetivas apenas para a próxima barra, embora sejam tomadas com base na anterior. Em outras palavras, a alta covariância por causa dos grandes movimentos da barra anterior já é recuperada, uma vez que essa barra está no histórico. Por isso, foi proposta a fórmula da covariância "assimétrica", na qual são levados em consideração apenas o intervalo da barra futura e o sinal do produto com a anterior.
Guiado pelo valor desse campo, pode-se verificar duas estratégias: de tendência e de reversão. Por exemplo, se usarmos o agregador de fator de lucro para o campo em questão, valores maiores que 1 indicarão que será lucrativo negociar com base na direção da barra anterior, enquanto valores menores que 1 indicarão que é bom operar de acordo com o movimento oposto. Por analogia com cálculos anteriores, quanto mais extremo o valor (muito mais que 1 ou muito menos que 1), mais lucrativas serão as operações de tendência ou reversão, respectivamente, de acordo com as estatísticas.
No quarto campo personalizado, manteremos o sinal de barras adjacentes unidirecionais (+1) ou multidirecionais (-1). Isso permitirá o uso de agregadores para determinar o número de barras de reversão adjacentes e a eficácia das entradas para estratégias de tendência e de reversão.
Como é garantido que as barras sejam tratadas em ordem cronológica (isso é fornecido pelo adaptador), podemos salvar os tamanhos de barra necessários para os cálculos da barra anterior e seu spread em variáveis estáticas. Obviamente, isso é possível desde que se deva usar uma única instância do adaptador de cotações (lembre-se de que sua instância padrão é criada no arquivo de cabeçalho). Além de ser válido para nosso exemplo, isso é mais fácil de entender, porém, em geral, o adaptador deveria ser transferido ao construtor de registros personalizados (tais como CustomQuotesBaseRecord) e, em seguida, ao método fillCustomFields deveria ser passado um contêiner permitindo armazenar e restaurar o estado, por exemplo, como um link para a matriz: fillCustomFields(double &bundle[]).
class CustomQuotesRecord: public QuotesRecord { private: static double previousBarRange; static double previousSpread; public: // ... virtual void fillCustomFields() override { const double newBarRange = get(FIELD_PRICE_RANGE_OC); const double spread = get(FIELD_SPREAD); // ... if(MathAbs(previousBarRange) > previousSpread) { double mult = newBarRange * previousBarRange; double value = MathSign(mult) * MathAbs(newBarRange); // this is an attempt to approximate average losses due to spreads value += MathSignNonZero(value) * -1 * MathMax(spread, previousSpread); set(FIELD_CUSTOM3, value); set(FIELD_CUSTOM4, MathSign(mult)); } else { set(FIELD_CUSTOM3, 0); set(FIELD_CUSTOM4, 0); } previousBarRange = newBarRange; previousSpread = spread; } };
Modificamos os valores dos parâmetros de entrada OLAPQTS. A alteração mais importante é a escolha de "custom 3" no AggregatorField. Os seletores para X e Y, o tipo de agregador (fator de lucro) e a classificação permanecerão os mesmos. Também mudaremos o filtro de data.
- SelectorX=hour-of-day
- SelectorY=day-of-week
- Filter1=field
- Filter1Field=datetime
- Filter1Value1=2018.01.01
- Filter1Value2=2019.01.01
- AggregatorType=Profit Factor
- AggregatorField=custom 3
- SortBy=value (descending)
Como já vimos na análise de cotações de 2015, a escolha de um período prolongado é provavelmente justificada apenas para sistemas nos quais é procurado um ciclo sazonal - seletor month-of-year. No nosso caso, com os seletores de horas e dias da semana, analisaremos um de 2018 e, em seguida, executaremos o teste 'forward' para 2019.
OLAPQTS (EURUSD,H1) Bars read: 137642 OLAPQTS (EURUSD,H1) Aggregator: ProfitFactorAggregator<QUOTE_RECORD_FIELDS> FIELD_CUSTOM3 [120] OLAPQTS (EURUSD,H1) Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1514764800.0 ... 1546300800.0]; OLAPQTS (EURUSD,H1) Selectors: 2 OLAPQTS (EURUSD,H1) X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24] OLAPQTS (EURUSD,H1) Y: WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [5] OLAPQTS (EURUSD,H1) Processed records: 6203 OLAPQTS (EURUSD,H1) [value] [title] OLAPQTS (EURUSD,H1) [ 0] 2.65010 "23; 1`Monday" OLAPQTS (EURUSD,H1) [ 1] 2.37966 "03; 1`Monday" OLAPQTS (EURUSD,H1) [ 2] 2.33875 "04; 4`Thursday" OLAPQTS (EURUSD,H1) [ 3] 1.96317 "20; 3`Wednesday" OLAPQTS (EURUSD,H1) [ 4] 1.91188 "18; 2`Tuesday" OLAPQTS (EURUSD,H1) [ 5] 1.89293 "23; 3`Wednesday" OLAPQTS (EURUSD,H1) [ 6] 1.87159 "12; 1`Monday" OLAPQTS (EURUSD,H1) [ 7] 1.78903 "15; 5`Friday" OLAPQTS (EURUSD,H1) [ 8] 1.74461 "01; 4`Thursday" OLAPQTS (EURUSD,H1) [ 9] 1.73821 "13; 2`Tuesday" OLAPQTS (EURUSD,H1) [ 10] 1.73244 "14; 2`Tuesday" ... OLAPQTS (EURUSD,H1) [110] 0.57331 "22; 4`Thursday" OLAPQTS (EURUSD,H1) [111] 0.51515 "07; 5`Friday" OLAPQTS (EURUSD,H1) [112] 0.50202 "05; 5`Friday" OLAPQTS (EURUSD,H1) [113] 0.48557 "04; 2`Tuesday" OLAPQTS (EURUSD,H1) [114] 0.46313 "23; 2`Tuesday" OLAPQTS (EURUSD,H1) [115] 0.44182 "00; 2`Tuesday" OLAPQTS (EURUSD,H1) [116] 0.40907 "13; 1`Monday" OLAPQTS (EURUSD,H1) [117] 0.38230 "10; 1`Monday" OLAPQTS (EURUSD,H1) [118] 0.36296 "22; 5`Friday" OLAPQTS (EURUSD,H1) [119] 0.29462 "17; 5`Friday"
Para testar a estratégia incorporada no campo "custom 3", escreveremos o EA NextBar. Ele nos permitirá verificar as oportunidades de negociação encontradas no testador. A construção geral do Expert Advisor é semelhante à do SingleBar, uma vez que são usados os mesmos parâmetros de entrada, as mesmas funções e fragmentos de código. No entanto, a lógica de negociação se complica um pouco mais. Ela pode ser encontrada no arquivo de origem anexado.
Escolhemos as combinações de horas mais atraentes (com um fator de lucro de cerca de 2 e superior, ou 0,5 e inferior), por exemplo, para segunda-feira:
- PositiveHours=23,3
- NegativeHours=10,13
- ActiveDayOfWeek=1
Iniciamos o teste no intervalo 2018.01.01-2019.05.01:
Fig. 8 Relatório do EA NextBar - no intervalo 01/01/2018-01/05/2019 após o OLAP de 2018, EURUSD H1
Como se pode ver no relatório, a estratégia ainda dava certo em janeiro de 2019, já após isso começou uma série de perdidas. Precisamos, de alguma maneira, descobrir a vida útil dos padrões e aprender como alterá-los em tempo real.
Negociação adaptativa baseada na análise de cotações OLAP
Até o momento, para análise OLAP usamos o EA OLAPQTS não-negociante, e para testar hipóteses desenvolvimos EAs negociantes individuais. O mais lógico e conveniente seria ter um mecanismo OLAP incorporado ao EA. Assim, o robô poderia analisar automaticamente cotações, com uma determinada frequência, e alterar o horário de negociação. Além disso, mediante a exibição dos parâmetros de análise básicos nas configurações do EA, podemos otimizá-los, emulando, assim, a abordagem Walk-Forward mencionada anteriormente. Chamamos o EA de OLAPQRWF, como uma abreviação de OLAP of Quotes with Rolling Walk-Forward.
Parâmetros de entrada básicos:
input int BarNumberLookBack = 2880; // BarNumberLookBack (week: 120 H1, month: 480 H1, year: 5760 H1) input double Threshold = 2.0; // Threshold (PF >= Threshold && PF <= 1/Threshold) input int Strategy = 0; // Strategy (0 - single bar, 1 - adjacent bars)
- BarNumberLookBack determina o número de barras históricas com base nas quais é realizada a análise OLAP (assume-se o uso do período gráfico H1).
- Threshold é o limiar-sinal de fator de lucro para abrir transações.
- Strategy é o número da estratégia a ser verificada (agora temos apenas dois: 0 - estatísticas da direção de barras individuais, 1 - estatísticas das direções de duas barras adjacentes).
Além disso, precisamos definir a frequência com que será recalculado o cubo OLAP.
enum UPDATEPERIOD { monthly, weekly }; input UPDATEPERIOD Update = monthly;
Além disso, podemos escolher não apenas uma estratégia, mas também campos personalizados nos quais é considerado o agregador. Lembre-se de que os campos 1 e 3 levam em consideração o tamanho das barras (para as estratégias 0 e 1, respectivamente), enquanto os campos 2 e 4 consideram apenas o número de barras em cada direção.
enum CUSTOMFIELD { range, count }; input CUSTOMFIELD CustomField = range;
Tomamos de empréstimo a classe CustomQuotesRecord inalterada ao OLAPQTS. No que diz repeito a todos os anteriores parâmetros de entrada para configurar os seletores, o filtro e o agregador, iremos torná-los constantes ou apenas variáveis globais (caso mudem dependendo da estratégia) sem alterar o nome.
const SELECTORS SelectorX = SELECTOR_DAYHOUR; const ENUM_FIELDS FieldX = FIELD_DATETIME; const SELECTORS SelectorY = SELECTOR_WEEKDAY; const ENUM_FIELDS FieldY = FIELD_DATETIME; const SELECTORS SelectorZ = SELECTOR_NONE; const ENUM_FIELDS FieldZ = FIELD_NONE; const SELECTORS _Filter1 = SELECTOR_FILTER; const ENUM_FIELDS _Filter1Field = FIELD_INDEX; int _Filter1value1 = -1; // to be filled with index of first bar to process const int _Filter1value2 = -1; const AGGREGATORS _AggregatorType = AGGREGATOR_PROFITFACTOR; ENUM_FIELDS _AggregatorField = FIELD_CUSTOM1; const SORT_BY _SortBy = SORT_BY_NONE;
Observe que não filtraremos as barras por tempo, mas, sim, por quantidade usando FIELD_INDEX. O valor real para _Filter1value1 será calculado como a diferença entre o número total de barras e o BarNumberLookBack especificado. Portanto, sempre serão analisadas as últimas barras BarNumberLookBack.
O Expert Advisor negociará no modo de barras desde o manipulador OnTick.
bool freshStart = true; void OnTick() { if(!isNewBar()) return; if(Bars(_Symbol, _Period) < BarNumberLookBack) return; const int m0 = TimeMonth(iTime(_Symbol, _Period, 0)); const int w0 = _TimeDayOfWeek(); const int m1 = TimeMonth(iTime(_Symbol, _Period, 1)); const int w1 = _TimeDayOfWeek(); static bool success = false; if((Update == monthly && m0 != m1) || (Update == weekly && w0 < w1) || freshStart) { success = calcolap(); freshStart = !success; } //... }
Dependendo da frequência da análise, aguardamos até que o mês ou a semana mude e executamos o OLAP na função calcolap.
bool calcolap() { _Filter1value1 = Bars(_Symbol, _Period) - BarNumberLookBack; _AggregatorField = Strategy == 0 ? (ENUM_FIELDS)(FIELD_CUSTOM1 + CustomField) : (ENUM_FIELDS)(FIELD_CUSTOM3 + CustomField); _defaultQuotesAdapter.reset(); const int processed = _defaultEngine.process(_selectorArray, _selectorField, _AggregatorType, _AggregatorField, stats, // custom display object _SortBy, _Filter1value1, _Filter1value2); return processed == BarNumberLookBack; }
Também já conhecemos essa parte do código. Algumas alterações se aplicam apenas à seleção do campo de agregação de acordo com os parâmetros de entrada, bem como à instalação do índice da primeira barra analisada.
Outra alteração importante tem a ver com o uso de um objeto de exibição próprio especial (stats), chamado pelo mecanismo OLAP após a análise.
class MyOLAPStats: public Display { // ... public: virtual void display(MetaCube *cube, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override { // ... } void trade(const double threshold, const double lots, const int strategy = 0) { // ... } }; MyOLAPStats stats;
Como esse objeto irá extrair as melhores horas de negociação a partir das estatísticas obtidas, será mais fácil confiar a negociação a esse objeto. O método trade está reservado para fazer isso. Assim, ao OnTick podemos adicionar:
void OnTick() { // ... if(success) { stats.trade(Threshold, Lot, Strategy); } else { OrdersCloseAll(); } }
Agora trataremos da classe MyOLAPStats em mais detalhes. Os resultados da análise OLAP processam os métodos display (método de exibição virtual principal) e saveVector (auxiliar).
#define N_HOURS 24 #define N_DAYS 5 #define AXIS_HOURS 0 #define AXIS_DAYS 1 class MyOLAPStats: public Display { private: bool filled; double index[][3]; // value, hour, day int cursor; protected: bool saveVector(MetaCube *cube, const int &consts[], const SORT_BY sortby = SORT_BY_NONE) { PairArray *result = NULL; cube.getVector(0, consts, result, sortby); if(CheckPointer(result) == POINTER_DYNAMIC) { const int n = ArraySize(result.array); if(n == N_HOURS) { for(int i = 0; i < n; i++) { index[cursor][0] = result.array[i].value; index[cursor][1] = i; index[cursor][2] = consts[AXIS_DAYS]; cursor++; } } delete result; return n == N_HOURS; } return false; } public: virtual void display(MetaCube *cube, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override { int consts[]; const int n = cube.getDimension(); ArrayResize(consts, n); ArrayInitialize(consts, 0); filled = false; ArrayResize(index, N_HOURS * N_DAYS); ArrayInitialize(index, 1); cursor = 0; if(n == 2) { const int i = AXIS_DAYS; int m = cube.getDimensionRange(i); // should be 5 work days for(int j = 0; j < m; j++) { consts[i] = j; if(!saveVector(cube, consts, sortby)) // 24 hours (values) per current day { Print("Bad data format"); return; } consts[i] = 0; } filled = true; ArraySort(index); ArrayPrint(index); } else { Print("Incorrect cube structure"); } } //... };
Na classe é descrita a matriz bidimensional index para armazenar indicadores de desempenho da negociação em relação ao horário. No método display, esta matriz é preenchida sequencialmente com vetores do cubo OLAP. A função auxiliar saveVector copia números para todas as 24 horas de um dia útil específico. De acordo com a segunda dimensão, na matriz index são gravados sequencialmente o valor do índice, o número da hora e o número do dia útil. O fato de os indicadores estarem no primeiro elemento (zero) permite classificar a matriz por fator de lucro, mas, em princípio, isso é necessário apenas para tornar mais conveniente a exibição no log.
Com base nos dados da matriz index, é selecionado o modo de negociação e as ordens são enviadas diretamente para as contagens de hora do dia e do dia da semana em que excedido o limite de fator de lucro especificado.
void trade(const double threshold, const double lots, const int strategy = 0) { const int h = TimeHour(lastBar); const int w = _TimeDayOfWeek() - 1; int mode = 0; for(int i = 0; i < N_HOURS * N_DAYS; i++) { if(index[i][1] == h && index[i][2] == w) { if(index[i][0] >= threshold) { mode = +1; Print("+ Rule ", i); break; } if(index[i][0] <= 1.0 / threshold) { mode = -1; Print("- Rule ", i); break; } } } // pick up existing orders (if any) const int direction = CurrentOrderDirection(); if(mode == 0) { if(direction != 0) { OrdersCloseAll(); } return; } if(strategy == 0) { if(direction != 0) // there exist open orders { if(mode == direction) // keep direction { return; // existing trade goes on } OrdersCloseAll(); } const int type = mode > 0 ? OP_BUY : OP_SELL; const double p = type == OP_BUY ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID); const double sl = StopLoss > 0 ? (type == OP_BUY ? p - StopLoss * _Point : p + StopLoss * _Point) : 0; OrderSend(_Symbol, type, Lot, p, 100, sl, 0); } // ... }
Por razões de brevidade, é fornecido o processamento de apenas uma estratégia de negociação. Esse código já é familiar. O código fonte completo está anexado ao artigo.
Iremos otimizar o EA OLAPQRWF no intervalo de 2015 a 2019 e, em seguida, faremos um teste para 2019. Observe que a essência da otimização realmente se resume a encontrar os meta-parâmetros da negociação, isto é: duração da análise OLAP, frequência da reconstrução do cubo OLAP, escolha da estratégia e do campo de agregação personalizado. Durante a otimização, a cada execução, o EA cria um cubo OLAP _com base no histórico_ e negocia com base em seu _futuro_ virtual, usando as configurações a partir do _pasado_. Parece como se, neste caso, não fosse necessário um teste 'forward'? Acontece que a eficácia desta negociação depende diretamente dos meta-parâmetros especificados, portanto, é necessário verificar a aplicabilidade das configurações selecionadas no intervalo out-of-sample.
Otimizaremos todos os parâmetros que afetam a análise, exceto o período Update (iremos deixá-lo como mensal):
- BarNumberLookBack=720||720||480||5760||Y
- Threshold=2.0||2.0||0.5||5.0||Y
- Strategy=0||0||1||1||Y
- Update=0||0||0||1||N
- CustomField=0||0||0||1||Y
O EA considera um indicador de otimização personalizado sintético igual ao produto do índice de Sharpe e ao número de transações. Para esse indicador, a melhor previsão é com os seguintes parâmetros de entrada do EA:
- BarNumberLookBack=2160
- Threshold=3.0
- Strategy=0
- Update=monthly
- CustomField=count
Iniciamos um teste separado de 2015 a 2020 e observamos o comportamento no teste 'forward'.
Fig. 9 Relatório do EA OLAPQRWF - 01/01/2015-01/01/2020 após 'otimização' da janela de análise OLAP incluindo 2018, EURUSD H1
Pode-se afirmar que o EA que encontra automaticamente um horário lucrativo negocia com sucesso em 2019 usando o tamanho da janela de agregação encontrado nos anos anteriores. Obviamente, esse sistema requer mais estudo e análise, mas podemos confirmar que essa ferramenta funciona integramente.
Fim do artigo
Bem, aprimoramos e expandimos a funcionalidade da biblioteca OLAP para processamento de dados on-line, em seguida, nós a vinculamos com uma área de cotações através de um adaptador especial e classes de registros de trabalho. Com ajuda dos programas apresentados, é possível encontrar no histórico padrões que garantam um trading lucrativo. No primeiro estágio, para se familiarizar com o OLAP, é conveniente usar EAs individuais que apenas processam dados de origem e apresentam indicadores generalizados, em outras palavras, que não operam. Eles também permitem elaborar e depurar algoritmos para calcular campos personalizados contendo os elementos básicos das estratégias de negociação (hipóteses). Nas seguintes etapas de aprendizado do OLAP, o mecanismo irá ser integrado aos robôs de negociação novos ou aos já existentes. Além disso, a otimização dos robôs deverá levar em conta não apenas os parâmetros operacionais usuais, mas também novos meta-parâmetros relacionados ao OLAP e à coleta de estatísticas.
Certamente, as ferramentas do OLAP não são uma panacéia para todos, especialmente em situações de mercado imprevisíveis, ademais, não podem oferecer um graal pronto para uso. Porém, uma análise de cotações embutida sem dúvida expande os recursos do trader ao tentar procurar estratégias e o desenvolvimento de EAs.
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/7535
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso