Ver no GitHub

Estratégia média corrigida do canal

Visão geral

A Estratégia de canal média corrigida é uma versão C# do MetaTrader consultor especialista e-CA-5. O sistema reconstrói o indicador "Média Corrigida" (CA) toda vez que uma vela fecha e abre uma posição quando o preço cruza a média móvel corrigida por um deslocamento sigma configurável. A implementação convertida depende da vela de alto nível API de API, usa ordens de mercado e gerencia saídas de proteção (stop-loss, take-profit, trailing stop) internamente para espelhar o comportamento do Expert Advisor original.

Corrected Average indicator

O filtro CA combina uma média móvel com feedback de volatilidade. A versão MQL expõe três entradas: comprimento da média móvel, método de cálculo da média e preço aplicado. Na porta StockSharp:

  1. O tipo de média móvel é selecionado através do parâmetro MaTypeOption (SMA, EMA, SMMA, LWMA) e do comprimento MaPeriod.
  2. Um indicador StandardDeviation com o mesmo período mede a volatilidade atual.
  3. Para cada vela finalizada, o valor corrigido é calculado iterativamente:
    • Seja M_t o valor MA na barra mais recente e CA_{t-1} o valor corrigido da barra anterior.
    • Calcule v1 = StdDev_t^2 e v2 = (CA_{t-1} - M_t)^2.
    • Se v2 <= 0 ou v2 < v1, mantenha o fator de correção k = 0. Caso contrário, defina k = 1 - v1 / v2.
    • Atualize CA_t = CA_{t-1} + k * (M_t - CA_{t-1}).
    • O primeiro valor corrigido é padronizado para a própria média móvel.

Este ciclo de feedback amortece o MA durante períodos de silêncio e permite ajustes rápidos quando o preço diverge além da estimativa de volatilidade atual.

Lógica de negociação

  1. A estratégia assina o tipo de vela configurado (CandleType) e espera até que a média móvel e o desvio padrão estejam totalmente formados.
  2. Assim que uma vela termina, o algoritmo calcula o novo valor corrigido e compara o fechamento da vela anterior com o nível corrigido anterior.
  3. Dois deslocamentos sigma, SigmaBuyPoints e SigmaSellPoints, são convertidos em distâncias de preço usando o PriceStep do instrumento.
  4. As regras de entrada usam o fechamento da vela anterior e o nível corrigido recentemente calculado:
    • Compre se o fechamento anterior estiver abaixo da média corrigida mais o sigma de compra, e o fechamento atual terminar acima desse limite superior.
    • Venda se o fechamento anterior estiver acima da média corrigida menos o sigma de venda e o fechamento atual terminar abaixo desse limite inferior.
  5. Apenas uma posição líquida é permitida. Uma nova negociação é submetida apenas quando não há exposição.

Como a versão StockSharp opera em velas finalizadas, a confirmação do rompimento ocorre uma vez por barra, em vez de a cada tick, fornecendo comportamento determinístico adequado para backtesting e automação ao vivo com dados de velas.

Gestão de risco

A porta reproduz todas as três mecânicas de proteção do Expert Advisor original:

  • Stop-loss fixo: StopLossPoints multiplicado pela etapa de preço define a distância entre o preço de entrada e o stop de proteção. Um stop acionado fecha toda a posição com uma ordem de mercado.
  • Take-profit fixo: TakeProfitPoints converte em uma distância alvo de lucro. Quando o preço atinge o nível durante uma vela, a posição é fechada com uma ordem de mercado.
  • Trailing stop: Quando TrailingPoints é maior que zero, a estratégia rastreia o lucro não realizado e, uma vez que o preço tenha avançado pelo menos essa distância, armazena um nível móvel atrás do último fechamento. O trailing stop apenas avança e honra TrailingStepPoints, o que representa a melhoria mínima antes que um novo nível móvel seja aceito. Os níveis finais são arredondados com Security.ShrinkPrice para que se alinhem com o tamanho do tick do instrumento.

Todas as saídas redefinem o estado de risco interno. Quando o próximo sinal aparece, os níveis stop, alvo e trailing são recalculados a partir do novo preço de preenchimento, garantindo um comportamento próximo à versão MQL que modifica as proteções da ordem original.

Parâmetros

Parâmetro Descrição
OrderVolume Quantidade utilizada para entradas no mercado. Deve ser positivo.
TakeProfitPoints Meta de lucro em etapas de preço (0 desativa o take-profit).
StopLossPoints Distância de stop-loss em etapas de preço (0 desativa o stop-loss).
TrailingPoints Distância de lucro (em etapas de preço) necessária antes da ativação do trailing stop.
TrailingStepPoints Distância extra mínima que deve ser capturada antes de mover o stop móvel novamente.
MaPeriod Período da média móvel e do desvio padrão.
MaTypeOption Tipo de média móvel: SMA, EMA, SMMA ou LWMA.
SigmaBuyPoints O deslocamento Sigma foi adicionado acima da média corrigida antes de abrir uma posição longa.
SigmaSellPoints Compensação Sigma subtraída abaixo da média corrigida antes de abrir uma posição curta.
CandleType Série de velas usada para cálculos de indicadores e avaliação de sinal.

