Automatizando Estratégias de Trading em MQL5 (Parte 11): Desenvolvendo um Sistema de Trading em Grade Multi-Nível
Introdução
Em nosso artigo anterior (Parte 10), desenvolvemos um Expert Advisor para automatizar a Estratégia de Impulso em Tendência Lateral usando uma combinação de médias móveis e filtros de impulso em MetaQuotes Language 5 (MQL5). Agora, na Parte 11, focamos na construção de um sistema de trading em grade multi-nível que utiliza uma abordagem de grade em camadas para capitalizar as flutuações do mercado. Estruturaremos o artigo por meio dos seguintes tópicos:
- Introdução
- Compreendendo a Arquitetura de um Sistema de Grade Multi-Nível
- Implementação em MQL5
- Backtesting
- Conclusão
Ao final deste artigo, você terá uma compreensão abrangente e um programa totalmente funcional pronto para trading em ambiente real. Vamos mergulhar!
Compreendendo a Arquitetura de um Sistema de Grade Multi-Nível
Um sistema de trading em grade multi-nível é uma abordagem estruturada que capitaliza a volatilidade do mercado ao posicionar uma série de ordens de compra e venda em intervalos predeterminados ao longo de uma faixa de níveis de preço. A estratégia que estamos prestes a implementar não se trata de prever a direção do mercado, mas sim de lucrar com o fluxo natural dos preços, capturando ganhos independentemente de o mercado subir, cair ou se mover lateralmente.
Com base nesse conceito, nosso programa implementará a estratégia de grade multi-nível por meio de um design modular que separa a detecção de sinais, a execução de ordens e o gerenciamento de risco. No desenvolvimento do sistema, primeiro inicializaremos parâmetros-chave — como médias móveis para identificação de sinais de trade — e configuraremos uma estrutura de cesta que encapsula detalhes das operações, como tamanhos iniciais de lote, espaçamento da grade e níveis de take profit.
À medida que o mercado evolui, o programa monitorará os movimentos de preço para acionar novas operações e gerenciar posições existentes, adicionando ordens em cada nível da grade com base em condições predefinidas e ajustando dinamicamente os parâmetros de risco. A arquitetura também incluirá funções para recalcular pontos de equilíbrio (break-even), modificar alvos de take profit e encerrar posições quando metas de lucro ou limites de risco forem atingidos. Esse plano estruturado não apenas organizará o programa em componentes distintos e gerenciáveis, mas também garantirá que cada camada da grade contribua para uma estratégia de trading coesa e com gerenciamento de risco, pronta para backtesting robusto e implementação em ambiente de negociação. Em resumo, é assim que a arquitetura será.

