Estratégia de Canal de Cruzamento Percentual
Visão geral
A Estratégia de Canal de Cruzamento Percentual origina-se do consultor especialista de MetaTrader 5 Percentage_Crossover_Channel_EA. Ela depende de um canal personalizado construído em torno de uma média móvel rápida e reage a toques de banda ou cruzamentos da linha média. Esta implementação do StockSharp segue a mesma lógica enquanto usa a API de alto nível para processar velas concluídas.
Construção do canal
O indicador subjacente constrói um canal dinâmico em torno do preço selecionado (fechamento por padrão):
- Calcular o preço base usando o modo Applied Price configurado.
- Aplicar uma média móvel simples de 1 período para obter o preço de referência de curto prazo.
- Calcular dois limites usando o parâmetro Percent (p. ex., 50 → ±0,5%).
- Limitar a linha média anterior dentro dos novos limites para obter o valor médio atual.
- As bandas superior e inferior são o valor médio limitado multiplicado pelos fatores ±percentagem.
Esta recursão permite que o canal atrase durante tendências fortes enquanto mantém um envelope apertado quando o preço consolida.
Lógica de trading
Dois modos de sinal diferentes estão disponíveis:
- Modo de toque de banda (padrão):
- Entrada comprada quando o mínimo da vela anterior estava acima da banda inferior e a última vela concluída a toca ou perfura.
- Entrada vendida quando o máximo da vela anterior estava abaixo da banda superior e a última vela concluída a toca ou perfura.
- Modo de cruzamento da linha média (TradeOnMiddleCross = true):
- Entrada comprada quando o preço cruza a linha média de cima para baixo.
- Entrada vendida quando o preço cruza a linha média de baixo para cima.
O indicador ReverseSignals troca as regras compradas e vendidas. A estratégia sempre fecha e reverte posições existentes enviando uma única ordem a mercado cujo volume equivale ao OrderVolume configurado mais o valor absoluto da posição atual.
Gestão de risco
Os níveis protetores opcionais emulam as configurações originais de stop-loss e take-profit do MT5:
- StopLossPoints – distância em passos de preço subtraída (comprado) ou adicionada (vendido) do preço de entrada estimado.
- TakeProfitPoints – distância em passos de preço adicionada (comprado) ou subtraída (vendido) do preço de entrada.
Se qualquer parâmetro for zero, a proteção correspondente é desativada. Os stops são avaliados em cada vela finalizada comparando máximos e mínimos da vela com os níveis armazenados. Nenhuma lógica de trailing é aplicada.
Parâmetros
| Parâmetro |
Descrição |
CandleType |
Tipo de dados de vela para assinar (período de 15 minutos por padrão). |
Percent |
Largura do canal em porcentagem do preço (convertido para fatores ±porcentagem/100). |
PriceMode |
Preço aplicado para o canal. Opções: Close, Open, High, Low, Median (H+L)/2, Typical (H+L+C)/3, Weighted (H+L+2C)/4, Average (O+H+L+C)/4. |
TradeOnMiddleCross |
Alternar entre lógica de toque de banda e lógica de cruzamento de linha média. |
ReverseSignals |
Inverter as condições compradas e vendidas. |
StopLossPoints |
Distância do stop protetor expressa em passos de preço do instrumento. |
TakeProfitPoints |
Distância do alvo de lucro expressa em passos de preço do instrumento. |
OrderVolume |
Volume base para entradas a mercado. A estratégia adiciona a posição aberta absoluta para reverter em uma transação. |
Notas de implementação
- As ordens são emitidas apenas após as velas terminarem, o que espelha o consultor especialista do MT5 que agia no início da próxima barra usando os dados da barra anterior.
- O indicador de canal é recriado dentro da estratégia sem armazenar coleções históricas, contando com variáveis de estado escalares.
- Stops e alvos de proteção são verificados manualmente para replicar o tratamento de ordens específico da plataforma do MT5.
- Garantir que o instrumento selecionado exponha um
PriceStep válido; caso contrário, as distâncias de stop-loss e take-profit serão ignoradas.
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Percentage Crossover Channel strategy converted from MetaTrader 5.
/// </summary>
public class PercentageCrossoverChannelStrategy : Strategy
{
public enum PercentageChannelPriceModes
{
Close,
Open,
High,
Low,
Median,
Typical,
Weighted,
Average
}
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _percent;
private readonly StrategyParam<PercentageChannelPriceModes> _priceMode;
private readonly StrategyParam<bool> _tradeOnMiddleCross;
private readonly StrategyParam<bool> _reverseSignals;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private readonly StrategyParam<decimal> _orderVolume;
// Cached indicator values for the previous two finished candles.
private decimal? _prevUpper;
private decimal? _prevMiddle;
private decimal? _prevLower;
private decimal? _prevPrevUpper;
private decimal? _prevPrevMiddle;
private decimal? _prevPrevLower;
// Stored price data for signal evaluation.
private decimal? _prevClose;
private decimal? _prevHigh;
private decimal? _prevLow;
private decimal? _prevPrevClose;
private decimal? _prevPrevHigh;
private decimal? _prevPrevLow;
// Internal state of the channel middle line recursion.
private decimal _lastMiddle;
private bool _hasIndicatorState;
// Protective levels that mimic MT5 stop loss and take profit requests.
private decimal? _stopPrice;
private decimal? _takePrice;
private decimal _entryPrice;
public PercentageCrossoverChannelStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for processing", "General");
_percent = Param(nameof(Percent), 1m)
.SetDisplay("Percent", "Channel width percent", "Channel")
.SetGreaterThanZero();
_priceMode = Param(nameof(PriceMode), PercentageChannelPriceModes.Close)
.SetDisplay("Applied Price", "Price source for channel calculations", "Channel");
_tradeOnMiddleCross = Param(nameof(TradeOnMiddleCross), false)
.SetDisplay("Trade Middle Cross", "Use middle line crossovers instead of band touches", "Signals");
_reverseSignals = Param(nameof(ReverseSignals), false)
.SetDisplay("Reverse Signals", "Invert long and short logic", "Signals");
_stopLossPoints = Param(nameof(StopLossPoints), 0)
.SetDisplay("Stop Loss (points)", "Protective stop distance in points", "Risk")
.SetNotNegative();
_takeProfitPoints = Param(nameof(TakeProfitPoints), 0)
.SetDisplay("Take Profit (points)", "Target profit distance in points", "Risk")
.SetNotNegative();
_orderVolume = Param(nameof(OrderVolume), 1m)
.SetDisplay("Order Volume", "Base volume for market entries", "Trading")
.SetGreaterThanZero();
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public decimal Percent
{
get => _percent.Value;
set => _percent.Value = value;
}
public PercentageChannelPriceModes PriceMode
{
get => _priceMode.Value;
set => _priceMode.Value = value;
}
public bool TradeOnMiddleCross
{
get => _tradeOnMiddleCross.Value;
set => _tradeOnMiddleCross.Value = value;
}
public bool ReverseSignals
{
get => _reverseSignals.Value;
set => _reverseSignals.Value = value;
}
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevUpper = null;
_prevMiddle = null;
_prevLower = null;
_prevPrevUpper = null;
_prevPrevMiddle = null;
_prevPrevLower = null;
_prevClose = null;
_prevHigh = null;
_prevLow = null;
_prevPrevClose = null;
_prevPrevHigh = null;
_prevPrevLow = null;
_lastMiddle = 0m;
_hasIndicatorState = false;
ResetProtection();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = OrderVolume;
// Subscribe to candle updates that will drive the high level logic.
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
// Work only with completed candles to stay consistent with the MT5 implementation.
if (candle.State != CandleStates.Finished)
return;
var exitTriggered = CheckProtection(candle);
if (!exitTriggered)
TryEnterPositions(candle);
UpdateChannelState(candle);
}
private void TryEnterPositions(ICandleMessage candle)
{
// Wait until the channel has valid values for two completed candles.
if (!_prevLower.HasValue || !_prevPrevLower.HasValue)
return;
if (!_prevClose.HasValue || !_prevPrevClose.HasValue || !_prevHigh.HasValue || !_prevPrevHigh.HasValue || !_prevLow.HasValue || !_prevPrevLow.HasValue)
return;
var openLong = false;
var openShort = false;
if (TradeOnMiddleCross)
{
// Evaluate crossovers of the price and the middle channel line.
var crossDown = _prevPrevClose.Value > _prevPrevMiddle.Value && _prevClose.Value < _prevMiddle.Value;
var crossUp = _prevPrevClose.Value < _prevPrevMiddle.Value && _prevClose.Value > _prevMiddle.Value;
if (!ReverseSignals)
{
if (crossDown)
openLong = true;
if (crossUp)
openShort = true;
}
else
{
if (crossDown)
openShort = true;
if (crossUp)
openLong = true;
}
}
else
{
// Default mode trades touches of the outer channel boundaries.
var touchLower = _prevPrevLow.Value > _prevPrevLower.Value && _prevLow.Value <= _prevLower.Value;
var touchUpper = _prevPrevHigh.Value < _prevPrevUpper.Value && _prevHigh.Value >= _prevUpper.Value;
if (!ReverseSignals)
{
if (touchLower)
openLong = true;
if (touchUpper)
openShort = true;
}
else
{
if (touchLower)
openShort = true;
if (touchUpper)
openLong = true;
}
}
if (openLong)
{
EnterLong(candle);
}
else if (openShort)
{
EnterShort(candle);
}
}
private void EnterLong(ICandleMessage candle)
{
// Combine base order volume with the size required to flatten shorts.
var volume = OrderVolume + (Position < 0 ? Math.Abs(Position) : 0m);
if (volume <= 0m)
return;
BuyMarket(volume);
_entryPrice = candle.OpenPrice;
_stopPrice = CalculateStopPrice(Sides.Buy, _entryPrice);
_takePrice = CalculateTakePrice(Sides.Buy, _entryPrice);
}
private void EnterShort(ICandleMessage candle)
{
// Combine base order volume with the size required to flatten longs.
var volume = OrderVolume + (Position > 0 ? Position : 0m);
if (volume <= 0m)
return;
SellMarket(volume);
_entryPrice = candle.OpenPrice;
_stopPrice = CalculateStopPrice(Sides.Sell, _entryPrice);
_takePrice = CalculateTakePrice(Sides.Sell, _entryPrice);
}
private bool CheckProtection(ICandleMessage candle)
{
// Emulate MT5 protective stop and take profit that were attached to market orders.
if (Position > 0)
{
if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
{
SellMarket(Math.Abs(Position));
ResetProtection();
return true;
}
if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
{
SellMarket(Math.Abs(Position));
ResetProtection();
return true;
}
}
else if (Position < 0)
{
if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetProtection();
return true;
}
if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetProtection();
return true;
}
}
else
{
ResetProtection();
}
return false;
}
private void UpdateChannelState(ICandleMessage candle)
{
// Recreate the Percentage Crossover Channel middle line recursion.
var percent = Percent <= 0m ? 0.001m : Percent;
var plusFactor = 1m + percent / 100m;
var minusFactor = 1m - percent / 100m;
var price = GetAppliedPrice(candle);
decimal currentMiddle;
if (!_hasIndicatorState)
{
currentMiddle = price;
_hasIndicatorState = true;
}
else
{
var lowerBound = price * minusFactor;
var upperBound = price * plusFactor;
var previousMiddle = _lastMiddle;
currentMiddle = previousMiddle;
if (lowerBound > previousMiddle)
currentMiddle = lowerBound;
else if (upperBound < previousMiddle)
currentMiddle = upperBound;
}
var currentUpper = currentMiddle * plusFactor;
var currentLower = currentMiddle * minusFactor;
if (_prevUpper.HasValue)
{
_prevPrevUpper = _prevUpper;
_prevPrevMiddle = _prevMiddle;
_prevPrevLower = _prevLower;
_prevPrevClose = _prevClose;
_prevPrevHigh = _prevHigh;
_prevPrevLow = _prevLow;
}
_prevUpper = currentUpper;
_prevMiddle = currentMiddle;
_prevLower = currentLower;
_prevClose = candle.ClosePrice;
_prevHigh = candle.HighPrice;
_prevLow = candle.LowPrice;
_lastMiddle = currentMiddle;
}
private decimal GetAppliedPrice(ICandleMessage candle)
{
// Convert the selected price mode into a candle value.
return PriceMode switch
{
PercentageChannelPriceModes.Open => candle.OpenPrice,
PercentageChannelPriceModes.High => candle.HighPrice,
PercentageChannelPriceModes.Low => candle.LowPrice,
PercentageChannelPriceModes.Median => (candle.HighPrice + candle.LowPrice) / 2m,
PercentageChannelPriceModes.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
PercentageChannelPriceModes.Weighted => (candle.HighPrice + candle.LowPrice + (2m * candle.ClosePrice)) / 4m,
PercentageChannelPriceModes.Average => (candle.OpenPrice + candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 4m,
_ => candle.ClosePrice,
};
}
private decimal? CalculateStopPrice(Sides side, decimal entryPrice)
{
if (StopLossPoints <= 0)
return null;
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
return null;
var offset = StopLossPoints * step;
return side == Sides.Buy ? entryPrice - offset : entryPrice + offset;
}
private decimal? CalculateTakePrice(Sides side, decimal entryPrice)
{
if (TakeProfitPoints <= 0)
return null;
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
return null;
var offset = TakeProfitPoints * step;
return side == Sides.Buy ? entryPrice + offset : entryPrice - offset;
}
private void ResetProtection()
{
_stopPrice = null;
_takePrice = null;
_entryPrice = 0m;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
class percentage_crossover_channel_strategy(Strategy):
def __init__(self):
super(percentage_crossover_channel_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Timeframe", "General")
self._percent = self.Param("Percent", 1.0).SetGreaterThanZero().SetDisplay("Percent", "Channel width percent", "Channel")
self._sl_points = self.Param("StopLossPoints", 0).SetNotNegative().SetDisplay("Stop Loss (points)", "Protective stop distance", "Risk")
self._tp_points = self.Param("TakeProfitPoints", 0).SetNotNegative().SetDisplay("Take Profit (points)", "Target profit distance", "Risk")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(percentage_crossover_channel_strategy, self).OnReseted()
self._last_middle = 0
self._has_state = False
self._prev_upper = None
self._prev_lower = None
self._prev_close = None
self._prev_high = None
self._prev_low = None
self._prev_prev_upper = None
self._prev_prev_lower = None
self._prev_prev_close = None
self._prev_prev_high = None
self._prev_prev_low = None
self._stop_price = None
self._take_price = None
self._entry_price = 0
def OnStarted2(self, time):
super(percentage_crossover_channel_strategy, self).OnStarted2(time)
self._last_middle = 0
self._has_state = False
self._prev_upper = None
self._prev_lower = None
self._prev_close = None
self._prev_high = None
self._prev_low = None
self._prev_prev_upper = None
self._prev_prev_lower = None
self._prev_prev_close = None
self._prev_prev_high = None
self._prev_prev_low = None
self._stop_price = None
self._take_price = None
self._entry_price = 0
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def _get_step(self):
if self.Security is not None and self.Security.PriceStep is not None and self.Security.PriceStep > 0:
return float(self.Security.PriceStep)
return 0.01
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
exit_triggered = self._check_protection(candle)
if not exit_triggered:
self._try_enter(candle)
self._update_channel(candle)
def _try_enter(self, candle):
if self._prev_lower is None or self._prev_prev_lower is None:
return
if self._prev_close is None or self._prev_prev_close is None:
return
touch_lower = self._prev_prev_low > self._prev_prev_lower and self._prev_low <= self._prev_lower
touch_upper = self._prev_prev_high < self._prev_prev_upper and self._prev_high >= self._prev_upper
if touch_lower:
self._enter_long(candle)
elif touch_upper:
self._enter_short(candle)
def _enter_long(self, candle):
volume = 1 + (Math.Abs(self.Position) if self.Position < 0 else 0)
self.BuyMarket(volume)
self._entry_price = float(candle.OpenPrice)
step = self._get_step()
self._stop_price = self._entry_price - self._sl_points.Value * step if self._sl_points.Value > 0 and step > 0 else None
self._take_price = self._entry_price + self._tp_points.Value * step if self._tp_points.Value > 0 and step > 0 else None
def _enter_short(self, candle):
volume = 1 + (self.Position if self.Position > 0 else 0)
self.SellMarket(volume)
self._entry_price = float(candle.OpenPrice)
step = self._get_step()
self._stop_price = self._entry_price + self._sl_points.Value * step if self._sl_points.Value > 0 and step > 0 else None
self._take_price = self._entry_price - self._tp_points.Value * step if self._tp_points.Value > 0 and step > 0 else None
def _check_protection(self, candle):
if self.Position > 0:
if self._stop_price is not None and candle.LowPrice <= self._stop_price:
self.SellMarket(Math.Abs(self.Position))
self._reset_protection()
return True
if self._take_price is not None and candle.HighPrice >= self._take_price:
self.SellMarket(Math.Abs(self.Position))
self._reset_protection()
return True
elif self.Position < 0:
if self._stop_price is not None and candle.HighPrice >= self._stop_price:
self.BuyMarket(Math.Abs(self.Position))
self._reset_protection()
return True
if self._take_price is not None and candle.LowPrice <= self._take_price:
self.BuyMarket(Math.Abs(self.Position))
self._reset_protection()
return True
else:
self._reset_protection()
return False
def _reset_protection(self):
self._stop_price = None
self._take_price = None
self._entry_price = 0
def _update_channel(self, candle):
pct = self._percent.Value if self._percent.Value > 0 else 0.001
plus_factor = 1.0 + pct / 100.0
minus_factor = 1.0 - pct / 100.0
price = float(candle.ClosePrice)
if not self._has_state:
current_middle = price
self._has_state = True
else:
lower_bound = price * minus_factor
upper_bound = price * plus_factor
current_middle = self._last_middle
if lower_bound > current_middle:
current_middle = lower_bound
elif upper_bound < current_middle:
current_middle = upper_bound
current_upper = current_middle * plus_factor
current_lower = current_middle * minus_factor
if self._prev_upper is not None:
self._prev_prev_upper = self._prev_upper
self._prev_prev_lower = self._prev_lower
self._prev_prev_close = self._prev_close
self._prev_prev_high = self._prev_high
self._prev_prev_low = self._prev_low
self._prev_upper = current_upper
self._prev_lower = current_lower
self._prev_close = float(candle.ClosePrice)
self._prev_high = float(candle.HighPrice)
self._prev_low = float(candle.LowPrice)
self._last_middle = current_middle
def CreateClone(self):
return percentage_crossover_channel_strategy()