View on GitHub

Firebird Channel Averaging Strategy

Overview

The Firebird Channel Averaging strategy replicates the MetaTrader 5 expert "Firebird v0.60" using StockSharp's high-level API. It trades a configurable moving-average channel and progressively averages into positions when price extends away from the channel. The approach is designed for mean-reversion forex trading where grid-style entries and pip-based risk controls are required.

Indicator Setup

  • A moving average (simple, exponential, smoothed or weighted) is calculated on the selected candle series. The price source (close, high, low, median, etc.) can be configured.
  • Upper and lower channel bands are derived by offsetting the moving average by a user-defined percentage.

Entry Logic

  1. Buy Conditions
    • Price of the chosen candle source closes below the lower band.
    • Either no position exists, or the new entry is at least Step (pips) away from the most recent fill when accounting for the Step Exponent growth.
    • The strategy enforces a cooldown of two candle intervals between entries.
  2. Sell Conditions
    • Price closes above the upper band.
    • Distance and cooldown checks identical to the long logic must be satisfied.

When a valid signal occurs the strategy submits a market order with the configured lot volume. Only one direction is maintained at a time—opposite signals will wait until the current inventory is closed by risk rules.

Position Management

  • Each entry is stored so the strategy can compute the average price of the open grid.
  • Stop-loss and take-profit levels are defined in pips. For a single position, the stop loss equals the entry price minus/plus Stop Loss (pips) and the take profit equals entry price plus/minus Take Profit (pips).
  • When multiple positions exist the stop-loss distance is divided by the number of entries, emulating the averaging behaviour of the original expert.
  • Profit targets remain fixed relative to the average price, while stop-loss exits are recalculated on every candle.
  • Trading can be optionally disabled on Fridays.

Parameters

Parameter Description
Volume Order size in lots for every averaged entry (default 0.1).
Stop Loss (pips) Protective stop distance in pips (default 50).
Take Profit (pips) Take-profit distance in pips (default 150).
MA Period Lookback length of the moving average (default 10).
MA Shift Forward shift in candles applied to the moving average output.
MA Type Moving-average calculation method: Simple, Exponential, Smoothed, or Weighted.
Price Source Candle price used for indicator calculations (close by default).
Channel % Percentage offset from the moving average used to form the bands (default 0.3%).
Trade Friday Enables or disables trading on Fridays.
Step (pips) Minimum pip distance between averaged orders (default 30).
Step Exponent Exponent that scales the step based on the number of open entries (0 keeps the step constant).
Candle Type Timeframe for the working candles.

Notes

  • The strategy assumes the instrument's PriceStep represents one pip. If unavailable it falls back to 0.0001.
  • Protective exits are executed with market orders rather than native stop/limit orders to stay consistent with the high-level API.
  • The averaging grid is capped by the cooldown logic and by the growing distance when a step exponent greater than zero is used.
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>
/// Firebird grid strategy that trades price deviations from a moving average channel
/// and averages into positions at configurable pip intervals.
/// </summary>
public class FirebirdChannelAveragingStrategy : Strategy
{
	/// <summary>
	/// Moving average calculation modes supported by the strategy.
	/// </summary>
	public enum MovingAverageTypes
	{
		/// <summary>
		/// Simple moving average.
		/// </summary>
		Simple,

		/// <summary>
		/// Exponential moving average.
		/// </summary>
		Exponential,

		/// <summary>
		/// Smoothed moving average.
		/// </summary>
		Smoothed,

		/// <summary>
		/// Weighted moving average.
		/// </summary>
		Weighted
	}

	public enum CandlePrices
	{
		Open,
		High,
		Low,
		Close,
		Median,
		Typical,
		Weighted
	}

	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _maShift;
	private readonly StrategyParam<MovingAverageTypes> _maType;
	private readonly StrategyParam<CandlePrices> _priceSource;
	private readonly StrategyParam<decimal> _pricePercent;
	private readonly StrategyParam<bool> _tradeOnFriday;
	private readonly StrategyParam<int> _stepPips;
	private readonly StrategyParam<decimal> _stepExponent;
	private readonly StrategyParam<DataType> _candleType;