Implementação em MQL5
Para criar o programa em MQL5, abra o MetaEditor, vá até o Navigator, localize a pasta Indicators, clique na aba "New" e siga as instruções para criar o arquivo. Depois de criado, no ambiente de codificação, precisaremos declarar alguns metadados e variáveis globais que utilizaremos ao longo do programa.
//+------------------------------------------------------------------+ //| Copyright 2025, Forex Algo-Trader, Allan. | //| "https://t.me/Forex_Algo_Trader" | //+------------------------------------------------------------------+ #property copyright "Forex Algo-Trader, Allan" #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #property description "This EA trades multiple signals with grid strategy using baskets" #property strict #include <Trade/Trade.mqh> //--- Includes the standard trading library for executing trades CTrade obj_Trade; //--- Instantiates the CTrade object used for managing trade operations //--- Closure Mode Enumeration and Inputs enum ClosureMode { CLOSE_BY_PROFIT, //--- Use total profit (in currency) to close positions CLOSE_BY_POINTS //--- Use points threshold from breakeven to close positions }; input group "General EA Settings" input ClosureMode closureMode = CLOSE_BY_POINTS; input double inpLotSize = 0.01; input long inpMagicNo = 1234567; input int inpTp_Points = 100; input int inpGridSize = 100; input double inpMultiplier = 2.0; input int inpBreakevenPts = 50; input int maxBaskets = 5; input group "MA Indicator Settings" //--- Begins the input group for Moving Average indicator settings input int inpMAPeriod = 21; //--- Period used for the Moving Average calculation
Aqui, estabelecemos os componentes fundamentais do nosso programa, garantindo execução de trades fluida e gerenciamento estratégico de posições. Começamos incluindo a biblioteca "Trade/Trade.mqh", que concede acesso às funções essenciais de execução de trades. Para facilitar as operações de trading, instanciamos o objeto "CTrade" como "obj_Trade", permitindo posicionar, modificar e fechar ordens de forma eficiente dentro da nossa estratégia automatizada.
Definimos a enumeração "ClosureMode" para fornecer flexibilidade no gerenciamento das saídas das operações. O programa pode operar em dois modos: "CLOSE_BY_PROFIT", que aciona o fechamento quando o lucro total acumulado atinge um limite especificado na moeda da conta, e "CLOSE_BY_POINTS", que fecha posições com base em uma distância predefinida a partir do nível de break-even. Isso garante que o usuário possa ajustar dinamicamente sua estratégia de saída com base no comportamento do mercado e na tolerância ao risco.
Em seguida, introduzimos uma seção estruturada de input em "General EA Settings" para permitir a personalização da estratégia de trading pelo usuário. Especificamos "inpLotSize" para controlar o volume inicial de trade e usamos "inpMagicNo" para identificar de forma única as operações do EA, evitando conflitos com outras estratégias ativas. Para execução baseada em grade, definimos "inpTp_Points" para determinar o nível de take profit por operação, enquanto "inpGridSize" define o espaçamento entre ordens sucessivas da grade. O parâmetro "inpMultiplier" escala progressivamente os tamanhos das operações, implementando uma expansão adaptativa da grade para maximizar o potencial de lucro enquanto gerencia a exposição ao risco. Para refinar ainda mais o controle de risco, configuramos "inpBreakevenPts", que move as operações para o break-even após determinado limite, e "maxBaskets", que limita o número de estruturas de grade independentes que o EA pode gerenciar simultaneamente.
Para aprimorar a filtragem de trades, incorporamos um mecanismo de Média Móvel em "MA Indicator Settings". Aqui, definimos "inpMAPeriod", que determina o número de períodos usados para calcular a Média Móvel. Isso ajuda a alinhar o trading em grade com as tendências predominantes do mercado, filtrando condições desfavoráveis e garantindo que as entradas de trade estejam alinhadas com o impulso geral do mercado. Em seguida, como precisaremos lidar com várias instâncias de sinais, podemos definir uma estrutura de cesta.
//--- Basket Structure struct BasketInfo { int basketId; //--- Unique basket identifier (e.g., 1, 2, 3...) long magic; //--- Unique magic number for this basket to differentiate its trades int direction; //--- Direction of the basket: POSITION_TYPE_BUY or POSITION_TYPE_SELL double initialLotSize; //--- The initial lot size assigned to the basket double currentLotSize; //--- The current lot size for subsequent grid trades double gridSize; //--- The next grid level price for the basket double takeProfit; //--- The current take-profit price for the basket datetime signalTime; //--- Timestamp of the signal to avoid duplicate trade entries };
Aqui, definimos a estrutura "BasketInfo" para organizar e gerenciar cada cesta de grade de forma independente. Atribuímos um "basketId" único para rastrear cada cesta e usamos "magic" para garantir que nossas operações permaneçam distintas das demais. Determinamos a direção da operação com "direction", decidindo se estamos executando uma estratégia de compra ou venda.
Definimos "initialLotSize" para a primeira operação na cesta, enquanto "currentLotSize" é ajustado dinamicamente para operações subsequentes. Utilizamos "gridSize" para estabelecer o espaçamento entre as operações e "takeProfit" para definir nosso alvo de lucro. Para evitar entradas duplicadas, rastreamos o tempo do sinal usando "signalTime". Em seguida, podemos declarar um array de armazenamento usando a estrutura definida e algumas variáveis globais iniciais.
BasketInfo baskets[]; //--- Dynamic array to store active basket information int nextBasketId = 1; //--- Counter for assigning unique IDs to new baskets long baseMagic = inpMagicNo;//--- Base magic number obtained from user input double takeProfitPts = inpTp_Points * _Point; //--- Convert take profit points into price units double gridSize_Spacing = inpGridSize * _Point; //--- Convert grid size spacing from points into price units double profitTotal_inCurrency = 100; //--- Target profit in account currency for closing positions //--- Global Variables int totalBars = 0; //--- Stores the total number of bars processed so far int handle; //--- Handle for the Moving Average indicator double maData[]; //--- Array to store Moving Average indicator data
Utilizamos o array dinâmico "baskets[]" para armazenar informações das cestas ativas, garantindo que possamos rastrear múltiplas posições de forma eficiente. A variável "nextBasketId" atribui identificadores únicos a cada nova cesta, enquanto "baseMagic" garante que todas as operações dentro do sistema sejam distinguíveis usando o número mágico definido pelo usuário. Convertimos as entradas do usuário em unidades de preço multiplicando "inpTp_Points" e "inpGridSize" por "_Point", permitindo controle preciso sobre "takeProfitPts" e "gridSize_Spacing". A variável "profitTotal_inCurrency" define o limite de lucro necessário para fechar todas as posições ao utilizar um modo de fechamento baseado em moeda.
Para análise técnica, inicializamos "totalBars" para rastrear o número de barras de preço processadas, "handle" para armazenar o identificador do indicador de Média Móvel e "maData[]" como um array para armazenar os valores calculados da Média Móvel. Com isso, podemos definir alguns protótipos de funções que utilizaremos ao longo do programa quando necessário.
//--- Function Prototypes void InitializeBaskets(); //--- Prototype for basket initialization function (if used) void CheckAndCloseProfitTargets(); //--- Prototype to check and close positions if profit target is reached void CheckForNewSignal(double ask, double bid); //--- Prototype to check for new trading signals based on price bool ExecuteInitialTrade(int basketIdx, double ask, double bid, int direction); //--- Prototype to execute the initial trade for a basket void ManageGridPositions(int basketIdx, double ask, double bid); //--- Prototype to manage and add grid positions for an active basket void UpdateMovingAverage(); //--- Prototype to update the Moving Average indicator data bool IsNewBar(); //--- Prototype to check whether a new bar has formed double CalculateBreakevenPrice(int basketId); //--- Prototype to calculate the weighted breakeven price for a basket void CheckBreakevenClose(int basketIdx, double ask, double bid); //--- Prototype to check and close positions based on breakeven criteria void CloseBasketPositions(int basketId); //--- Prototype to close all positions within a basket string GetPositionComment(int basketId, bool isInitial); //--- Prototype to generate a comment for a position based on basket and trade type int CountBasketPositions(int basketId); //--- Prototype to count the number of open positions in a basket
Aqui, definimos protótipos de funções que descrevem as operações principais do nosso sistema de trading em grade multi-nível. Essas funções garantirão modularidade, permitindo estruturar de forma eficiente a execução de operações, o gerenciamento de posições e o controle de risco. Começamos com "InitializeBaskets()", que prepara o sistema para rastrear cestas ativas. A função "CheckAndCloseProfitTargets()" garante que as posições sejam fechadas assim que as condições de lucro predefinidas forem atingidas. Para detectar oportunidades de operação, "CheckForNewSignal()" avalia os níveis de preço para determinar se um novo sinal de trading deve ser executado.
A função "ExecuteInitialTrade()" gerenciará a primeira operação dentro de uma cesta, enquanto "ManageGridPositions()" garantirá que os níveis da grade sejam expandidos de forma sistemática à medida que o mercado se move. "UpdateMovingAverage()" recupera e processa os dados do indicador de Média Móvel para dar suporte à geração de sinais. Para o gerenciamento das operações, "IsNewBar()" ajuda a otimizar a execução garantindo que as ações sejam realizadas apenas com dados de preço novos. "CalculateBreakevenPrice()" calcula o preço de break-even ponderado para uma cesta, enquanto "CheckBreakevenClose()" determina se as condições foram atendidas para encerrar posições com base nos critérios de break-even.
Para gerenciar as posições das cestas, "CloseBasketPositions()" facilita saídas controladas, garantindo que todas as posições dentro de uma cesta sejam fechadas quando necessário. "GetPositionComment()" fornece anotações estruturadas das operações, melhorando o rastreamento das operações, e "CountBasketPositions()" ajuda a monitorar o número de posições ativas dentro de uma cesta, garantindo que o sistema opere dentro dos limites de risco definidos.
Agora podemos começar inicializando a média móvel, já que a utilizaremos exclusivamente para a geração de sinais.
//+------------------------------------------------------------------+ //--- Expert initialization function //+------------------------------------------------------------------+ int OnInit() { handle = iMA(_Symbol, _Period, inpMAPeriod, 0, MODE_SMA, PRICE_CLOSE); //--- Initialize the Moving Average indicator with specified period and parameters if(handle == INVALID_HANDLE) { Print("ERROR: Unable to initialize Moving Average indicator!"); //--- Log error if indicator initialization fails return(INIT_FAILED); //--- Terminate initialization with a failure code } ArraySetAsSeries(maData, true); //--- Set the moving average data array as a time series (newest data at index 0) ArrayResize(baskets, 0); //--- Initialize the baskets array as empty at startup obj_Trade.SetExpertMagicNumber(baseMagic); //--- Set the default magic number for trade operations return(INIT_SUCCEEDED); //--- Signal that initialization completed successfully }
No manipulador de evento OnInit, começamos inicializando o indicador de Média Móvel usando a função iMA(), onde aplicamos o período e os parâmetros especificados para obter dados baseados em tendência. Se o identificador for inválido (INVALID_HANDLE), registramos uma mensagem de erro e encerramos o processo de inicialização com INIT_FAILED para evitar que o EA seja executado com dados ausentes.
Em seguida, configuramos o array de dados da média móvel usando a função ArraySetAsSeries, garantindo que os valores mais recentes sejam armazenados no índice 0 para acesso eficiente. Depois, redimensionamos o array "baskets" para zero, preparando-o para alocação dinâmica à medida que novas operações forem abertas. Por fim, atribuímos o número mágico base ao objeto de trading usando o método "SetExpertMagicNumber()", permitindo que o EA rastreie e gerencie operações com um identificador único. Se todos os componentes forem inicializados com sucesso, retornamos INIT_SUCCEEDED para confirmar que o EA está pronto para iniciar a execução.
Como armazenamos dados, podemos liberar os recursos quando não precisarmos mais do programa no manipulador de evento OnDeinit, chamando a função IndicatorRelease.
//+------------------------------------------------------------------+ //--- Expert deinitialization function //+------------------------------------------------------------------+ void OnDeinit(const int reason) { IndicatorRelease(handle); //--- Release the indicator handle to free up resources when the EA is removed }
Em seguida, podemos prosseguir para processar os dados a cada tick no manipulador de evento OnTick. No entanto, queremos executar o programa uma vez por barra, então precisaremos definir uma função para isso.
//+------------------------------------------------------------------+ //--- Expert tick function //+------------------------------------------------------------------+ void OnTick() { if(IsNewBar()) { //--- Execute logic only when a new bar is detected } }
O protótipo da função é o seguinte.
//+------------------------------------------------------------------+ //--- Check for New Bar //+------------------------------------------------------------------+ bool IsNewBar() { int bars = iBars(_Symbol, _Period); //--- Get the current number of bars on the chart for the symbol and period if(bars > totalBars) { //--- Compare the current number of bars with the previously stored total totalBars = bars; //--- Update the stored bar count to the new value return true; //--- Return true to indicate a new bar has formed } return false; //--- Return false if no new bar has been detected }
Aqui, definimos a função "IsNewBar()", que verifica se uma nova barra foi formada no gráfico, o que é essencial para garantir que nosso EA processe novos dados de preço apenas quando uma nova barra aparecer, evitando recálculos desnecessários. Começamos obtendo o número atual de barras no gráfico usando a função iBars, que fornece a contagem total de barras históricas para o símbolo e período ativos. Em seguida, comparamos esse valor com a variável "totalBars", que armazena a contagem de barras registrada anteriormente.
Se a contagem atual de barras for maior que o valor armazenado na variável "totalBars", isso significa que uma nova barra apareceu. Nesse caso, atualizamos a variável "totalBars" com a nova contagem e retornamos "true", sinalizando que o EA deve prosseguir com os cálculos baseados em barra ou com a lógica de trading. Se nenhuma nova barra for detectada, a função retorna "false", garantindo que o EA não execute operações redundantes na mesma barra.
Agora, ao detectar uma nova barra, precisamos obter os dados da média móvel para processamento adicional. Para isso, utilizamos uma função.
//+------------------------------------------------------------------+ //--- Update Moving Average //+------------------------------------------------------------------+ void UpdateMovingAverage() { if(CopyBuffer(handle, 0, 1, 3, maData) < 0) { //--- Copy the latest 3 values from the Moving Average indicator buffer into the maData array Print("Error: Unable to update Moving Average data."); //--- Log an error if copying the indicator data fails } }
Para a função "UpdateMovingAverage()", que garante que nosso EA obtenha os valores mais recentes do indicador de Média Móvel, utilizamos a função CopyBuffer() para extrair os três valores mais recentes do buffer do indicador de Média Móvel e armazená-los no array "maData". Os parâmetros especificam o identificador do indicador ("handle"), o índice do buffer (0 para a linha principal), a posição inicial (1 para ignorar a barra atual em formação), a quantidade de valores (3) e o array de destino ("maData").
Se não conseguirmos obter os dados, registramos uma mensagem de erro usando a função Print() para nos alertar sobre possíveis problemas na obtenção dos dados do indicador, protegendo o EA contra valores de média móvel incompletos ou ausentes e garantindo confiabilidade na tomada de decisões. Em seguida, podemos chamar a função e usar os dados obtidos para geração de sinais.
UpdateMovingAverage(); //--- Update the Moving Average data for the current bar double ask = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits); //--- Get and normalize the current ask price double bid = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits); //--- Get and normalize the current bid price //--- Check for new signals and create baskets accordingly CheckForNewSignal(ask, bid);
Após obter os dados do indicador, recuperamos os preços atuais de ask e bid usando a função SymbolInfoDouble() com as constantes SYMBOL_ASK e SYMBOL_BID, respectivamente. Como os valores de preço frequentemente possuem várias casas decimais, utilizamos a função NormalizeDouble com o parâmetro _Digits para garantir que estejam formatados corretamente de acordo com a precisão do símbolo.
Por fim, chamamos a função "CheckForNewSignal()", passando os preços de ask e bid normalizados. Aqui está o trecho de código da função.
//+------------------------------------------------------------------+ //--- Check for New Crossover Signal //+------------------------------------------------------------------+ void CheckForNewSignal(double ask, double bid) { double close1 = iClose(_Symbol, _Period, 1); //--- Retrieve the close price of the previous bar double close2 = iClose(_Symbol, _Period, 2); //--- Retrieve the close price of the bar before the previous one datetime currentBarTime = iTime(_Symbol, _Period, 1); //--- Get the time of the current bar if(ArraySize(baskets) >= maxBaskets) return; //--- Exit if the maximum allowed baskets are already active //--- Buy signal: current bar closes above the MA while the previous closed below it if(close1 > maData[1] && close2 < maData[1]) { //--- Check if this signal was already processed by comparing signal times in existing baskets for(int i = 0; i < ArraySize(baskets); i++) { if(baskets[i].signalTime == currentBarTime) return; //--- Signal already acted upon; exit the function } int basketIdx = ArraySize(baskets); //--- Index for the new basket equals the current array size ArrayResize(baskets, basketIdx + 1); //--- Increase the size of the baskets array to add a new basket if (ExecuteInitialTrade(basketIdx, ask, bid, POSITION_TYPE_BUY)){ baskets[basketIdx].signalTime = currentBarTime; //--- Record the time of the signal after a successful trade } } //--- Sell signal: current bar closes below the MA while the previous closed above it else if(close1 < maData[1] && close2 > maData[1]) { //--- Check for duplicate signals by verifying the signal time in active baskets for(int i = 0; i < ArraySize(baskets); i++) { if(baskets[i].signalTime == currentBarTime) return; //--- Signal already acted upon; exit the function } int basketIdx = ArraySize(baskets); //--- Determine the index for the new basket ArrayResize(baskets, basketIdx + 1); //--- Resize the baskets array to accommodate the new basket if (ExecuteInitialTrade(basketIdx, ask, bid, POSITION_TYPE_SELL)){ baskets[basketIdx].signalTime = currentBarTime; //--- Record the signal time for the new sell basket } } }
Para a função "CheckForNewSignal()", primeiro obtemos os preços de fechamento das duas barras anteriores usando a função iClose(). Isso nos ajuda a determinar se ocorreu um cruzamento. Também utilizamos a função iTime() para obter o timestamp da barra mais recente, garantindo que não processemos o mesmo sinal múltiplas vezes.
Antes de prosseguir, verificamos se o número de cestas ativas atingiu o limite "maxBaskets". Se sim, a função retorna para evitar o acúmulo excessivo de operações. Para sinais de compra, verificamos se o preço de fechamento mais recente está acima da Média Móvel enquanto o fechamento anterior estava abaixo dela. Se essa condição de cruzamento for atendida, percorremos as cestas existentes para garantir que o mesmo sinal ainda não tenha sido processado. Se o sinal for novo, aumentamos o tamanho do array "baskets", armazenamos a nova cesta no próximo índice disponível e chamamos a função "ExecuteInitialTrade()" com uma ordem POSITION_TYPE_BUY. Se a operação for executada com sucesso, registramos o tempo do sinal para evitar entradas duplicadas.
Da mesma forma, para sinais de venda, verificamos se o preço de fechamento mais recente está abaixo da Média Móvel enquanto o fechamento anterior estava acima dela. Se essa condição for atendida e nenhum sinal duplicado for encontrado, expandimos o array "baskets", executamos uma operação inicial de venda usando a função "ExecuteInitialTrade()" com uma ordem POSITION_TYPE_SELL e armazenamos o tempo do sinal para manter a unicidade. A função para executar as operações é a seguinte.
//+------------------------------------------------------------------+ //--- Execute Initial Trade //+------------------------------------------------------------------+ bool ExecuteInitialTrade(int basketIdx, double ask, double bid, int direction) { baskets[basketIdx].basketId = nextBasketId++; //--- Assign a unique basket ID and increment the counter baskets[basketIdx].magic = baseMagic + baskets[basketIdx].basketId * 10000; //--- Calculate a unique magic number for the basket baskets[basketIdx].initialLotSize = inpLotSize; //--- Set the initial lot size for the basket from input baskets[basketIdx].currentLotSize = inpLotSize; //--- Initialize current lot size to the same as the initial lot size baskets[basketIdx].direction = direction; //--- Set the trade direction (buy or sell) for the basket bool isTradeExecuted = false; //--- Initialize flag to track if the trade was successfully executed string comment = GetPositionComment(baskets[basketIdx].basketId, true); //--- Generate a comment string indicating an initial trade obj_Trade.SetExpertMagicNumber(baskets[basketIdx].magic); //--- Set the trade object's magic number to the basket's unique value if(direction == POSITION_TYPE_BUY) { baskets[basketIdx].gridSize = ask - gridSize_Spacing; //--- Set the grid level for subsequent buy orders below the current ask price baskets[basketIdx].takeProfit = ask + takeProfitPts; //--- Calculate the take profit level for the buy order if(obj_Trade.Buy(baskets[basketIdx].currentLotSize, _Symbol, ask, 0, baskets[basketIdx].takeProfit, comment)) { Print("Basket ", baskets[basketIdx].basketId, ": Initial BUY at ", ask, " | Magic: ", baskets[basketIdx].magic); //--- Log the successful buy order details isTradeExecuted = true; //--- Mark the trade as executed successfully } else { Print("Basket ", baskets[basketIdx].basketId, ": Initial BUY failed, error: ", GetLastError()); //--- Log the error if the buy order fails ArrayResize(baskets, ArraySize(baskets) - 1); //--- Remove the basket if trade execution fails } } else if(direction == POSITION_TYPE_SELL) { baskets[basketIdx].gridSize = bid + gridSize_Spacing; //--- Set the grid level for subsequent sell orders above the current bid price baskets[basketIdx].takeProfit = bid - takeProfitPts; //--- Calculate the take profit level for the sell order if(obj_Trade.Sell(baskets[basketIdx].currentLotSize, _Symbol, bid, 0, baskets[basketIdx].takeProfit, comment)) { Print("Basket ", baskets[basketIdx].basketId, ": Initial SELL at ", bid, " | Magic: ", baskets[basketIdx].magic); //--- Log the successful sell order details isTradeExecuted = true; //--- Mark the trade as executed successfully } else { Print("Basket ", baskets[basketIdx].basketId, ": Initial SELL failed, error: ", GetLastError()); //--- Log the error if the sell order fails ArrayResize(baskets, ArraySize(baskets) - 1); //--- Remove the basket if trade execution fails } } return (isTradeExecuted); //--- Return the status of the trade execution }
Definimos a função "ExecuteInitialTrade()" para garantir que cada cesta tenha um identificador único, atribuir um número mágico distinto e inicializar os principais parâmetros de trading antes de enviar a ordem. Primeiro, atribuímos um "basketId" incrementando a variável "nextBasketId". Em seguida, geramos um número mágico único para a cesta adicionando um deslocamento escalonado ao valor "baseMagic", garantindo que cada cesta opere de forma independente. Os tamanhos de lote inicial e atual são ambos definidos como "inpLotSize" para estabelecer o tamanho base da operação para essa cesta. A "direction" é armazenada para diferenciar entre cestas de compra e venda.
Para garantir que as operações sejam identificáveis, chamamos a função "GetPositionComment()" para gerar um comentário descritivo e aplicamos o número mágico da cesta ao objeto de trading usando o método "SetExpertMagicNumber()". A função é definida a seguir, onde utilizamos a função StringFormat para obter o comentário por meio de um operador ternário.
//+------------------------------------------------------------------+ //--- Generate Position Comment //+------------------------------------------------------------------+ string GetPositionComment(int basketId, bool isInitial) { return StringFormat("Basket_%d_%s", basketId, isInitial ? "Initial" : "Grid"); //--- Generate a standardized comment string for a position indicating basket ID and trade type }
Se a direção for POSITION_TYPE_BUY, calculamos o nível da grade subtraindo "gridSize_Spacing" do preço de ask e determinamos o nível de take profit adicionando "takeProfitPts" ao preço de ask. Em seguida, utilizamos a função "Buy()" da classe "CTrade" para enviar a ordem. Se for bem-sucedido, registramos os detalhes da operação usando a função Print() e marcamos a operação como executada. Se a operação falhar, registramos o erro usando a função GetLastError() e utilizamos a função ArrayResize() para reduzir o tamanho do array "baskets", removendo a cesta que falhou.
Para uma operação de venda (POSITION_TYPE_SELL), calculamos o nível da grade adicionando "gridSize_Spacing" ao preço de oferta e determinamos o nível de take profit subtraindo "takeProfitPts" do preço de oferta. A operação é executada usando a função "Sell()". Assim como nas operações de compra, a execução bem-sucedida é registrada usando a função "Print()", e falhas resultam em um registro de erro com GetLastError, seguido do redimensionamento do array "baskets" usando "ArrayResize()" para remover a cesta que falhou.
Antes de executar qualquer operação, a função garante que o array tenha espaço suficiente chamando "ArrayResize()" para aumentar seu tamanho. Por fim, a função retorna "true" se a operação for executada com sucesso e "false" caso contrário. Ao executar o programa, temos o seguinte resultado.

