Открыть на GitHub

Стратегия «Corrected Average Channel»

Общее описание

Corrected Average Channel — порт MetaTrader-советника e-CA-5, переписанный на C# для платформы StockSharp. Стратегия пересчитывает индикатор «Corrected Average» (CA) после закрытия каждой свечи и открывает сделку, когда цена пересекает скорректированную среднюю на заданное число пунктов (сигму). В новой реализации используются высокоуровневые свечи StockSharp, рыночные заявки и встроенное управление риском (стоп-лосс, тейк-профит и трейлинг-стоп), что сохраняет торговую логику оригинального советника.

Индикатор Corrected Average

CA сочетает сглаживание и волатильность. В MetaTrader задаются три параметра: период MA, тип MA и применяемая цена. Перенос в StockSharp реализован следующим образом:

  1. Тип MA выбирается через MaTypeOption (SMA, EMA, SMMA, LWMA), период — MaPeriod.
  2. Параллельно рассчитывается StandardDeviation с тем же периодом, чтобы измерить текущую волатильность.
  3. Для каждой завершённой свечи corrected average вычисляется рекуррентно:
    • Пусть M_t — значение MA на текущей свече, CA_{t-1} — скорректированная средняя на предыдущей свече.
    • v1 = StdDev_t^2, v2 = (CA_{t-1} - M_t)^2.
    • Если v2 <= 0 или v2 < v1, коэффициент коррекции k = 0, иначе k = 1 - v1 / v2.
    • Новое значение CA_t = CA_{t-1} + k * (M_t - CA_{t-1}).
    • На первой свече corrected average совпадает с MA.

Такой механизм подавляет шум в спокойные периоды и ускоряет реакцию индикатора, когда цена резко отходит от средней.

Логика торговли

  1. Стратегия подписывается на свечи типа CandleType и ждёт формирования MA и StdDev.
  2. После закрытия свечи вычисляется новый corrected average и сравнивается предыдущий close с предыдущим CA.
  3. Смещения SigmaBuyPoints и SigmaSellPoints переводятся в цены через Security.PriceStep.
  4. Сигналы формируются по двум условиям:
    • Покупка — если предыдущая свеча закрылась ниже CA + sigma_buy, а текущая закрывается выше этого уровня.
    • Продажа — если предыдущая свеча закрылась выше CA - sigma_sell, а текущая закрывается ниже этого уровня.
  5. В рынке допускается только одна позиция. Новая заявка отправляется, когда текущая позиция равна нулю.

Работа по закрытым свечам делает результаты воспроизводимыми и упрощает тестирование.

Управление рисками

Перенос сохраняет все защитные механизмы EA:

  • Фиксированный стоп-лосс (StopLossPoints): расстояние от цены входа до стопа задаётся в шагах цены и реализуется рыночным закрытием при достижении уровня.
  • Фиксированный тейк-профит (TakeProfitPoints): аналогично задаётся в пунктах и закрывает позицию по рынку при достижении цели.
  • Трейлинг-стоп (TrailingPoints и TrailingStepPoints): когда прибыль превышает заданное расстояние, стратегия фиксирует новый уровень стопа за текущей ценой. Стоп двигается только в сторону профита и увеличивается не менее чем на TrailingStepPoints. Уровни приводятся к допустимым значениям через Security.ShrinkPrice.

После любого защитного выхода внутренние переменные сбрасываются. Новая сделка получает свежие значения стопов и трейлинга, что соответствует логике изменения ордеров в MetaTrader.

Параметры

Параметр Описание
OrderVolume Объём рыночных заявок.
TakeProfitPoints Дистанция до тейк-профита в шагах цены (0 — отключить).
StopLossPoints Дистанция до стоп-лосса в шагах цены (0 — отключить).
TrailingPoints Прибыль в шагах цены, необходимая для активации трейлинг-стопа.
TrailingStepPoints Минимальное дополнительное смещение трейлинг-стопа.
MaPeriod Период MA и StdDev.
MaTypeOption Тип MA: SMA, EMA, SMMA, LWMA.
SigmaBuyPoints Смещение выше corrected average для входа в лонг.
SigmaSellPoints Смещение ниже corrected average для входа в шорт.
CandleType Тип свечей, используемых для расчётов и сигналов.

Каждый числовой параметр помечен SetCanOptimize(true), что облегчает оптимизацию в StockSharp.

Рекомендации по применению

  • По умолчанию используется часовой таймфрейм. При необходимости выберите другой интервал, соответствующий настройкам исходного советника.
  • Все параметры в «пунктах» автоматически переводятся в цены через PriceStep. Если тик не задан, принимается значение 1.
  • Стратегия принимает решения только на закрытии свечей. Для внутридневного анализа используйте более короткие интервалы.
  • Трейлинг реализован через рыночные заявки при пробитии уровня, что повторяет модификацию стопов в MetaTrader и не требует дополнительных отложенных ордеров.
  • Согласно заданию, Python-реализация не создавалась.

Отличия от оригинала

  • В StockSharp стратегия работает со свечами, а не с тиковой лентой, поэтому сигналы возникают один раз на свечу.
  • Позиции ведутся в неттинговом режиме без встречных сделок, как и в MQL-версии.
  • Стопы и тейк-профиты закрываются по рынку вместо изменения параметров существующих ордеров — такой подход удобен в инфраструктуре StockSharp и даёт эквивалентный результат на неттинговых счетах.

Эти изменения сохраняют идею e-CA-5, одновременно адаптируя стратегию под архитектуру StockSharp и рекомендации репозитория.

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