	private DecimalLengthIndicator _ma;
	private readonly Queue<decimal> _maHistory = new();
	private readonly List<PositionEntry> _entries = new();
	private bool? _isLong;
	private DateTimeOffset? _lastEntryTime;


	/// <summary>
	/// Stop loss distance expressed in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Moving average lookback period.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Forward shift applied to the moving average in candles.
	/// </summary>
	public int MaShift
	{
		get => _maShift.Value;
		set => _maShift.Value = value;
	}

	/// <summary>
	/// Moving average calculation mode.
	/// </summary>
	public MovingAverageTypes MaType
	{
		get => _maType.Value;
		set => _maType.Value = value;
	}

	/// <summary>
	/// Candle price source used for the moving average and signal checks.
	/// </summary>
	public CandlePrices PriceSource
	{
		get => _priceSource.Value;
		set => _priceSource.Value = value;
	}

	/// <summary>
	/// Channel width as percentage offset from the moving average.
	/// </summary>
	public decimal PricePercent
	{
		get => _pricePercent.Value;
		set => _pricePercent.Value = value;
	}

	/// <summary>
	/// Enables trading on Fridays.
	/// </summary>
	public bool TradeOnFriday
	{
		get => _tradeOnFriday.Value;
		set => _tradeOnFriday.Value = value;
	}

	/// <summary>
	/// Minimum distance between averaged entries expressed in pips.
	/// </summary>
	public int StepPips
	{
		get => _stepPips.Value;
		set => _stepPips.Value = value;
	}

	/// <summary>
	/// Exponent controlling how the averaging step grows with position count.
	/// </summary>
	public decimal StepExponent
	{
		get => _stepExponent.Value;
		set => _stepExponent.Value = value;
	}