A partir da imagem, podemos ver que temos posições iniciais confirmadas conforme as cestas ou sinais realizados. Em seguida, precisamos avançar para o gerenciamento dessas posições, gerenciando cada cesta individualmente. Para isso, utilizamos um loop for para iteração.
//--- Loop through all active baskets to manage grid positions and potential closures for(int i = 0; i < ArraySize(baskets); i++) { ManageGridPositions(i, ask, bid); //--- Manage grid trading for the current basket }
Aqui, iteramos por todas as cestas ativas usando um for, garantindo que cada cesta seja gerenciada adequadamente. A função ArraySize determina o número atual de cestas no array "baskets", fornecendo o limite superior do loop. Isso garante que processemos todas as cestas existentes sem exceder os limites do array. Para cada cesta, chamamos a função "ManageGridPositions()", passando o índice da cesta juntamente com os preços normalizados de "ask" e "bid". A função é a seguinte.
//+------------------------------------------------------------------+ //--- Manage Grid Positions //+------------------------------------------------------------------+ void ManageGridPositions(int basketIdx, double ask, double bid) { bool newPositionOpened = false; //--- Flag to track if a new grid position has been opened string comment = GetPositionComment(baskets[basketIdx].basketId, false); //--- Generate a comment for grid trades in this basket obj_Trade.SetExpertMagicNumber(baskets[basketIdx].magic); //--- Ensure the trade object uses the basket's unique magic number if(baskets[basketIdx].direction == POSITION_TYPE_BUY) { if(ask <= baskets[basketIdx].gridSize) { //--- Check if the ask price has reached the grid level for a buy order baskets[basketIdx].currentLotSize *= inpMultiplier; //--- Increase the lot size based on the defined multiplier if(obj_Trade.Buy(baskets[basketIdx].currentLotSize, _Symbol, ask, 0, baskets[basketIdx].takeProfit, comment)) { newPositionOpened = true; //--- Set flag if the grid buy order is successfully executed Print("Basket ", baskets[basketIdx].basketId, ": Grid BUY at ", ask); //--- Log the grid buy execution details baskets[basketIdx].gridSize = ask - gridSize_Spacing; //--- Adjust the grid level for the next potential buy order } else { Print("Basket ", baskets[basketIdx].basketId, ": Grid BUY failed, error: ", GetLastError()); //--- Log an error if the grid buy order fails } } } else if(baskets[basketIdx].direction == POSITION_TYPE_SELL) { if(bid >= baskets[basketIdx].gridSize) { //--- Check if the bid price has reached the grid level for a sell order baskets[basketIdx].currentLotSize *= inpMultiplier; //--- Increase the lot size based on the multiplier for grid orders if(obj_Trade.Sell(baskets[basketIdx].currentLotSize, _Symbol, bid, 0, baskets[basketIdx].takeProfit, comment)) { newPositionOpened = true; //--- Set flag if the grid sell order is successfully executed Print("Basket ", baskets[basketIdx].basketId, ": Grid SELL at ", bid); //--- Log the grid sell execution details baskets[basketIdx].gridSize = bid + gridSize_Spacing; //--- Adjust the grid level for the next potential sell order } else { Print("Basket ", baskets[basketIdx].basketId, ": Grid SELL failed, error: ", GetLastError()); //--- Log an error if the grid sell order fails } } } //--- If a new grid position was opened and there are multiple positions, adjust the take profit to breakeven if(newPositionOpened && CountBasketPositions(baskets[basketIdx].basketId) > 1) { double breakevenPrice = CalculateBreakevenPrice(baskets[basketIdx].basketId); //--- Calculate the weighted breakeven price for the basket double newTP = (baskets[basketIdx].direction == POSITION_TYPE_BUY) ? breakevenPrice + (inpBreakevenPts * _Point) : //--- Set new TP for buy positions breakevenPrice - (inpBreakevenPts * _Point); //--- Set new TP for sell positions baskets[basketIdx].takeProfit = newTP; //--- Update the basket's take profit level with the new value for(int j = PositionsTotal() - 1; j >= 0; j--) { //--- Loop through all open positions to update TP where necessary ulong ticket = PositionGetTicket(j); //--- Get the ticket number for the current position if(PositionSelectByTicket(ticket) && PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == baskets[basketIdx].magic) { //--- Identify positions that belong to the current basket if(!obj_Trade.PositionModify(ticket, 0, newTP)) { //--- Attempt to modify the position's take profit level Print("Basket ", baskets[basketIdx].basketId, ": Failed to modify TP for ticket ", ticket); //--- Log error if modifying TP fails } } } Print("Basket ", baskets[basketIdx].basketId, ": Breakeven = ", breakevenPrice, ", New TP = ", newTP); //--- Log the new breakeven and take profit levels } }
Aqui, implementamos a função "ManageGridPositions()" para gerenciar dinamicamente o trading baseado em grade dentro de cada cesta ativa. Garantimos que novas posições de grade sejam executadas nos níveis de preço corretos e que ajustes de lucro sejam realizados quando necessário. Começamos inicializando o indicador "newPositionOpened" para rastrear se uma nova operação de grade foi executada. Usando a função "GetPositionComment()", geramos uma string de comentário específica para o tipo de operação (inicial ou de grade). Em seguida, chamamos a função "SetExpertMagicNumber()" para atribuir o número mágico único da cesta, garantindo que todas as operações dentro da cesta sejam corretamente rastreadas.
Para cestas de compra, verificamos se o preço pedido caiu para o limite "gridSize" ou abaixo dele. Se essa condição for atendida, ajustamos o tamanho do lote multiplicando "currentLotSize" pelo parâmetro de entrada "inpMultiplier". Em seguida, tentamos enviar uma ordem de compra usando o método "Buy()" do objeto de trade "obj_Trade". Se a operação for executada com sucesso, atualizamos "gridSize" subtraindo "gridSize_Spacing", garantindo que a próxima operação de compra seja posicionada corretamente. Também registramos a execução bem-sucedida usando a função Print(). Se a ordem de compra falhar, recuperamos e registramos o erro usando a função GetLastError().
Para cestas de venda, seguimos um processo semelhante, mas, em vez disso, verificamos se o preço de oferta subiu até ou acima do limite "gridSize". Se essa condição for atendida, ajustamos o tamanho do lote aplicando o "inpMultiplier" a "currentLotSize". Em seguida, executamos uma ordem de venda usando a função "Sell()", atualizando "gridSize" ao adicionar "gridSize_Spacing" para definir o próximo nível de venda. Se a ordem for bem-sucedida, registramos os detalhes com "Print()", e se falhar, registramos o erro usando "GetLastError()".
Se uma nova posição de grade for aberta e a cesta agora contiver múltiplas operações, prosseguimos para ajustar o take profit para um nível de break-even. Primeiro, determinamos o preço de break-even chamando a função "CalculateBreakevenPrice()". Em seguida, calculamos um novo nível de take profit com base na direção da cesta:
- Para cestas de compra, o take profit é definido adicionando "inpBreakevenPts" (convertido em pontos de preço) ao preço de break-even.
- Para cestas de venda, o take profit é ajustado subtraindo "inpBreakevenPts" do preço de break-even.
Em seguida, percorremos todas as posições abertas usando a função PositionsTotal(), recuperando o número do ticket de cada posição com PositionGetTicket(). Utilizamos PositionSelectByTicket() para selecionar a posição e verificar seu símbolo com a função "PositionGetString". Também garantimos que a posição pertença à cesta correta verificando seu número mágico com o parâmetro "POSITION_MAGIC". Uma vez verificado, tentamos modificar seu take profit usando o método "PositionModify()". Se essa modificação falhar, registramos o erro.
Por fim, registramos o novo preço de break-even calculado e o nível de take profit atualizado usando a função Print(). Isso garante que a estratégia de trading em grade se adapte dinamicamente enquanto mantém pontos de saída eficientes. A função responsável por calcular o preço médio é a seguinte.
//+------------------------------------------------------------------+ //--- Calculate Weighted Breakeven Price for a Basket //+------------------------------------------------------------------+ double CalculateBreakevenPrice(int basketId) { double weightedSum = 0.0; //--- Initialize sum for weighted prices double totalLots = 0.0; //--- Initialize sum for total lot sizes for(int i = 0; i < PositionsTotal(); i++) { //--- Loop over all open positions ulong ticket = PositionGetTicket(i); //--- Retrieve the ticket for the current position if(PositionSelectByTicket(ticket) && PositionGetString(POSITION_SYMBOL) == _Symbol && StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(basketId)) >= 0) { //--- Check if the position belongs to the specified basket double lot = PositionGetDouble(POSITION_VOLUME); //--- Get the lot size of the position double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get the open price of the position weightedSum += openPrice * lot; //--- Add the weighted price to the sum totalLots += lot; //--- Add the lot size to the total lots } } return (totalLots > 0) ? (weightedSum / totalLots) : 0; //--- Return the weighted average price (breakeven) or 0 if no positions found }
Implementamos a função "CalculateBreakevenPrice()" para determinar o preço de break-even ponderado para uma determinada cesta de operações, garantindo que o nível de take profit possa ser ajustado dinamicamente com base nos preços de entrada ponderados pelo volume de todas as posições abertas dentro da cesta. Começamos inicializando "weightedSum" para armazenar a soma dos preços ponderados e "totalLots" para rastrear o tamanho total de lote de todas as posições na cesta. Em seguida, iteramos por todas as posições abertas.
Para cada posição, recuperamos seu número de ticket usando PositionGetTicket() e selecionamos a posição com PositionSelectByTicket(). Verificamos se a posição pertence ao símbolo de negociação atual. Além disso, verificamos se a posição faz parte da cesta especificada procurando o ID da cesta na string de comentário usando a função StringFind(). O comentário deve conter "Basket_" + IntegerToString(basketId) para ser classificado dentro da mesma cesta.
Uma vez verificada a posição, extraímos seu tamanho de lote usando "PositionGetDouble(POSITION_VOLUME)" e seu preço de abertura usando POSITION_PRICE_OPEN. Em seguida, multiplicamos o preço de abertura pelo tamanho do lote e adicionamos o resultado a "weightedSum", garantindo que tamanhos de lote maiores tenham maior influência no preço final de break-even. Simultaneamente, acumulamos o tamanho total de lote em "totalLots".
Após percorrer todas as posições, calculamos o preço médio ponderado de break-even dividindo "weightedSum" por "totalLots". Se não existirem posições na cesta ("totalLots" == 0), retornamos 0 para evitar erros de divisão por zero. Ao executar o programa, temos o seguinte resultado.

