Rabbit M2 策略
概述
Rabbit M2 是 Peter Byrom 为 MetaTrader 4 编写的专家顾问,现已移植到 StockSharp。策略通过比较小时级 40/80 周期指数移动平均线来判断市场处于多头还是空头状态。在允许交易的方向上,程序等待 Williams %R 摆脱关键区间并由 CCI 指标确认后,以市价单入场。保护模块完全复制原始 EA:为每笔交易设置固定距离的 止损/止盈,并在价格突破相反方向的 Donchian 通道时强制平仓。资金管理逻辑会在获得超额利润后增加基础 手数,并将下一次加仓所需的利润目标加倍。
数据源与指标
- 主周期(默认 1 分钟 K 线)用于计算 Williams %R、CCI 以及 Donchian 通道。
- 小时周期提供 40 与 80 周期 EMA,用于切换交易方向并关闭反向头寸。
- Williams %R (50) 监控 -20/-80 阈值附近的动量突破。
- CCI (14) 确认市场处于超买或超卖状态。
- Donchian 通道 (100) 在突破前高/前低时触发保护性平仓。
- 固定止损和止盈 根据点数(默认 50)以及品种最小报价单位换算为价格距离,并针对三位或五位小数 的外汇品种进行调整。
交易规则
趋势控制
- 当小时级 EMA(40) 下穿 EMA(80) 时,立即平掉所有多单,只允许做空信号。
- 当 EMA(40) 上穿 EMA(80) 时,所有空单被平仓,只允许做多信号。
入场条件
- 做空 需满足:
- Williams %R 从 -20..0 区域跌入超卖区(<-20)。
- CCI 高于
CciSellLevel(默认 101)。 - 当前净空头仓位未超过
MaxTrades限制(每次加仓增加一个基础手数)。
- 做多 需满足:
- Williams %R 从 -100..-80 区域上升并突破 -80。
- CCI 低于
CciBuyLevel(默认 99)。 - 当前净多头仓位低于
MaxTrades限制。
StockSharp 账户使用净持仓模式,因此重复信号会在允许范围内逐步增加净仓量,而不会开启独立订单。
出场条件
- 每根完成的 K 线都会检查止损和止盈,一旦价格触及目标即平仓。
- 若多单收盘价跌破上一根 Donchian 下轨,或空单收盘价突破上一根上轨,也会强制离场。
- 小时级 EMA 发生反向交叉时,所有反向仓位立即平掉。
资金管理
- 基础手数由
InitialVolume(默认 0.01)初始化,并自动匹配交易品种的交易步长、最小与最大手数限制。 - 每当实现利润超过
BigWinTarget(默认 15 账户货币单位)时,基础手数增加VolumeIncrement(默认 0.01), 同时把利润阈值翻倍,模拟原始 EA 的递进加仓机制。 - 当仓位归零时,会清除内部记录的止损/止盈价格,避免残留数据影响后续交易。
参数
| 名称 | 默认值 | 说明 |
|---|---|---|
CciSellLevel |
101 | 触发做空所需的最小 CCI 值。 |
CciBuyLevel |
99 | 触发做多所需的最大 CCI 值。 |
CciPeriod |
14 | CCI 指标的计算周期。 |
DonchianPeriod |
100 | Donchian 通道的回溯长度。 |
MaxTrades |
1 | 允许的净持仓倍数上限。 |
BigWinTarget |
15 | 触发加仓的实现利润阈值。 |
VolumeIncrement |
0.01 | 达到目标后增加的基础手数。 |
WprPeriod |
50 | Williams %R 的计算周期。 |
FastEmaPeriod |
40 | 小时级快速 EMA 的周期。 |
SlowEmaPeriod |
80 | 小时级慢速 EMA 的周期。 |
TakeProfitPoints |
50 | 止盈距离(点)。 |
StopLossPoints |
50 | 止损距离(点)。 |
InitialVolume |
0.01 | 初始基础手数。 |
CandleType |
1 分钟 K 线 | 主要数据周期。 |
实现说明
- 止损和止盈在策略内部根据价格监控执行,而不是发送独立的保护单,以贴近原始 EA 的
OrderSend行为。 - 基础手数的调整依赖 StockSharp 返回的已实现盈亏信息,请确保适配器能够推送成交结果。
CalculatePriceOffset方法会根据品种的小数位数调整点值,模拟 MetaTrader 中的Point常量。
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;
using StockSharp.Algo;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Port of the "Rabbit M2" MetaTrader expert advisor by Peter Byrom.
/// The strategy combines hourly EMA regime detection with Williams %R momentum
/// and Donchian channel exits on the primary timeframe.
/// </summary>
public class RabbitM2RegimeSwingStrategy : Strategy
{
private static readonly DataType TrendCandleType = TimeSpan.FromHours(2).TimeFrame();
private readonly StrategyParam<int> _cciSellLevel;
private readonly StrategyParam<int> _cciBuyLevel;
private readonly StrategyParam<int> _cciPeriod;
private readonly StrategyParam<int> _donchianPeriod;
private readonly StrategyParam<int> _maxTrades;
private readonly StrategyParam<decimal> _bigWinTarget;
private readonly StrategyParam<decimal> _volumeIncrement;
private readonly StrategyParam<int> _wprPeriod;
private readonly StrategyParam<int> _fastEmaPeriod;
private readonly StrategyParam<int> _slowEmaPeriod;
private readonly StrategyParam<int> _takeProfitPoints;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<decimal> _initialVolume;
private readonly StrategyParam<DataType> _candleType;
private decimal _baseVolume;
private decimal _profitThreshold;
private decimal _lastRealizedPnL;
private decimal? _previousWpr;
private decimal? _previousUpperBand;
private decimal? _previousLowerBand;
private bool _longRegimeEnabled;
private bool _shortRegimeEnabled;
private decimal _stopDistance;
private decimal _takeDistance;
private decimal _activeStop;
private decimal _activeTake;
/// <summary>
/// Initializes a new instance of the <see cref="RabbitM2RegimeSwingStrategy"/> class.
/// </summary>
public RabbitM2RegimeSwingStrategy()
{
_cciSellLevel = Param(nameof(CciSellLevel), 101)
.SetDisplay("CCI Sell Level", "CCI threshold confirming a short signal", "CCI")
;
_cciBuyLevel = Param(nameof(CciBuyLevel), 99)
.SetDisplay("CCI Buy Level", "CCI threshold confirming a long signal", "CCI")
;
_cciPeriod = Param(nameof(CciPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("CCI Period", "Lookback window for the Commodity Channel Index", "CCI")
;
_donchianPeriod = Param(nameof(DonchianPeriod), 100)
.SetGreaterThanZero()
.SetDisplay("Donchian Period", "Length of the Donchian channel used for exits", "Donchian")
;
_maxTrades = Param(nameof(MaxTrades), 1)
.SetGreaterThanZero()
.SetDisplay("Max Trades", "Maximum number of base-volume units that can be open", "Risk")
;
_bigWinTarget = Param(nameof(BigWinTarget), 15m)
.SetGreaterThanZero()
.SetDisplay("Big Win Target", "Profit needed before the volume increases", "Money Management")
;
_volumeIncrement = Param(nameof(VolumeIncrement), 0.01m)
.SetGreaterThanZero()
.SetDisplay("Volume Increment", "How much to add to the base volume after a big win", "Money Management")
;
_wprPeriod = Param(nameof(WprPeriod), 50)
.SetGreaterThanZero()
.SetDisplay("Williams %R Period", "Length of the Williams %R oscillator", "Momentum")
;
_fastEmaPeriod = Param(nameof(FastEmaPeriod), 40)
.SetGreaterThanZero()
.SetDisplay("Fast EMA Period", "Fast EMA period on the hourly trend feed", "Trend")
;
_slowEmaPeriod = Param(nameof(SlowEmaPeriod), 80)
.SetGreaterThanZero()
.SetDisplay("Slow EMA Period", "Slow EMA period on the hourly trend feed", "Trend")
;
_takeProfitPoints = Param(nameof(TakeProfitPoints), 50)
.SetNotNegative()
.SetDisplay("Take Profit (points)", "Distance from entry price to the take profit", "Risk")
;
_stopLossPoints = Param(nameof(StopLossPoints), 50)
.SetNotNegative()
.SetDisplay("Stop Loss (points)", "Distance from entry price to the stop loss", "Risk")
;
_initialVolume = Param(nameof(InitialVolume), 0.01m)
.SetGreaterThanZero()
.SetDisplay("Initial Volume", "Starting base order size before scaling", "Money Management")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
.SetDisplay("Primary Candle Type", "Timeframe for Williams %R, CCI and Donchian calculations", "General");
}
/// <summary>
/// Minimum CCI value required to confirm a short setup.
/// </summary>
public int CciSellLevel
{
get => _cciSellLevel.Value;
set => _cciSellLevel.Value = value;
}
/// <summary>
/// Maximum CCI value required to confirm a long setup.
/// </summary>
public int CciBuyLevel
{
get => _cciBuyLevel.Value;
set => _cciBuyLevel.Value = value;
}
/// <summary>
/// Lookback used for the Commodity Channel Index.
/// </summary>
public int CciPeriod
{
get => _cciPeriod.Value;
set => _cciPeriod.Value = value;
}
/// <summary>
/// Donchian channel period that drives breakout exits.
/// </summary>
public int DonchianPeriod
{
get => _donchianPeriod.Value;
set => _donchianPeriod.Value = value;
}
/// <summary>
/// Maximum number of base-volume multiples allowed in the net position.
/// </summary>
public int MaxTrades
{
get => _maxTrades.Value;
set => _maxTrades.Value = value;
}
/// <summary>
/// Profit threshold that triggers a volume increase.
/// </summary>
public decimal BigWinTarget
{
get => _bigWinTarget.Value;
set => _bigWinTarget.Value = value;
}
/// <summary>
/// Volume increment added after a qualifying profit.
/// </summary>
public decimal VolumeIncrement
{
get => _volumeIncrement.Value;
set => _volumeIncrement.Value = value;
}
/// <summary>
/// Williams %R calculation period.
/// </summary>
public int WprPeriod
{
get => _wprPeriod.Value;
set => _wprPeriod.Value = value;
}
/// <summary>
/// Fast EMA period on the hourly trend feed.
/// </summary>
public int FastEmaPeriod
{
get => _fastEmaPeriod.Value;
set => _fastEmaPeriod.Value = value;
}
/// <summary>
/// Slow EMA period on the hourly trend feed.
/// </summary>
public int SlowEmaPeriod
{
get => _slowEmaPeriod.Value;
set => _slowEmaPeriod.Value = value;
}
/// <summary>
/// Take profit distance expressed in price steps.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Stop loss distance expressed in price steps.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Starting base volume used for each entry.
/// </summary>
public decimal InitialVolume
{
get => _initialVolume.Value;
set => _initialVolume.Value = value;
}
/// <summary>
/// Candle type used for Williams %R, CCI and Donchian calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
if (Security != null)
{
yield return (Security, CandleType);
yield return (Security, TrendCandleType);
}
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_baseVolume = 0m;
_profitThreshold = 0m;
_lastRealizedPnL = 0m;
_previousWpr = null;
_previousUpperBand = null;
_previousLowerBand = null;
_longRegimeEnabled = false;
_shortRegimeEnabled = false;
_stopDistance = 0m;
_takeDistance = 0m;
_activeStop = 0m;
_activeTake = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_baseVolume = InitialVolume;
_profitThreshold = BigWinTarget;
_lastRealizedPnL = PnL;
_previousWpr = null;
_previousUpperBand = null;
_previousLowerBand = null;
_longRegimeEnabled = false;
_shortRegimeEnabled = false;
_activeStop = 0m;
_activeTake = 0m;
_stopDistance = CalculatePriceOffset(StopLossPoints);
_takeDistance = CalculatePriceOffset(TakeProfitPoints);
NormalizeBaseVolume();
var wpr = new WilliamsR { Length = WprPeriod };
var cci = new CommodityChannelIndex { Length = CciPeriod };
var donchian = new DonchianChannels { Length = DonchianPeriod };
var emaFast = new EMA { Length = FastEmaPeriod };
var emaSlow = new EMA { Length = SlowEmaPeriod };
// The hourly subscription controls the trading regime and closes opposite positions when a cross happens.
var trendSubscription = SubscribeCandles(TrendCandleType);
trendSubscription
.Bind(emaFast, emaSlow, ProcessTrend)
.Start();
// The primary subscription provides momentum signals and breakout exits.
var primarySubscription = SubscribeCandles(CandleType);
primarySubscription
.BindEx(wpr, cci, donchian, ProcessPrimaryCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, primarySubscription);
DrawIndicator(area, emaFast);
DrawIndicator(area, emaSlow);
DrawOwnTrades(area);
}
}
private void ProcessTrend(ICandleMessage candle, decimal fastEma, decimal slowEma)
{
if (candle.State != CandleStates.Finished)
return;
// Fast EMA below slow EMA activates the short regime and forces longs to exit.
if (fastEma < slowEma)
{
_shortRegimeEnabled = true;
_longRegimeEnabled = false;
CloseLongPosition("Hourly trend turned bearish");
}
// Fast EMA above slow EMA activates the long regime and forces shorts to exit.
else if (fastEma > slowEma)
{
_longRegimeEnabled = true;
_shortRegimeEnabled = false;
CloseShortPosition("Hourly trend turned bullish");
}
}
private void ProcessPrimaryCandle(
ICandleMessage candle,
IIndicatorValue wprValue,
IIndicatorValue cciValue,
IIndicatorValue donchianValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!wprValue.IsFinal || !cciValue.IsFinal || !donchianValue.IsFinal)
return;
var donchian = (DonchianChannelsValue)donchianValue;
if (donchian.UpperBand is not decimal upperBand || donchian.LowerBand is not decimal lowerBand)
return;
// Always evaluate exit conditions before looking for new signals.
HandleActivePosition(candle);
var currentWpr = wprValue.ToDecimal();
if (currentWpr == 0m)
currentWpr = -1m;
var previousWpr = _previousWpr;
var currentCci = cciValue.ToDecimal();
if (!IsFormedAndOnlineAndAllowTrading())
{
_previousWpr = currentWpr;
_previousUpperBand = upperBand;
_previousLowerBand = lowerBand;
return;
}
if (previousWpr is null)
{
_previousWpr = currentWpr;
_previousUpperBand = upperBand;
_previousLowerBand = lowerBand;
return;
}
var wprLag = previousWpr.Value;
if (wprLag == 0m)
wprLag = -1m;
if (_shortRegimeEnabled)
TryOpenShort(candle, currentWpr, wprLag, currentCci);
if (_longRegimeEnabled)
TryOpenLong(candle, currentWpr, wprLag, currentCci);
_previousWpr = currentWpr;
_previousUpperBand = upperBand;
_previousLowerBand = lowerBand;
}
private void HandleActivePosition(ICandleMessage candle)
{
if (Position > 0m)
{
// Long positions exit on take profit, stop loss or a Donchian breakout against the trade.
if (_takeDistance > 0m && _activeTake > 0m && candle.HighPrice >= _activeTake)
{
CloseLongPosition("Take profit reached");
}
else if (_stopDistance > 0m && _activeStop > 0m && candle.LowPrice <= _activeStop)
{
CloseLongPosition("Stop loss reached");
}
else if (_previousLowerBand is decimal previousLower && candle.ClosePrice < previousLower)
{
CloseLongPosition("Closed below previous Donchian low");
}
}
else if (Position < 0m)
{
// Short positions exit using mirrored conditions.
if (_takeDistance > 0m && _activeTake > 0m && candle.LowPrice <= _activeTake)
{
CloseShortPosition("Take profit reached");
}
else if (_stopDistance > 0m && _activeStop > 0m && candle.HighPrice >= _activeStop)
{
CloseShortPosition("Stop loss reached");
}
else if (_previousUpperBand is decimal previousUpper && candle.ClosePrice > previousUpper)
{
CloseShortPosition("Closed above previous Donchian high");
}
}
}
private void TryOpenShort(ICandleMessage candle, decimal currentWpr, decimal previousWpr, decimal currentCci)
{
if (!(currentWpr < -20m && previousWpr > -20m && previousWpr < 0m && currentCci > CciSellLevel))
return;
if (_baseVolume <= 0m)
return;
// Net short exposure cannot exceed MaxTrades multiples of the base volume.
var netVolume = Math.Abs(Position);
var maxVolume = _baseVolume * MaxTrades;
if (maxVolume <= 0m || netVolume >= maxVolume)
return;
var volume = Math.Min(_baseVolume, maxVolume - netVolume);
volume = AlignVolume(volume);
if (volume <= 0m)
return;
SellMarket(volume);
_activeStop = _stopDistance > 0m ? candle.ClosePrice + _stopDistance : 0m;
_activeTake = _takeDistance > 0m ? candle.ClosePrice - _takeDistance : 0m;
}
private void TryOpenLong(ICandleMessage candle, decimal currentWpr, decimal previousWpr, decimal currentCci)
{
if (!(currentWpr > -80m && previousWpr < -80m && previousWpr < 0m && currentCci < CciBuyLevel))
return;
if (_baseVolume <= 0m)
return;
var netVolume = Math.Abs(Position);
var maxVolume = _baseVolume * MaxTrades;
if (maxVolume <= 0m || netVolume >= maxVolume)
return;
var volume = Math.Min(_baseVolume, maxVolume - netVolume);
volume = AlignVolume(volume);
if (volume <= 0m)
return;
BuyMarket(volume);
_activeStop = _stopDistance > 0m ? candle.ClosePrice - _stopDistance : 0m;
_activeTake = _takeDistance > 0m ? candle.ClosePrice + _takeDistance : 0m;
}
private void CloseLongPosition(string reason)
{
var volume = Math.Abs(Position);
if (volume <= 0m)
return;
SellMarket(volume);
_activeStop = 0m;
_activeTake = 0m;
LogInfo($"Closing long position: {reason}.");
}
private void CloseShortPosition(string reason)
{
var volume = Math.Abs(Position);
if (volume <= 0m)
return;
BuyMarket(volume);
_activeStop = 0m;
_activeTake = 0m;
LogInfo($"Closing short position: {reason}.");
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
var realizedChange = PnL - _lastRealizedPnL;
_lastRealizedPnL = PnL;
// Increase the base volume after trades with sufficiently large realized profits.
if (realizedChange > _profitThreshold && VolumeIncrement > 0m)
{
_baseVolume += VolumeIncrement;
NormalizeBaseVolume();
if (_profitThreshold > 0m)
_profitThreshold *= 2m;
}
if (Math.Abs(Position) == 0m)
{
_activeStop = 0m;
_activeTake = 0m;
}
}
/// <inheritdoc />
protected override void OnPositionReceived(Position position)
{
base.OnPositionReceived(position);
if (Position == 0m)
{
_activeStop = 0m;
_activeTake = 0m;
}
}
private void NormalizeBaseVolume()
{
if (_baseVolume <= 0m)
{
Volume = 0m;
return;
}
_baseVolume = AlignVolume(_baseVolume);
Volume = _baseVolume;
}
private decimal AlignVolume(decimal volume)
{
if (Security == null || volume <= 0m)
return volume;
var step = Security.VolumeStep;
if (step.HasValue && step.Value > 0m)
{
var steps = Math.Floor(volume / step.Value);
volume = steps > 0m ? steps * step.Value : step.Value;
}
var min = Security.MinVolume;
if (min.HasValue && min.Value > 0m && volume < min.Value)
volume = min.Value;
var max = Security.MaxVolume;
if (max.HasValue && max.Value > 0m && volume > max.Value)
volume = max.Value;
return volume;
}
private decimal CalculatePriceOffset(int points)
{
if (points <= 0)
return 0m;
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
step = 0.0001m;
var decimals = Security?.Decimals;
if (decimals == 3 || decimals == 5)
step *= 10m;
return points * step;
}
}
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
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import (
ExponentialMovingAverage,
WilliamsR,
CommodityChannelIndex,
DonchianChannels,
)
class rabbit_m2_regime_swing_strategy(Strategy):
def __init__(self):
super(rabbit_m2_regime_swing_strategy, self).__init__()
self._cci_sell_level = self.Param("CciSellLevel", 101) \
.SetDisplay("CCI Sell Level", "CCI threshold confirming a short signal", "CCI")
self._cci_buy_level = self.Param("CciBuyLevel", 99) \
.SetDisplay("CCI Buy Level", "CCI threshold confirming a long signal", "CCI")
self._cci_period = self.Param("CciPeriod", 14) \
.SetDisplay("CCI Period", "Lookback window for the Commodity Channel Index", "CCI")
self._donchian_period = self.Param("DonchianPeriod", 100) \
.SetDisplay("Donchian Period", "Length of the Donchian channel used for exits", "Donchian")
self._max_trades = self.Param("MaxTrades", 1) \
.SetDisplay("Max Trades", "Maximum number of base-volume units that can be open", "Risk")
self._big_win_target = self.Param("BigWinTarget", 15.0) \
.SetDisplay("Big Win Target", "Profit needed before the volume increases", "Money Management")
self._volume_increment = self.Param("VolumeIncrement", 0.01) \
.SetDisplay("Volume Increment", "How much to add to the base volume after a big win", "Money Management")
self._wpr_period = self.Param("WprPeriod", 50) \
.SetDisplay("Williams %R Period", "Length of the Williams %R oscillator", "Momentum")
self._fast_ema_period = self.Param("FastEmaPeriod", 40) \
.SetDisplay("Fast EMA Period", "Fast EMA period on the trend feed", "Trend")
self._slow_ema_period = self.Param("SlowEmaPeriod", 80) \
.SetDisplay("Slow EMA Period", "Slow EMA period on the trend feed", "Trend")
self._take_profit_points = self.Param("TakeProfitPoints", 50) \
.SetDisplay("Take Profit (points)", "Distance from entry price to the take profit", "Risk")
self._stop_loss_points = self.Param("StopLossPoints", 50) \
.SetDisplay("Stop Loss (points)", "Distance from entry price to the stop loss", "Risk")
self._initial_volume = self.Param("InitialVolume", 0.01) \
.SetDisplay("Initial Volume", "Starting base order size before scaling", "Money Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(2))) \
.SetDisplay("Primary Candle Type", "Timeframe for Williams %R, CCI and Donchian calculations", "General")
self._base_volume = 0.0
self._profit_threshold = 0.0
self._last_realized_pnl = 0.0
self._previous_wpr = None
self._long_regime_enabled = False
self._short_regime_enabled = False
self._stop_distance = 0.0
self._take_distance = 0.0
self._active_stop = 0.0
self._active_take = 0.0
self._previous_upper_band = None
self._previous_lower_band = None
@property
def CciSellLevel(self):
return self._cci_sell_level.Value
@property
def CciBuyLevel(self):
return self._cci_buy_level.Value
@property
def CciPeriod(self):
return self._cci_period.Value
@property
def DonchianPeriod(self):
return self._donchian_period.Value
@property
def MaxTrades(self):
return self._max_trades.Value
@property
def BigWinTarget(self):
return self._big_win_target.Value
@property
def VolumeIncrement(self):
return self._volume_increment.Value
@property
def WprPeriod(self):
return self._wpr_period.Value
@property
def FastEmaPeriod(self):
return self._fast_ema_period.Value
@property
def SlowEmaPeriod(self):
return self._slow_ema_period.Value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@property
def InitialVolume(self):
return self._initial_volume.Value
@property
def CandleType(self):
return self._candle_type.Value
def _calculate_price_offset(self, points):
pts = int(points)
if pts <= 0:
return 0.0
step = 0.0
if self.Security is not None and self.Security.PriceStep is not None:
step = float(self.Security.PriceStep)
if step <= 0:
step = 0.0001
decimals = None
if self.Security is not None and self.Security.Decimals is not None:
decimals = int(self.Security.Decimals)
if decimals == 3 or decimals == 5:
step *= 10.0
return pts * step
def _align_volume(self, volume):
if self.Security is None or volume <= 0:
return volume
vs = self.Security.VolumeStep
step = float(vs) if vs is not None and float(vs) > 0 else 0.0
if step > 0:
import math
steps = math.floor(volume / step)
volume = steps * step if steps > 0 else step
min_v = self.Security.MinVolume
if min_v is not None and float(min_v) > 0 and volume < float(min_v):
volume = float(min_v)
max_v = self.Security.MaxVolume
if max_v is not None and float(max_v) > 0 and volume > float(max_v):
volume = float(max_v)
return volume
def OnStarted2(self, time):
super(rabbit_m2_regime_swing_strategy, self).OnStarted2(time)
self._base_volume = float(self.InitialVolume)
self._profit_threshold = float(self.BigWinTarget)
self._last_realized_pnl = float(self.PnL)
self._stop_distance = self._calculate_price_offset(self.StopLossPoints)
self._take_distance = self._calculate_price_offset(self.TakeProfitPoints)
self._base_volume = self._align_volume(self._base_volume)
self.Volume = self._base_volume
self._wpr = WilliamsR()
self._wpr.Length = self.WprPeriod
self._cci = CommodityChannelIndex()
self._cci.Length = self.CciPeriod
self._donchian = DonchianChannels()
self._donchian.Length = self.DonchianPeriod
self._ema_fast = ExponentialMovingAverage()
self._ema_fast.Length = self.FastEmaPeriod
self._ema_slow = ExponentialMovingAverage()
self._ema_slow.Length = self.SlowEmaPeriod
self._previous_upper_band = None
self._previous_lower_band = None
trend_type = DataType.TimeFrame(TimeSpan.FromHours(2))
trend_sub = self.SubscribeCandles(trend_type)
trend_sub.Bind(self._ema_fast, self._ema_slow, self._process_trend).Start()
primary_sub = self.SubscribeCandles(self.CandleType)
primary_sub.BindEx(self._wpr, self._cci, self._donchian, self._process_primary).Start()
def _process_trend(self, candle, fast_ema, slow_ema):
if candle.State != CandleStates.Finished:
return
fast_ema = float(fast_ema)
slow_ema = float(slow_ema)
if fast_ema < slow_ema:
self._short_regime_enabled = True
self._long_regime_enabled = False
self._close_long_position()
elif fast_ema > slow_ema:
self._long_regime_enabled = True
self._short_regime_enabled = False
self._close_short_position()
def _process_primary(self, candle, wpr_value, cci_value, donchian_value):
if candle.State != CandleStates.Finished:
return
if not wpr_value.IsFinal or not cci_value.IsFinal or not donchian_value.IsFinal:
return
upper_band = donchian_value.UpperBand
lower_band = donchian_value.LowerBand
if upper_band is None or lower_band is None:
return
upper_band = float(upper_band)
lower_band = float(lower_band)
self._handle_active_position(candle)
current_wpr = float(wpr_value.Value) if hasattr(wpr_value, 'Value') else float(wpr_value)
if current_wpr == 0:
current_wpr = -1.0
current_cci = float(cci_value.Value) if hasattr(cci_value, 'Value') else float(cci_value)
previous_wpr = self._previous_wpr
if not self.IsFormedAndOnlineAndAllowTrading():
self._previous_wpr = current_wpr
self._previous_upper_band = upper_band
self._previous_lower_band = lower_band
return
if previous_wpr is None:
self._previous_wpr = current_wpr
self._previous_upper_band = upper_band
self._previous_lower_band = lower_band
return
wpr_lag = previous_wpr
if wpr_lag == 0:
wpr_lag = -1.0
if self._short_regime_enabled:
self._try_open_short(candle, current_wpr, wpr_lag, current_cci)
if self._long_regime_enabled:
self._try_open_long(candle, current_wpr, wpr_lag, current_cci)
self._previous_wpr = current_wpr
self._previous_upper_band = upper_band
self._previous_lower_band = lower_band
def _handle_active_position(self, candle):
if self.Position > 0:
if self._take_distance > 0 and self._active_take > 0 and float(candle.HighPrice) >= self._active_take:
self._close_long_position()
elif self._stop_distance > 0 and self._active_stop > 0 and float(candle.LowPrice) <= self._active_stop:
self._close_long_position()
elif self._previous_lower_band is not None and float(candle.ClosePrice) < self._previous_lower_band:
self._close_long_position()
elif self.Position < 0:
if self._take_distance > 0 and self._active_take > 0 and float(candle.LowPrice) <= self._active_take:
self._close_short_position()
elif self._stop_distance > 0 and self._active_stop > 0 and float(candle.HighPrice) >= self._active_stop:
self._close_short_position()
elif self._previous_upper_band is not None and float(candle.ClosePrice) > self._previous_upper_band:
self._close_short_position()
def _try_open_short(self, candle, current_wpr, previous_wpr, current_cci):
if not (current_wpr < -20 and previous_wpr > -20 and previous_wpr < 0 and current_cci > self.CciSellLevel):
return
if self._base_volume <= 0:
return
net_volume = Math.Abs(self.Position)
max_volume = self._base_volume * self.MaxTrades
if max_volume <= 0 or net_volume >= max_volume:
return
volume = min(self._base_volume, max_volume - net_volume)
volume = self._align_volume(volume)
if volume <= 0:
return
close_price = float(candle.ClosePrice)
self.SellMarket(volume)
self._active_stop = close_price + self._stop_distance if self._stop_distance > 0 else 0.0
self._active_take = close_price - self._take_distance if self._take_distance > 0 else 0.0
def _try_open_long(self, candle, current_wpr, previous_wpr, current_cci):
if not (current_wpr > -80 and previous_wpr < -80 and previous_wpr < 0 and current_cci < self.CciBuyLevel):
return
if self._base_volume <= 0:
return
net_volume = Math.Abs(self.Position)
max_volume = self._base_volume * self.MaxTrades
if max_volume <= 0 or net_volume >= max_volume:
return
volume = min(self._base_volume, max_volume - net_volume)
volume = self._align_volume(volume)
if volume <= 0:
return
close_price = float(candle.ClosePrice)
self.BuyMarket(volume)
self._active_stop = close_price - self._stop_distance if self._stop_distance > 0 else 0.0
self._active_take = close_price + self._take_distance if self._take_distance > 0 else 0.0
def _close_long_position(self):
volume = Math.Abs(self.Position)
if volume <= 0:
return
self.SellMarket(volume)
self._active_stop = 0.0
self._active_take = 0.0
def _close_short_position(self):
volume = Math.Abs(self.Position)
if volume <= 0:
return
self.BuyMarket(volume)
self._active_stop = 0.0
self._active_take = 0.0
def OnOwnTradeReceived(self, trade):
super(rabbit_m2_regime_swing_strategy, self).OnOwnTradeReceived(trade)
realized_change = float(self.PnL) - self._last_realized_pnl
self._last_realized_pnl = float(self.PnL)
if realized_change > self._profit_threshold and float(self.VolumeIncrement) > 0:
self._base_volume += float(self.VolumeIncrement)
self._base_volume = self._align_volume(self._base_volume)
self.Volume = self._base_volume
if self._profit_threshold > 0:
self._profit_threshold *= 2.0
if Math.Abs(self.Position) == 0:
self._active_stop = 0.0
self._active_take = 0.0
def OnReseted(self):
super(rabbit_m2_regime_swing_strategy, self).OnReseted()
self._base_volume = 0.0
self._profit_threshold = 0.0
self._last_realized_pnl = 0.0
self._previous_wpr = None
self._long_regime_enabled = False
self._short_regime_enabled = False
self._stop_distance = 0.0
self._take_distance = 0.0
self._active_stop = 0.0
self._active_take = 0.0
self._previous_upper_band = None
self._previous_lower_band = None
def CreateClone(self):
return rabbit_m2_regime_swing_strategy()