	/// <summary>
	/// Candle type used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initialize <see cref="FirebirdChannelAveragingStrategy"/>.
	/// </summary>
	public FirebirdChannelAveragingStrategy()
	{

		_stopLossPips = Param(nameof(StopLossPips), 50)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk")
			
			.SetOptimize(20, 150, 10);

		_takeProfitPips = Param(nameof(TakeProfitPips), 150)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk")
			
			.SetOptimize(50, 300, 10);

		_maPeriod = Param(nameof(MaPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "Moving average length", "Indicator")
			
			.SetOptimize(5, 30, 1);

		_maShift = Param(nameof(MaShift), 0)
			.SetNotNegative()
			.SetDisplay("MA Shift", "Forward shift for moving average", "Indicator");

		_maType = Param(nameof(MaType), MovingAverageTypes.Exponential)
			.SetDisplay("MA Type", "Moving average calculation mode", "Indicator");

		_priceSource = Param(nameof(PriceSource), CandlePrices.Close)
			.SetDisplay("Price Source", "Candle price used for signals", "Data");

		_pricePercent = Param(nameof(PricePercent), 0.3m)
			.SetGreaterThanZero()
			.SetDisplay("Channel %", "Channel width percentage", "Indicator")
			
			.SetOptimize(0.1m, 1m, 0.1m);

		_tradeOnFriday = Param(nameof(TradeOnFriday), true)
			.SetDisplay("Trade Friday", "Allow trading on Fridays", "Risk");

		_stepPips = Param(nameof(StepPips), 30)
			.SetGreaterThanZero()
			.SetDisplay("Step (pips)", "Distance between averaged entries", "Grid")
			
			.SetOptimize(10, 60, 5);

		_stepExponent = Param(nameof(StepExponent), 0m)
			.SetNotNegative()
			.SetDisplay("Step Exponent", "Power growth for step size", "Grid")
			
			.SetOptimize(0m, 2m, 0.5m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Working timeframe", "Data");
	}

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

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

		_entries.Clear();
		_maHistory.Clear();
		_isLong = null;
		_lastEntryTime = null;
	}

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

		_ma = CreateMovingAverage(MaType);
		_ma.Length = MaPeriod;

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

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _ma);
			DrawOwnTrades(area);
		}

	}

	private void ProcessCandle(ICandleMessage candle, decimal maValue)
	{
		// Only work with closed candles to avoid intra-bar noise.
		if (candle.State != CandleStates.Finished)
		{
			return;
		}

		// Ensure the moving average has enough historical data.
		if (_ma == null || !_ma.IsFormed)
		{
			return;
		}

		var shiftedValue = ApplyShift(maValue);
		if (shiftedValue is null)
		{
			return;
		}

		var price = GetCandlePrice(candle);
		var ma = shiftedValue.Value;

		var lowerBand = ma * (1m - PricePercent / 100m);
		var upperBand = ma * (1m + PricePercent / 100m);

		var allowEntry = TradeOnFriday || candle.OpenTime.DayOfWeek != DayOfWeek.Friday;

		if (!IsOnline)
		{
			allowEntry = false;
		}

		var pipSize = GetPipSize();
		var baseStep = StepPips * pipSize;
		if (baseStep <= 0)
		{
			baseStep = pipSize;
		}

		var entriesCount = _entries.Count;
		var stepMultiplier = StepExponent <= 0m
			? 1m
			: (decimal)Math.Pow(Math.Max(entriesCount, 1), (double)StepExponent);
		var currentStep = baseStep * stepMultiplier;
		if (currentStep <= 0)
		{
			currentStep = baseStep;
		}

		var canOpenByTime = true;
		var timeFrame = GetTimeFrame();
		var lastEntryTime = _lastEntryTime;
		if (entriesCount > 0 && lastEntryTime.HasValue && timeFrame != null)
		{
			var minDelay = timeFrame.Value + timeFrame.Value;
			canOpenByTime = candle.CloseTime - lastEntryTime.Value >= minDelay;
		}

		if (allowEntry)
		{
			TryOpenLong(candle, price, lowerBand, currentStep, canOpenByTime);
			TryOpenShort(candle, price, upperBand, currentStep, canOpenByTime);
		}

		ManageOpenPositions(candle, price, pipSize);
	}

	private void TryOpenLong(ICandleMessage candle, decimal price, decimal lowerBand, decimal currentStep, bool canOpenByTime)
	{
		if (price >= lowerBand)
		{
			return;
		}

		if (_entries.Count > 0 && _isLong != true)
		{
			return;
		}

		if (_entries.Count > 0 && !canOpenByTime)
		{
			return;
		}

		if (_entries.Count > 0)
		{
			var lastEntry = _entries[_entries.Count - 1];
			if (price > lastEntry.Price - currentStep)
			{
				return;
			}
		}

		BuyMarket(Volume);

		var entry = new PositionEntry
		{
			Price = price,
			Time = candle.CloseTime
		};

		_entries.Add(entry);
		_isLong = true;
		_lastEntryTime = entry.Time;
	}

	private void TryOpenShort(ICandleMessage candle, decimal price, decimal upperBand, decimal currentStep, bool canOpenByTime)
	{
		if (price <= upperBand)
		{
			return;
		}

		if (_entries.Count > 0 && _isLong != false)
		{
			return;
		}

		if (_entries.Count > 0 && !canOpenByTime)
		{
			return;
		}

		if (_entries.Count > 0)
		{
			var lastEntry = _entries[_entries.Count - 1];
			if (price < lastEntry.Price + currentStep)
			{
				return;
			}
		}

		SellMarket(Volume);

		var entry = new PositionEntry
		{
			Price = price,
			Time = candle.CloseTime
		};

		_entries.Add(entry);
		_isLong = false;
		_lastEntryTime = entry.Time;
	}

	private void ManageOpenPositions(ICandleMessage candle, decimal price, decimal pipSize)
	{
		var entriesCount = _entries.Count;
		if (entriesCount == 0)
		{
			return;
		}

		if (pipSize <= 0)
		{
			pipSize = 0.0001m;
		}

		var stopDistance = StopLossPips * pipSize;
		var takeDistance = TakeProfitPips * pipSize;

		decimal averagePrice = 0m;
		for (var i = 0; i < _entries.Count; i++)
		{
			averagePrice += _entries[i].Price;
		}
		if (entriesCount == 0)
		{
			return;
		}

		averagePrice /= entriesCount;

		if (_isLong == true)
		{
			var stopPrice = stopDistance > 0
			? averagePrice - (entriesCount > 1 ? stopDistance / entriesCount : stopDistance)
			: averagePrice;
			var takePrice = takeDistance > 0 ? averagePrice + takeDistance : decimal.MaxValue;

			if (price <= stopPrice)
			{
				CloseLongPositions();
				return;
			}

			if (price >= takePrice)
			{
				CloseLongPositions();
			}
		}
		else if (_isLong == false)
		{
			var stopPrice = stopDistance > 0
			? averagePrice + (entriesCount > 1 ? stopDistance / entriesCount : stopDistance)
			: averagePrice;
			var takePrice = takeDistance > 0 ? averagePrice - takeDistance : decimal.MinValue;

			if (price >= stopPrice)
			{
				CloseShortPositions();
				return;
			}

			if (price <= takePrice)
			{
				CloseShortPositions();
			}
		}
	}

	private void CloseLongPositions()
	{
		var volume = Position;
		if (volume > 0)
		{
			SellMarket(volume);
		}

		ResetEntries();
	}

	private void CloseShortPositions()
	{
		var volume = Math.Abs(Position);
		if (volume > 0)
		{
			BuyMarket(volume);
		}

		ResetEntries();
	}

	private void ResetEntries()
	{
		_entries.Clear();
		_isLong = null;
		_lastEntryTime = null;
	}

	private decimal? ApplyShift(decimal maValue)
	{
		var shift = MaShift;
		if (shift <= 0)
		{
			return maValue;
		}

		_maHistory.Enqueue(maValue);

		if (_maHistory.Count <= shift)
		{
			return null;
		}

		while (_maHistory.Count > shift + 1)
		{
			_maHistory.Dequeue();
		}

		return _maHistory.Peek();
	}

	private DecimalLengthIndicator CreateMovingAverage(MovingAverageTypes type)
	{
		return type switch
		{
			MovingAverageTypes.Simple => new SimpleMovingAverage(),
			MovingAverageTypes.Smoothed => new SmoothedMovingAverage(),
			MovingAverageTypes.Weighted => new WeightedMovingAverage(),
			_ => new ExponentialMovingAverage()
		};
	}

	private decimal GetCandlePrice(ICandleMessage candle)
	{
		return PriceSource switch
		{
			CandlePrices.Open => candle.OpenPrice,
			CandlePrices.High => candle.HighPrice,
			CandlePrices.Low => candle.LowPrice,
			CandlePrices.Close => candle.ClosePrice,
			CandlePrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			CandlePrices.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			CandlePrices.Weighted => (candle.HighPrice + candle.LowPrice + 2m * candle.ClosePrice) / 4m,
			_ => candle.ClosePrice
		};
	}

	private decimal GetPipSize()
	{
		var security = Security;
		if (security == null)
		{
			return 0.0001m;
		}

		if (security.PriceStep is > 0)
		{
			return security.PriceStep.Value;
		}

		return 0.0001m;
	}

	private TimeSpan? GetTimeFrame()
	{
		return CandleType.Arg is TimeSpan span ? span : null;
	}

	private sealed class PositionEntry
	{
		public decimal Price { get; set; }

		public DateTimeOffset Time { get; set; }
	}
}