Стратегия Firebird Channel Averaging
Обзор
Стратегия Firebird Channel Averaging переносит логику эксперта MetaTrader 5 «Firebird v0.60» на высокоуровневый API StockSharp. Торговля ведётся по настраиваемому каналу вокруг скользящей средней, а позиции усредняются по мере ухода цены от канала. Решение ориентировано на форекс-стратегии возврата к среднему с сеточным набором позиций и фиксированными рисками в пунктах.
Настройка индикаторов
- Рассчитывается выбранная скользящая средняя (простая, экспоненциальная, сглаженная или взвешенная). Источник цены свечи (close, high, low, median и т.д.) задаётся параметром.
- Верхняя и нижняя границы канала формируются путём умножения значения скользящей средней на заданный процент.
Логика входа
- Покупка
- Закрытие выбранной цены свечи ниже нижней границы канала.
- Либо позиция отсутствует, либо новая заявка удалена от последнего входа минимум на
Step (pips)с учётом роста шага черезStep Exponent. - Между открытиями должно пройти не менее двух свечных интервалов.
- Продажа
- Цена закрывается выше верхней границы канала.
- Проверки дистанции и таймаута полностью аналогичны длинной стороне.
При выполнении условий отправляется рыночная заявка указанным объёмом. Одновременно удерживается только одно направление: противоположные сигналы ждут закрытия текущей позиции по стопу или тейк-профиту.
Управление позицией
- Все входы сохраняются, что позволяет вычислять среднюю цену сетки.
- Стоп-лосс и тейк-профит задаются в пунктах. Для одиночной позиции стоп равен цене входа минус/плюс
Stop Loss (pips), тейк — цене входа плюс/минусTake Profit (pips). - При нескольких позициях расстояние до стопа делится на число входов, что повторяет механику усреднения оригинального эксперта.
- Тейк-профит фиксирован относительно средней цены, а стоп пересчитывается на каждой свече.
- Торговлю по пятницам можно отключить отдельным параметром.
Параметры
| Параметр | Описание |
|---|---|
Volume |
Объём каждой рыночной заявки (по умолчанию 0.1 лота). |
Stop Loss (pips) |
Дистанция стоп-лосса в пунктах (по умолчанию 50). |
Take Profit (pips) |
Дистанция тейк-профита в пунктах (по умолчанию 150). |
MA Period |
Период расчёта скользящей средней (по умолчанию 10). |
MA Shift |
Сдвиг значения скользящей средней вперёд на указанное число свечей. |
MA Type |
Тип скользящей: Simple, Exponential, Smoothed или Weighted. |
Price Source |
Цена свечи, используемая в индикаторе (по умолчанию close). |
Channel % |
Процентное смещение каналов относительно скользящей средней (по умолчанию 0.3%). |
Trade Friday |
Разрешение на торговлю в пятницу. |
Step (pips) |
Минимальный шаг между усредняющими входами (по умолчанию 30 пунктов). |
Step Exponent |
Показатель степени, увеличивающий шаг в зависимости от количества позиций (0 — фиксированный шаг). |
Candle Type |
Рабочий таймфрейм свечей. |
Дополнительные замечания
- Стратегия считает, что
PriceStepинструмента равен одному пункту; при отсутствии значения используется 0.0001. - Защитные выходы исполняются рыночными заявками, что соответствует возможностям высокоуровневого API.
- Холодный период и увеличивающийся шаг ограничивают размер сетки и предотвращают чрезмерное наращивание позиции.
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()