A partir da imagem, podemos ver que as cestas são gerenciadas de forma independente, abrindo grades e realizando o preço médio. Por exemplo, a cesta 2 possui os mesmos níveis de take profit de 0.68074. Podemos confirmar isso no diário conforme visualizado abaixo.

A partir da imagem, podemos ver que, ao abrir a posição de compra em grade para a cesta 4, também modificamos o take profit. Agora, precisamos fechar as posições com base nos modos apenas por segurança, embora não seja necessário, já que já temos os níveis modificados, conforme a seguir.
if(closureMode == CLOSE_BY_PROFIT) CheckAndCloseProfitTargets(); //--- If using profit target closure mode, check for profit conditions if(closureMode == CLOSE_BY_POINTS && CountBasketPositions(baskets[i].basketId) > 1) { CheckBreakevenClose(i, ask, bid); //--- If using points-based closure and multiple positions exist, check breakeven conditions }
Aqui, gerenciamos o fechamento das operações com base no "closureMode" selecionado. Se estiver definido como "CLOSE_BY_PROFIT", chamamos "CheckAndCloseProfitTargets()" para fechar as cestas que atingirem suas metas de lucro. Se estiver definido como "CLOSE_BY_POINTS", garantimos que a cesta possua múltiplas posições usando "CountBasketPositions()" antes de chamar "CheckBreakevenClose()" para fechar as operações no break-even quando as condições forem atendidas. As funções são as seguintes.
//+------------------------------------------------------------------+ //--- Check and Close Profit Targets (for CLOSE_BY_PROFIT mode) //+------------------------------------------------------------------+ void CheckAndCloseProfitTargets() { for(int i = 0; i < ArraySize(baskets); i++) { //--- Loop through each active basket int posCount = CountBasketPositions(baskets[i].basketId); //--- Count how many positions belong to the current basket if(posCount <= 1) continue; //--- Skip baskets with only one position as profit target checks apply to multiple positions double totalProfit = 0; //--- Initialize the total profit accumulator for the basket for(int j = PositionsTotal() - 1; j >= 0; j--) { //--- Loop through all open positions to sum their profits ulong ticket = PositionGetTicket(j); //--- Get the ticket for the current position if(PositionSelectByTicket(ticket) && StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(baskets[i].basketId)) >= 0) { //--- Check if the position is part of the current basket totalProfit += PositionGetDouble(POSITION_PROFIT); //--- Add the position's profit to the basket's total profit } } if(totalProfit >= profitTotal_inCurrency) { //--- Check if the accumulated profit meets or exceeds the profit target Print("Basket ", baskets[i].basketId, ": Profit target reached (", totalProfit, ")"); //--- Log that the profit target has been reached for the basket CloseBasketPositions(baskets[i].basketId); //--- Close all positions in the basket to secure the profits } } }
Aqui, verificamos e fechamos as cestas quando atingem o alvo de lucro no modo "CLOSE_BY_PROFIT". Percorremos "baskets" e utilizamos "CountBasketPositions()" para garantir que existam múltiplas posições. Em seguida, somamos os lucros usando "PositionGetDouble(POSITION_PROFIT)" para todas as posições na cesta. Se o lucro total atingir ou exceder "profitTotal_inCurrency", registramos o evento e chamamos "CloseBasketPositions()" para garantir os ganhos. A função "CountBasketPositions" é definida a seguir.
//+------------------------------------------------------------------+ //--- Count Positions in a Basket //+------------------------------------------------------------------+ int CountBasketPositions(int basketId) { int count = 0; //--- Initialize the counter for positions in the basket for(int i = 0; i < PositionsTotal(); i++) { //--- Loop through all open positions ulong ticket = PositionGetTicket(i); //--- Retrieve the ticket for the current position if(PositionSelectByTicket(ticket) && StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(basketId)) >= 0) { //--- Check if the position belongs to the specified basket count++; //--- Increment the counter if a matching position is found } } return count; //--- Return the total number of positions in the basket }
Utilizamos a função "CountBasketPositions()" para contar as posições em uma cesta específica. Percorremos todas as posições, recuperamos cada "ticket" com a função PositionGetTicket() e verificamos se o POSITION_COMMENT contém o ID da cesta. Se uma correspondência for encontrada, incrementamos "count". Por fim, retornamos o número total de posições na cesta. A definição da função "CloseBasketPositions()" também é a seguinte.
//+------------------------------------------------------------------+ //--- Close All Positions in a Basket //+------------------------------------------------------------------+ void CloseBasketPositions(int basketId) { for(int i = PositionsTotal() - 1; i >= 0; i--) { //--- Loop backwards through all open positions ulong ticket = PositionGetTicket(i); //--- Retrieve the ticket of the current position if(PositionSelectByTicket(ticket) && StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(basketId)) >= 0) { //--- Identify if the position belongs to the specified basket if(obj_Trade.PositionClose(ticket)) { //--- Attempt to close the position Print("Basket ", basketId, ": Closed position ticket ", ticket); //--- Log the successful closure of the position } } } }
Utilizamos a mesma lógica para iterar por todas as posições, verificá-las e fechá-las usando o método "PositionClose" Por fim, temos a função responsável por forçar o fechamento das posições quando elas ultrapassam os níveis de alvo definidos.
//+------------------------------------------------------------------+ //--- Check Breakeven Close //+------------------------------------------------------------------+ void CheckBreakevenClose(int basketIdx, double ask, double bid) { double breakevenPrice = CalculateBreakevenPrice(baskets[basketIdx].basketId); //--- Calculate the breakeven price for the basket if(baskets[basketIdx].direction == POSITION_TYPE_BUY) { if(bid >= breakevenPrice + (inpBreakevenPts * _Point)) { //--- Check if the bid price exceeds breakeven plus threshold for buy positions Print("Basket ", baskets[basketIdx].basketId, ": Closing BUY positions at breakeven + points"); //--- Log that breakeven condition is met for closing positions CloseBasketPositions(baskets[basketIdx].basketId); //--- Close all positions for the basket } } else if(baskets[basketIdx].direction == POSITION_TYPE_SELL) { if(ask <= breakevenPrice - (inpBreakevenPts * _Point)) { //--- Check if the ask price is below breakeven minus threshold for sell positions Print("Basket ", baskets[basketIdx].basketId, ": Closing SELL positions at breakeven + points"); //--- Log that breakeven condition is met for closing positions CloseBasketPositions(baskets[basketIdx].basketId); //--- Close all positions for the basket } } }
Aqui, implementamos fechamentos baseados em break-even usando "CheckBreakevenClose()". Primeiro, determinamos o preço de break-even com "CalculateBreakevenPrice()". Se a cesta estiver na direção COMPRA e o preço de oferta exceder o break-even mais o limite definido ("inpBreakevenPts * _Point"), registramos o evento e executamos "CloseBasketPositions()" para garantir os lucros. Da mesma forma, para as ordens de VENDA, verificamos se o preço de venda cai abaixo do ponto de equilíbrio menos o limite, o que aciona o fechamento da ordem. Isso garante que as posições sejam protegidas assim que o movimento de preço se alinhar com as condições de break-even.
Por fim, como inicialmente fechamos as posições por take profit, isso significa que temos "estruturas vazias" ou cestas de posições que permanecem no sistema. Portanto, para garantir a limpeza, precisamos identificar as cestas vazias que não contêm elementos e removê-las. Implementamos a seguinte lógica.
//--- Remove inactive baskets that no longer have any open positions for(int i = ArraySize(baskets) - 1; i >= 0; i--) { if(CountBasketPositions(baskets[i].basketId) == 0) { Print("Removing inactive basket ID: ", baskets[i].basketId); //--- Log the removal of an inactive basket for(int j = i; j < ArraySize(baskets) - 1; j++) { baskets[j] = baskets[j + 1]; //--- Shift basket elements down to fill the gap } ArrayResize(baskets, ArraySize(baskets) - 1); //--- Resize the baskets array to remove the empty slot } }
Aqui, garantimos que cestas inativas, que não possuem mais posições abertas, sejam removidas de forma eficiente. Percorremos o array "baskets" em ordem reversa para evitar problemas de deslocamento de índice durante a remoção. Usando "CountBasketPositions()", verificamos se uma cesta não possui operações restantes. Se estiver vazia, registramos sua remoção e deslocamos os elementos subsequentes para baixo para manter a estrutura do array. Por fim, chamamos ArrayResize() para ajustar o tamanho do array, evitando uso desnecessário de memória e garantindo que apenas cestas ativas sejam rastreadas. Essa abordagem mantém o gerenciamento das cestas eficiente e evita acúmulo desnecessário no sistema. Ao executar o programa, temos o seguinte resultado.

