Ver no GitHub

Corrected Average Channel Strategy

Overview

The Corrected Average Channel Strategy is a C# port of the MetaTrader expert advisor e-CA-5. The system rebuilds the "Corrected Average" (CA) indicator every time a candle closes and opens a position when price crosses the corrected moving average by a configurable sigma offset. The converted implementation relies on StockSharp's high-level candle API, uses market orders, and manages protective exits (stop-loss, take-profit, trailing stop) internally to mirror the behaviour of the original Expert Advisor.

Corrected Average indicator

The CA filter combines a moving average with volatility feedback. The MQL version exposes three inputs: moving-average length, averaging method, and applied price. In the StockSharp port:

  1. The moving average type is selected via the MaTypeOption parameter (SMA, EMA, SMMA, LWMA) and the MaPeriod length.
  2. A StandardDeviation indicator with the same period measures current volatility.
  3. For each finished candle the corrected value is computed iteratively:
    • Let M_t be the MA value on the latest bar and CA_{t-1} the corrected value from the previous bar.
    • Compute v1 = StdDev_t^2 and v2 = (CA_{t-1} - M_t)^2.
    • If v2 <= 0 or v2 < v1, keep the correction factor k = 0. Otherwise set k = 1 - v1 / v2.
    • Update CA_t = CA_{t-1} + k * (M_t - CA_{t-1}).
    • The very first corrected value defaults to the moving average itself.

This feedback loop dampens the MA during quiet periods and allows rapid adjustments when price diverges beyond the current volatility estimate.

Trading logic

  1. The strategy subscribes to the configured candle type (CandleType) and waits until both the moving average and the standard deviation are fully formed.
  2. Once a candle finishes, the algorithm calculates the new corrected value and compares the previous candle close against the previous corrected level.
  3. Two sigma offsets, SigmaBuyPoints and SigmaSellPoints, are converted into price distances using the instrument's PriceStep.
  4. Entry rules use the previous candle close and the freshly computed corrected level:
    • Buy if the previous close was below the corrected average plus the buy sigma, and the current close finishes above that upper boundary.
    • Sell if the previous close was above the corrected average minus the sell sigma, and the current close finishes below that lower boundary.
  5. Only one net position is allowed. A new trade is submitted only when no exposure is present.

Because the StockSharp version operates on finished candles, the breakout confirmation happens once per bar instead of on every tick, providing deterministic behaviour suitable for backtesting and live automation with candle data.

Risk management

The port reproduces all three protective mechanics from the original Expert Advisor:

  • Fixed stop-loss: StopLossPoints multiplied by the price step defines the distance between the entry price and the protective stop. A triggered stop closes the entire position with a market order.
  • Fixed take-profit: TakeProfitPoints converts to a profit target distance. When price reaches the level during a candle, the position is closed with a market order.
  • Trailing stop: When TrailingPoints is greater than zero the strategy tracks unrealised profit and, once the price has advanced by at least that distance, stores a trailing level behind the latest close. The trailing stop only moves forward and honours TrailingStepPoints, which represents the minimum improvement before a new trailing level is accepted. Trailing levels are rounded with Security.ShrinkPrice so they align with the instrument's tick size.

All exits reset the internal risk state. When the next signal appears the stop, target, and trailing levels are recalculated from the new fill price, ensuring behaviour close to the MQL version that modifies the original order protections.

Parameters

Parameter Description
OrderVolume Quantity used for market entries. Must be positive.
TakeProfitPoints Profit target in price steps (0 disables the take-profit).
StopLossPoints Stop-loss distance in price steps (0 disables the stop-loss).
TrailingPoints Profit distance (in price steps) required before the trailing stop activates.
TrailingStepPoints Minimum extra distance that must be captured before moving the trailing stop again.
MaPeriod Period of both the moving average and the standard deviation.
MaTypeOption Moving average type: SMA, EMA, SMMA, or LWMA.
SigmaBuyPoints Sigma offset added above the corrected average before opening a long position.
SigmaSellPoints Sigma offset subtracted below the corrected average before opening a short position.
CandleType Candle series used for indicator calculations and signal evaluation.

All numeric parameters support optimisation through SetCanOptimize(true) so the strategy can be calibrated directly inside the StockSharp environment.

Usage notes

  • The default candle type is one hour. Adjust it to match the timeframe that was used when optimising the original MetaTrader strategy.
  • Security.PriceStep is used to translate all "points" inputs to actual price distances. Instruments without a configured step fall back to 1, preserving sensible behaviour for indexes or cryptocurrencies.
  • The strategy executes only on finished candles. If intrabar precision is required, lower the timeframe to the desired granularity.
  • Trailing stops are implemented with market orders when violated, mimicking the original EA that modified stop-loss prices. This approach avoids placing additional stop orders and keeps risk management contained within the strategy itself.
  • No Python version is provided for this conversion, per the task requirements.

Differences from the original EA

  • StockSharp's candle-based API replaces tick-level processing; all decisions are taken when a candle closes.
  • Order management is netted: opposing positions are not held simultaneously, matching the single-order logic of the MetaTrader version.
  • Protective stops and trailing exits are executed via market orders instead of modifying existing order tickets. This behaviour is equivalent on netting accounts while keeping the implementation consistent with other StockSharp strategies.

These adaptations preserve the trading idea of e-CA-5 while aligning the logic with StockSharp best practices and the high-level API conventions described in the repository guidelines.

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
	}
}