Exp XMA Range Bands 策略
该策略将 MetaTrader 示例“Exp_XMA_Range_Bands” 转换为 StockSharp 高级 API。它利用 Keltner 通道(由移动平均算和平均真实波幅构成)来确定动态支撑和阻力。当价格在突破通道后重新回到通道内部时产生交易信号。
工作原理
- 构建 Keltner 通道:
- EMA 周期
MaLength - ATR 周期
RangeLength - ATR 乘数
Deviation
- EMA 周期
- 当上一样 K 线收盘价高于上一个上轨时,并消除所有空头。如果下一样 K 线收盘价回到通道内(收盘价 ≤ 当前上轨),则开多。
- 当上一样 K 线收盘价低于上一个下轨时,并消除所有多头。如果下一样 K 线收盘价回到通道内(收盘价 ≥ 当前下轨),则开空。
- 开仓后根据点数设置止损和止盈。
参数
MaLength– 通道中心的 EMA 周期。RangeLength– 用于通道宽度的 ATR 周期。Deviation– ATR 乘数。StopLoss– 止损点数(通过Security.PriceStep换算为价格)。TakeProfit– 止盈点数。CandleType– 用于计算的 K 线系列。
指标
- KeltnerChannels(EMA + ATR)
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>
/// Strategy converted from the MetaTrader example "Exp_XMA_Range_Bands".
/// Uses an EMA and ATR to build dynamic bands and trades when price re-enters the channel.
/// </summary>
public class ExpXmaRangeBandsStrategy : Strategy
{
private static readonly TimeSpan _signalCooldown = TimeSpan.FromHours(8);
private readonly StrategyParam<int> _maLength;
private readonly StrategyParam<int> _rangeLength;
private readonly StrategyParam<decimal> _deviation;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<decimal> _takeProfit;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevUpper;
private decimal _prevLower;
private decimal _prevClose;
private decimal _entryPrice;
private bool _isFirst = true;
private DateTime _lastSignalTime;
/// <summary>
/// EMA period for the channel center.
/// </summary>
public int MaLength
{
get => _maLength.Value;
set => _maLength.Value = value;
}
/// <summary>
/// ATR period used for channel width.
/// </summary>
public int RangeLength
{
get => _rangeLength.Value;
set => _rangeLength.Value = value;
}
/// <summary>
/// ATR multiplier for band width.
/// </summary>
public decimal Deviation
{
get => _deviation.Value;
set => _deviation.Value = value;
}
/// <summary>
/// Stop loss in points.
/// </summary>
public decimal StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
/// <summary>
/// Take profit in points.
/// </summary>
public decimal TakeProfit
{
get => _takeProfit.Value;
set => _takeProfit.Value = value;
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="ExpXmaRangeBandsStrategy"/>.
/// </summary>
public ExpXmaRangeBandsStrategy()
{
_maLength = Param(nameof(MaLength), 100)
.SetGreaterThanZero()
.SetDisplay("MA Length", "EMA period for channel center", "Indicator")
.SetOptimize(50, 200, 10);
_rangeLength = Param(nameof(RangeLength), 20)
.SetGreaterThanZero()
.SetDisplay("ATR Length", "ATR period for channel width", "Indicator")
.SetOptimize(10, 60, 5);
_deviation = Param(nameof(Deviation), 2m)
.SetGreaterThanZero()
.SetDisplay("Deviation", "ATR multiplier for channel width", "Indicator")
.SetOptimize(1m, 5m, 0.5m);
_stopLoss = Param(nameof(StopLoss), 1000m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss", "Stop loss in points", "Risk")
.SetOptimize(500m, 2000m, 500m);
_takeProfit = Param(nameof(TakeProfit), 2000m)
.SetGreaterThanZero()
.SetDisplay("Take Profit", "Take profit in points", "Risk")
.SetOptimize(1000m, 4000m, 500m);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevUpper = 0;
_prevLower = 0;
_prevClose = 0;
_entryPrice = 0;
_isFirst = true;
_lastSignalTime = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var keltner = new KeltnerChannels(
new KeltnerChannelMiddle { Length = MaLength },
new AverageTrueRange { Length = RangeLength },
new KeltnerChannelBand { Length = MaLength },
new KeltnerChannelBand { Length = MaLength })
{
Multiplier = Deviation,
};
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(keltner, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, keltner);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue keltnerValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (keltnerValue is not IKeltnerChannelsValue keltnerTyped ||
keltnerTyped.Upper is not decimal upper ||
keltnerTyped.Lower is not decimal lower)
return;
if (_isFirst)
{
_prevUpper = upper;
_prevLower = lower;
_prevClose = candle.ClosePrice;
_isFirst = false;
return;
}
if (_prevClose > _prevUpper)
{
if (candle.ClosePrice <= upper && Position <= 0 && CanTrade(candle))
{
var volume = Position < 0 ? Volume + Math.Abs(Position) : Volume;
BuyMarket(volume);
_entryPrice = candle.ClosePrice;
_lastSignalTime = candle.CloseTime;
}
}
else if (_prevClose < _prevLower)
{
if (candle.ClosePrice >= lower && Position >= 0 && CanTrade(candle))
{
var volume = Position > 0 ? Volume + Position : Volume;
SellMarket(volume);
_entryPrice = candle.ClosePrice;
_lastSignalTime = candle.CloseTime;
}
}
var step = Security.PriceStep ?? 1m;
var sl = step * StopLoss;
var tp = step * TakeProfit;
if (Position > 0)
{
if (candle.ClosePrice <= _entryPrice - sl || candle.ClosePrice >= _entryPrice + tp)
{
SellMarket(Position);
_entryPrice = 0;
}
}
else if (Position < 0)
{
if (candle.ClosePrice >= _entryPrice + sl || candle.ClosePrice <= _entryPrice - tp)
{
BuyMarket(Math.Abs(Position));
_entryPrice = 0;
}
}
_prevUpper = upper;
_prevLower = lower;
_prevClose = candle.ClosePrice;
}
private bool CanTrade(ICandleMessage candle)
=> _lastSignalTime == default || candle.CloseTime >= _lastSignalTime + _signalCooldown;
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math, DateTime
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import KeltnerChannels, KeltnerChannelMiddle, KeltnerChannelBand, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class exp_xma_range_bands_strategy(Strategy):
def __init__(self):
super(exp_xma_range_bands_strategy, self).__init__()
self._ma_length = self.Param("MaLength", 100) \
.SetDisplay("MA Length", "EMA period for channel center", "Indicator")
self._range_length = self.Param("RangeLength", 20) \
.SetDisplay("ATR Length", "ATR period for channel width", "Indicator")
self._deviation = self.Param("Deviation", 2.0) \
.SetDisplay("Deviation", "ATR multiplier for channel width", "Indicator")
self._stop_loss = self.Param("StopLoss", 1000.0) \
.SetDisplay("Stop Loss", "Stop loss in points", "Risk")
self._take_profit = self.Param("TakeProfit", 2000.0) \
.SetDisplay("Take Profit", "Take profit in points", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._prev_upper = 0.0
self._prev_lower = 0.0
self._prev_close = 0.0
self._entry_price = 0.0
self._is_first = True
self._last_signal_time = DateTime.MinValue
self._signal_cooldown = TimeSpan.FromHours(8)
@property
def MaLength(self):
return self._ma_length.Value
@MaLength.setter
def MaLength(self, value):
self._ma_length.Value = value
@property
def RangeLength(self):
return self._range_length.Value
@RangeLength.setter
def RangeLength(self, value):
self._range_length.Value = value
@property
def Deviation(self):
return self._deviation.Value
@Deviation.setter
def Deviation(self, value):
self._deviation.Value = value
@property
def StopLoss(self):
return self._stop_loss.Value
@StopLoss.setter
def StopLoss(self, value):
self._stop_loss.Value = value
@property
def TakeProfit(self):
return self._take_profit.Value
@TakeProfit.setter
def TakeProfit(self, value):
self._take_profit.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def _can_trade(self, candle):
return (self._last_signal_time == DateTime.MinValue
or candle.CloseTime >= self._last_signal_time + self._signal_cooldown)
def OnStarted2(self, time):
super(exp_xma_range_bands_strategy, self).OnStarted2(time)
mid = KeltnerChannelMiddle()
mid.Length = self.MaLength
atr = AverageTrueRange()
atr.Length = self.RangeLength
upper_band = KeltnerChannelBand()
upper_band.Length = self.MaLength
lower_band = KeltnerChannelBand()
lower_band.Length = self.MaLength
keltner = KeltnerChannels(mid, atr, upper_band, lower_band)
keltner.Multiplier = self.Deviation
subscription = self.SubscribeCandles(self.CandleType)
subscription \
.BindEx(keltner, self.ProcessCandle) \
.Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, keltner)
self.DrawOwnTrades(area)
def ProcessCandle(self, candle, keltner_value):
if candle.State != CandleStates.Finished:
return
upper_raw = keltner_value.Upper
lower_raw = keltner_value.Lower
if upper_raw is None or lower_raw is None:
return
upper = float(upper_raw)
lower = float(lower_raw)
close = float(candle.ClosePrice)
if self._is_first:
self._prev_upper = upper
self._prev_lower = lower
self._prev_close = close
self._is_first = False
return
if self._prev_close > self._prev_upper:
if close <= upper and self.Position <= 0 and self._can_trade(candle):
volume = self.Volume + abs(self.Position) if self.Position < 0 else self.Volume
self.BuyMarket(volume)
self._entry_price = close
self._last_signal_time = candle.CloseTime
elif self._prev_close < self._prev_lower:
if close >= lower and self.Position >= 0 and self._can_trade(candle):
volume = self.Volume + self.Position if self.Position > 0 else self.Volume
self.SellMarket(volume)
self._entry_price = close
self._last_signal_time = candle.CloseTime
step_raw = self.Security.PriceStep
step = float(step_raw) if step_raw is not None else 1.0
sl = step * float(self.StopLoss)
tp = step * float(self.TakeProfit)
if self.Position > 0:
if close <= self._entry_price - sl or close >= self._entry_price + tp:
self.SellMarket(self.Position)
self._entry_price = 0.0
elif self.Position < 0:
if close >= self._entry_price + sl or close <= self._entry_price - tp:
self.BuyMarket(abs(self.Position))
self._entry_price = 0.0
self._prev_upper = upper
self._prev_lower = lower
self._prev_close = close
def OnReseted(self):
super(exp_xma_range_bands_strategy, self).OnReseted()
self._prev_upper = 0.0
self._prev_lower = 0.0
self._prev_close = 0.0
self._entry_price = 0.0
self._is_first = True
self._last_signal_time = DateTime.MinValue
def CreateClone(self):
return exp_xma_range_bands_strategy()