Firebirdチャネル平均化戦略
概要
Firebirdチャネル平均化戦略は、StockSharpの高レベルAPIを使用してMetaTrader 5のエキスパート「Firebird v0.60」を再現します。設定可能な移動平均チャネルで取引し、価格がチャネルから離れた場合にポジションを段階的に平均化します。グリッドスタイルのエントリーとpipベースのリスクコントロールが必要な平均回帰フォレックス取引向けに設計されています。
インジケーター設定
- 選択したローソク足シリーズで移動平均(単純、指数、平滑化、または加重)が計算されます。価格ソース(終値、高値、安値、中央値など)は設定可能です。
- 上側および下側チャネルバンドは、移動平均をユーザー定義のパーセンテージでオフセットすることによって導出されます。
エントリーロジック
- 買い条件
- 選択したローソク足ソースの価格が下側バンドを下回って終了します。
- ポジションが存在しないか、
Step Exponent成長を考慮した場合に新しいエントリーが最新のフィルから少なくともStep (pips)離れています。 - 戦略はエントリー間に2つのローソク足間隔のクールダウンを強制します。
- 売り条件
- 価格が上側バンドを上回って終了します。
- ロングロジックと同一の距離およびクールダウンチェックが満たされる必要があります。
有効なシグナルが発生すると、戦略は設定されたロットボリュームで成行注文を送信します。一度に1つの方向のみが維持されます — 反対のシグナルは現在の在庫がリスクルールによって閉じられるまで待機します。
ポジション管理
- 各エントリーは、戦略がオープングリッドの平均価格を計算できるように保存されます。
- ストップロスとテイクプロフィットレベルはpipで定義されます。単一ポジションの場合、ストップロスはエントリー価格から
Stop Loss (pips)を引いた/加えた値に等しく、テイクプロフィットはエントリー価格にTake Profit (pips)を加えた/引いた値に等しくなります。 - 複数のポジションが存在する場合、ストップロス距離はエントリー数で割られ、オリジナルエキスパートの平均化動作をエミュレートします。
- 利益目標は平均価格に対して固定のままですが、ストップロスエグジットは各ローソク足で再計算されます。
- 取引は金曜日にオプションで無効にすることができます。
パラメータ
| パラメータ | 説明 |
|---|---|
Volume |
各平均化エントリーのロット単位の注文サイズ(デフォルト0.1)。 |
Stop Loss (pips) |
pip単位の保護ストップ距離(デフォルト50)。 |
Take Profit (pips) |
pip単位のテイクプロフィット距離(デフォルト150)。 |
MA Period |
移動平均のルックバック長(デフォルト10)。 |
MA Shift |
移動平均出力に適用される前向きシフト(ローソク足単位)。 |
MA Type |
移動平均計算メソッド:Simple、Exponential、Smoothed、またはWeighted。 |
Price Source |
インジケーター計算に使用されるローソク足価格(デフォルトは終値)。 |
Channel % |
バンドを形成するために使用される移動平均からのパーセンテージオフセット(デフォルト0.3%)。 |
Trade Friday |
金曜日の取引を有効または無効にします。 |
Step (pips) |
平均化注文間の最小pip距離(デフォルト30)。 |
Step Exponent |
オープンエントリー数に基づいてステップをスケーリングする指数(0でステップを一定に保ちます)。 |
Candle Type |
作業ローソク足の時間軸。 |
注意事項
- 戦略は銘柄の
PriceStepが1 pipを表すと想定しています。利用できない場合は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()