Firebird通道均值策略
概述
Firebird通道均值策略在StockSharp高阶API上重现MetaTrader 5专家顾问“Firebird v0.60”。策略基于可配置的均线通道进行交易,当价格远离通道时逐步加仓,实现典型的外汇均值回归与网格化风格,并通过以点数计的风控参数控制风险。
指标设置
- 计算一种可选的移动平均线(简单、指数、平滑或加权)作为基础线,价格来源(收盘价、高价、低价、中位价等)可配置。
- 将移动平均线按用户定义的百分比向上和向下偏移,得到上下通道边界。
入场逻辑
- 做多条件
- 选定的蜡烛价格源收于下轨之下。
- 当前无持仓,或新的买入价距离最近一次成交至少达到
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 |
用于指标计算的蜡烛价格(默认收盘价)。 |
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()