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; }
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
import math
from collections import deque
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class firebird_channel_averaging_strategy(Strategy):
"""
Firebird Channel Averaging: grid strategy trading price deviations
from a moving average channel. Averages into positions at configurable
pip intervals with SL/TP management.
"""
def __init__(self):
super(firebird_channel_averaging_strategy, self).__init__()
self._stop_loss_pips = self.Param("StopLossPips", 50) \
.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 150) \
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk")
self._ma_period = self.Param("MaPeriod", 10) \
.SetDisplay("MA Period", "Moving average length", "Indicator")
self._ma_shift = self.Param("MaShift", 0) \
.SetDisplay("MA Shift", "Forward shift for moving average", "Indicator")
self._price_percent = self.Param("PricePercent", 0.3) \
.SetDisplay("Channel %", "Channel width percentage", "Indicator")
self._step_pips = self.Param("StepPips", 30) \
.SetDisplay("Step (pips)", "Distance between averaged entries", "Grid")
self._step_exponent = self.Param("StepExponent", 0.0) \
.SetDisplay("Step Exponent", "Power growth for step size", "Grid")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Working timeframe", "Data")
self._entries = []
self._ma_history = deque()
self._is_long = None
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(firebird_channel_averaging_strategy, self).OnReseted()
self._entries = []
self._ma_history = deque()
self._is_long = None
def OnStarted2(self, time):
super(firebird_channel_averaging_strategy, self).OnStarted2(time)
ma = ExponentialMovingAverage()
ma.Length = self._ma_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(ma, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ma)
self.DrawOwnTrades(area)
def _process_candle(self, candle, ma_value):
if candle.State != CandleStates.Finished:
return
ma_val = float(ma_value)
shifted = self._apply_shift(ma_val)
if shifted is None:
return
price = float(candle.ClosePrice)
ma = shifted
lower_band = ma * (1.0 - self._price_percent.Value / 100.0)
upper_band = ma * (1.0 + self._price_percent.Value / 100.0)
pip_size = self._get_pip_size()
base_step = self._step_pips.Value * pip_size
if base_step <= 0:
base_step = pip_size
entries_count = len(self._entries)
exp = self._step_exponent.Value
if exp <= 0:
step_mult = 1.0
else:
step_mult = math.pow(max(entries_count, 1), exp)
current_step = base_step * step_mult
if current_step <= 0:
current_step = base_step
# Try open long
if price < lower_band:
if entries_count == 0 or self._is_long == True:
if entries_count == 0 or price <= self._entries[-1][0] - current_step:
self.BuyMarket()
self._entries.append((price, candle.CloseTime))
self._is_long = True
# Try open short
if price > upper_band:
if entries_count == 0 or self._is_long == False:
if entries_count == 0 or price >= self._entries[-1][0] + current_step:
self.SellMarket()
self._entries.append((price, candle.CloseTime))
self._is_long = False
# Manage open positions
self._manage_positions(price, pip_size)
def _manage_positions(self, price, pip_size):
entries_count = len(self._entries)
if entries_count == 0:
return
if pip_size <= 0:
pip_size = 0.0001
stop_distance = self._stop_loss_pips.Value * pip_size
take_distance = self._take_profit_pips.Value * pip_size
avg_price = sum(e[0] for e in self._entries) / entries_count
if self._is_long == True:
stop_price = avg_price - (stop_distance / entries_count if entries_count > 1 else stop_distance) if stop_distance > 0 else avg_price
take_price = avg_price + take_distance if take_distance > 0 else float('inf')
if price <= stop_price:
self.SellMarket()
self._reset_entries()
return
if price >= take_price:
self.SellMarket()
self._reset_entries()
elif self._is_long == False:
stop_price = avg_price + (stop_distance / entries_count if entries_count > 1 else stop_distance) if stop_distance > 0 else avg_price
take_price = avg_price - take_distance if take_distance > 0 else float('-inf')
if price >= stop_price:
self.BuyMarket()
self._reset_entries()
return
if price <= take_price:
self.BuyMarket()
self._reset_entries()
def _reset_entries(self):
self._entries = []
self._is_long = None
def _apply_shift(self, ma_value):
shift = self._ma_shift.Value
if shift <= 0:
return ma_value
self._ma_history.append(ma_value)
if len(self._ma_history) <= shift:
return None
while len(self._ma_history) > shift + 1:
self._ma_history.popleft()
return self._ma_history[0]
def _get_pip_size(self):
if self.Security is not None and self.Security.PriceStep is not None:
ps = float(self.Security.PriceStep)
if ps > 0:
return ps
return 0.0001
def CreateClone(self):
return firebird_channel_averaging_strategy()