Todos os parâmetros numéricos suportam otimização por meio de SetCanOptimize(true) para que a estratégia possa ser calibrada diretamente dentro do ambiente StockSharp.

Notas de uso

  • O tipo de vela padrão é uma hora. Ajuste-o para corresponder ao período usado ao otimizar a estratégia MetaTrader original.
  • Security.PriceStep é usado para traduzir todas as entradas de "pontos" em distâncias de preços reais. Instrumentos sem uma etapa configurada voltam para 1, preservando o comportamento sensato para índices ou criptomoedas.
  • The strategy executes only on finished candles. Se for necessária precisão intrabarra, reduza o período para a granularidade desejada.
  • Os trailing stops são implementados com ordens de mercado quando violados, imitando o EA original que modificou os preços do stop-loss. Esta abordagem evita a colocação de ordens stop adicionais e mantém a gestão de risco contida na própria estratégia.
  • Nenhuma versão do Python é fornecida para esta conversão, de acordo com os requisitos da tarefa.

Diferenças do original EA

  • O API baseado em vela de StockSharp substitui o processamento em nível de tick; todas as decisões são tomadas quando uma vela se fecha.
  • O gerenciamento de pedidos é compensado: posições opostas não são mantidas simultaneamente, correspondendo à lógica de pedido único da versão MetaTrader.
  • As paradas protetoras e as saídas finais são executadas por meio de ordens de mercado, em vez de modificar os tickets de ordem existentes. Este comportamento é equivalente em contas de compensação, mantendo a implementação consistente com outras estratégias StockSharp.