A partir da imagem, podemos ver que lidamos de forma eficiente com a remoção de elementos desnecessários e conseguimos gerenciar as posições em grade, alcançando assim nosso objetivo. O que resta agora é realizar o backtesting do programa, e isso é tratado na próxima seção.
Backtesting
Após um backtesting completo, por 1 ano, em 2023, utilizando as configurações padrão, temos os seguintes resultados.
Gráfico de backtest:

Relatório de backtest:

Conclusão
Em conclusão, desenvolvemos um Expert Advisor de Trading em Grade Multi-Nível em MQL5 que gerencia eficientemente entradas de operações em camadas, ajustes dinâmicos da grade e recuperação estruturada. Ao integrar espaçamento de grade escalável, progressão controlada de lotes e saídas em break-even, o sistema se adapta às flutuações do mercado enquanto otimiza risco e retorno.
Aviso: Este artigo é apenas para fins educacionais. O trading envolve riscos financeiros significativos, e as condições de mercado podem ser imprevisíveis. Backtesting adequado e gerenciamento de risco são essenciais antes da implementação em ambiente real.
Ao aplicar essas técnicas, você pode aprimorar suas habilidades em trading algorítmico e refinar sua estratégia baseada em grade. Continue testando e otimizando para sucesso no longo prazo. Boa sorte!
Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/17350
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.
Caminhe em novos trilhos: Personalize indicadores no MQL5
Kit de Ferramentos de Negociação MQL5(Parte 8): Como Implementar e Utilizar a Biblioteca History Manager EX5 em sua Base de Código
Está chegando o novo MetaTrader 5 e MQL5
Desenvolvimento de um Kit de Ferramentas para análise de ação de preço (Parte 17): Ferramenta TrendLoom EA
- 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
Um código muito bom e um EA muito rápido!
Infelizmente, há um problema com o cálculo do tamanho do lote - os multiplicadores com um decimal (como 1,3, 1,5 etc.) podem causar problemas com as funções de ordem MQL, pois o tamanho do lote às vezes fornece códigos de erro 4756 quando o multiplicador não é 1 ou 2.
Seria muito bom se o cálculo do tamanho do lote pudesse ser modificado ligeiramente para garantir que os tamanhos de lote sejam calculados adequadamente para alimentar as funções de pedido para todos os valores de multiplicador.
Seria muito bom se o cálculo do tamanho do lote pudesse ser modificado ligeiramente para garantir que os tamanhos de lote sejam calculados adequadamente para alimentar as funções de pedido para todos os valores de multiplicador.
Obrigado pelo feedback gentil. Com certeza.
Hi,
Depois de ler o artigo, achei-o útil e com certeza vou testá-lo. No entanto, parece que não estou vendo ou talvez tenha perdido o artigo sobre a separação do TP da primeira posição, que acredito também ser útil e sustentável para a estratégia de negociação.
Muito obrigado.
Muito obrigado.
Claro, obrigado.