Essas adaptações preservam a ideia comercial de e-CA-5 enquanto alinham a lógica com as práticas recomendadas de StockSharp e as convenções de alto nível API descritas nas diretrizes do repositório.

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>
/// Port of the MetaTrader expert e-CA-5 that trades breakouts around the Corrected Average indicator.
/// The strategy subscribes to candles, rebuilds the indicator and places market orders when price crosses
/// the corrected moving average by the configured sigma offsets.
/// </summary>
public class CorrectedAverageChannelStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _trailingPoints;
	private readonly StrategyParam<int> _trailingStepPoints;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<MaTypes> _maType;
	private readonly StrategyParam<int> _sigmaBuyPoints;
	private readonly StrategyParam<int> _sigmaSellPoints;
	private readonly StrategyParam<DataType> _candleType;

	private DecimalLengthIndicator _ma;
	private StandardDeviation _std;

	private decimal _priceStep;
	private decimal _sigmaBuyOffset;
	private decimal _sigmaSellOffset;
	private decimal _stopLossDistance;
	private decimal _takeProfitDistance;
	private decimal _trailingDistance;
	private decimal _trailingStepDistance;

	private decimal? _previousCorrected;
	private decimal? _previousClose;

	private decimal? _entryPrice;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;
	private decimal? _longTrailingStop;
	private decimal? _shortTrailingStop;
	private decimal _previousPosition;
	private decimal? _lastTradePrice;
	private Sides? _lastTradeSide;

	/// <summary>
	/// Order size used for market entries.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in price steps.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in price steps.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Trailing stop trigger expressed in price steps.
	/// </summary>
	public int TrailingPoints
	{
		get => _trailingPoints.Value;
		set => _trailingPoints.Value = value;
	}

	/// <summary>
	/// Minimum increment required to advance the trailing stop in price steps.
	/// </summary>
	public int TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}

	/// <summary>
	/// Moving average period used by the Corrected Average filter.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Moving average type replicated from the MetaTrader input.
	/// </summary>
	public MaTypes MaTypesOption
	{
		get => _maType.Value;
		set => _maType.Value = value;
	}

	/// <summary>
	/// Buy-side sigma expressed in price steps.
	/// </summary>
	public int SigmaBuyPoints
	{
		get => _sigmaBuyPoints.Value;
		set => _sigmaBuyPoints.Value = value;
	}

	/// <summary>
	/// Sell-side sigma expressed in price steps.
	/// </summary>
	public int SigmaSellPoints
	{
		get => _sigmaSellPoints.Value;
		set => _sigmaSellPoints.Value = value;
	}

	/// <summary>
	/// Candle type used for indicator calculations and signal evaluation.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="CorrectedAverageChannelStrategy"/> class.
	/// </summary>
	public CorrectedAverageChannelStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Market order size used for entries", "Trading")
			;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 60)
			.SetNotNegative()
			.SetDisplay("Take Profit (points)", "Distance from entry to the profit target in price steps", "Risk")
			;

		_stopLossPoints = Param(nameof(StopLossPoints), 40)
			.SetNotNegative()
			.SetDisplay("Stop Loss (points)", "Distance from entry to the protective stop in price steps", "Risk")
			;

		_trailingPoints = Param(nameof(TrailingPoints), 0)
			.SetNotNegative()
			.SetDisplay("Trailing Trigger (points)", "Profit distance required before the trailing stop activates", "Risk")
			;

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 0)
			.SetNotNegative()
			.SetDisplay("Trailing Step (points)", "Minimum advance in price steps before the trailing stop moves", "Risk")
			;

		_maPeriod = Param(nameof(MaPeriod), 35)
			.SetRange(2, 500)
			.SetDisplay("MA Period", "Period of the moving average and standard deviation", "Indicator")
			;

		_maType = Param(nameof(MaTypesOption), MaTypes.Sma)
			.SetDisplay("MA Type", "Moving average type used inside the Corrected Average", "Indicator");

		_sigmaBuyPoints = Param(nameof(SigmaBuyPoints), 5)
			.SetNotNegative()
			.SetDisplay("Sigma BUY (points)", "Offset added above the corrected average before buying", "Signal")
			;

		_sigmaSellPoints = Param(nameof(SigmaSellPoints), 5)
			.SetNotNegative()
			.SetDisplay("Sigma SELL (points)", "Offset subtracted from the corrected average before selling", "Signal")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for calculations", "Data");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_ma = null;
		_std = null;
		_priceStep = 0m;
		_sigmaBuyOffset = 0m;
		_sigmaSellOffset = 0m;
		_stopLossDistance = 0m;
		_takeProfitDistance = 0m;
		_trailingDistance = 0m;
		_trailingStepDistance = 0m;
		_previousCorrected = null;
		_previousClose = null;
		_entryPrice = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_longTrailingStop = null;
		_shortTrailingStop = null;
		_previousPosition = 0m;
		_lastTradePrice = null;
		_lastTradeSide = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_ma = CreateMa(MaTypesOption, MaPeriod);
		_std = new StandardDeviation
		{
			Length = MaPeriod
		};

		_priceStep = Security?.PriceStep ?? 0m;
		if (_priceStep <= 0m)
		{
			_priceStep = 1m;
		}

		_sigmaBuyOffset = GetPriceOffset(SigmaBuyPoints);
		_sigmaSellOffset = GetPriceOffset(SigmaSellPoints);
		_stopLossDistance = GetPriceOffset(StopLossPoints);
		_takeProfitDistance = GetPriceOffset(TakeProfitPoints);
		_trailingDistance = GetPriceOffset(TrailingPoints);
		_trailingStepDistance = GetPriceOffset(TrailingStepPoints);

		Volume = OrderVolume;

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(_ma, _std, ProcessCandle).Start();
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (trade.Trade != null)
		{
			_lastTradePrice = trade.Trade.Price;
		}

		_lastTradeSide = trade.Order.Side;
	}

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		if (_previousPosition == 0m && Position != 0m)
		{
			var entryPrice = _lastTradePrice ?? _previousClose;
			if (entryPrice is decimal price)
			{
				if (Position > 0m && _lastTradeSide == Sides.Buy)
				{
					InitializeRiskState(price, true);
				}
				else if (Position < 0m && _lastTradeSide == Sides.Sell)
				{
					InitializeRiskState(price, false);
				}
			}
		}
		else if (Position == 0m && _previousPosition != 0m)
		{
			ResetRiskState();
		}

		_previousPosition = Position;
	}

	private void ProcessCandle(ICandleMessage candle, decimal maValue, decimal stdValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_ma is null || _std is null)
			return;

		if (!_ma.IsFormed || !_std.IsFormed)
		{
			_previousCorrected = maValue;
			_previousClose = candle.ClosePrice;
			return;
		}

		var previousCorrected = _previousCorrected;
		var previousClose = _previousClose;

		decimal corrected;

		if (previousCorrected is not decimal prevCorrected)
		{
			corrected = maValue;
		}
		else
		{
			var diff = prevCorrected - maValue;
			var v2 = diff * diff;
			var v1 = stdValue * stdValue;
			var k = (v2 <= 0m || v2 < v1) ? 0m : 1m - (v1 / v2);
			corrected = prevCorrected + k * (maValue - prevCorrected);
		}

		if (HandleTrailing(candle))
		{
			_previousCorrected = corrected;
			_previousClose = candle.ClosePrice;
			return;
		}

		if (HandleRiskExit(candle))
		{
			_previousCorrected = corrected;
			_previousClose = candle.ClosePrice;
			return;
		}

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			_previousCorrected = corrected;
			_previousClose = candle.ClosePrice;
			return;
		}

		if (Position == 0m && previousCorrected is decimal prevCorr && previousClose is decimal prevCls)
		{
			var buyThreshold = corrected + _sigmaBuyOffset;
			var sellThreshold = corrected - _sigmaSellOffset;

			var buySignal = prevCls < prevCorr + _sigmaBuyOffset && candle.ClosePrice >= buyThreshold;
			var sellSignal = prevCls > prevCorr - _sigmaSellOffset && candle.ClosePrice <= sellThreshold;

			if (buySignal)
			{
				BuyMarket();
			}
			else if (sellSignal)
			{
				SellMarket();
			}
		}

		_previousCorrected = corrected;
		_previousClose = candle.ClosePrice;
	}

	private bool HandleTrailing(ICandleMessage candle)
	{
		if (_trailingDistance <= 0m || _entryPrice is null)
			return false;

		var volume = Math.Abs(Position);
		if (volume <= 0m)
			return false;

		if (Position > 0m)
		{
			var moved = candle.ClosePrice - _entryPrice.Value;
			if (moved > _trailingDistance)
			{
				var candidate = candle.ClosePrice - _trailingDistance;
				if (_longTrailingStop is null || candidate - _longTrailingStop.Value >= _trailingStepDistance)
				{
					_longTrailingStop = Security?.ShrinkPrice(candidate) ?? candidate;
				}
			}

			if (_longTrailingStop is decimal trailing && candle.LowPrice <= trailing)
			{
				SellMarket(volume);
				ResetRiskState();
				return true;
			}
		}
		else if (Position < 0m)
		{
			var moved = _entryPrice.Value - candle.ClosePrice;
			if (moved > _trailingDistance)
			{
				var candidate = candle.ClosePrice + _trailingDistance;
				if (_shortTrailingStop is null || _shortTrailingStop.Value - candidate >= _trailingStepDistance)
				{
					_shortTrailingStop = Security?.ShrinkPrice(candidate) ?? candidate;
				}
			}

			if (_shortTrailingStop is decimal trailing && candle.HighPrice >= trailing)
			{
				BuyMarket(volume);
				ResetRiskState();
				return true;
			}
		}

		return false;
	}

	private bool HandleRiskExit(ICandleMessage candle)
	{
		var volume = Math.Abs(Position);
		if (volume <= 0m)
			return false;

		if (Position > 0m)
		{
			if (_stopLossPrice is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket(volume);
				ResetRiskState();
				return true;
			}

			if (_takeProfitPrice is decimal target && candle.HighPrice >= target)
			{
				SellMarket(volume);
				ResetRiskState();
				return true;
			}
		}
		else if (Position < 0m)
		{
			if (_stopLossPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket(volume);
				ResetRiskState();
				return true;
			}

			if (_takeProfitPrice is decimal target && candle.LowPrice <= target)
			{
				BuyMarket(volume);
				ResetRiskState();
				return true;
			}
		}

		return false;
	}

	private void InitializeRiskState(decimal entryPrice, bool isLong)
	{
		_entryPrice = entryPrice;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_longTrailingStop = null;
		_shortTrailingStop = null;

		if (_stopLossDistance > 0m)
		{
			var rawPrice = isLong ? entryPrice - _stopLossDistance : entryPrice + _stopLossDistance;
			_stopLossPrice = Security?.ShrinkPrice(rawPrice) ?? rawPrice;
		}

		if (_takeProfitDistance > 0m)
		{
			var rawPrice = isLong ? entryPrice + _takeProfitDistance : entryPrice - _takeProfitDistance;
			_takeProfitPrice = Security?.ShrinkPrice(rawPrice) ?? rawPrice;
		}
	}

	private void ResetRiskState()
	{
		_entryPrice = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_longTrailingStop = null;
		_shortTrailingStop = null;
	}

	private decimal GetPriceOffset(int points)
	{
		if (points <= 0 || _priceStep <= 0m)
			return 0m;

		return points * _priceStep;
	}

	private static DecimalLengthIndicator CreateMa(MaTypes type, int length)
	{
		return type switch
		{
			MaTypes.Sma => new SMA { Length = length },
			MaTypes.Ema => new EMA { Length = length },
			MaTypes.Smma => new SmoothedMovingAverage { Length = length },
			MaTypes.Lwma => new WeightedMovingAverage { Length = length },
			_ => throw new ArgumentOutOfRangeException(nameof(type))
		};
	}

	/// <summary>
	/// Supported moving average types.
	/// </summary>
	public enum MaTypes
	{
		Sma,
		Ema,
		Smma,
		Lwma
	}
}