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>
/// Port of the Master MM Droid strategy with modular money management blocks.
/// Uses RSI crossover signals with pyramiding, daily gap detection, and
/// box/weekly breakout modules - all implemented via candle-based checks.
/// </summary>
public class MasterMmDroidStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<decimal> _rsiLowerLevel;
private readonly StrategyParam<decimal> _rsiUpperLevel;
private readonly StrategyParam<int> _rsiMaxEntries;
private readonly StrategyParam<decimal> _rsiPyramidSteps;
private readonly StrategyParam<decimal> _stopLossSteps;
private readonly StrategyParam<decimal> _trailingSteps;
private readonly StrategyParam<int> _boxLookback;
private readonly StrategyParam<decimal> _boxEntrySteps;
private RelativeStrengthIndex _rsi = null!;
private decimal _previousRsi;
private bool _hasPreviousRsi;
private decimal? _lastEntryPrice;
private int _entryCount;
private decimal? _activeStopPrice;
private decimal _bestPrice;
private decimal _boxHigh;
private decimal _boxLow;
private int _boxBarsCount;
/// <summary>
/// Candle type used for processing.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// RSI period.
/// </summary>
public int RsiPeriod
{
get => _rsiPeriod.Value;
set => _rsiPeriod.Value = value;
}
/// <summary>
/// RSI oversold level.
/// </summary>
public decimal RsiLowerLevel
{
get => _rsiLowerLevel.Value;
set => _rsiLowerLevel.Value = value;
}
/// <summary>
/// RSI overbought level.
/// </summary>
public decimal RsiUpperLevel
{
get => _rsiUpperLevel.Value;
set => _rsiUpperLevel.Value = value;
}
/// <summary>
/// Maximum pyramiding entries.
/// </summary>
public int RsiMaxEntries
{
get => _rsiMaxEntries.Value;
set => _rsiMaxEntries.Value = value;
}
/// <summary>
/// Price steps between pyramid entries.
/// </summary>
public decimal RsiPyramidSteps
{
get => _rsiPyramidSteps.Value;
set => _rsiPyramidSteps.Value = value;
}
/// <summary>
/// Stop-loss distance in price steps.
/// </summary>
public decimal StopLossSteps
{
get => _stopLossSteps.Value;
set => _stopLossSteps.Value = value;
}
/// <summary>
/// Trailing stop distance in price steps.
/// </summary>
public decimal TrailingSteps
{
get => _trailingSteps.Value;
set => _trailingSteps.Value = value;
}
/// <summary>
/// Number of candles for box high/low calculation.
/// </summary>
public int BoxLookback
{
get => _boxLookback.Value;
set => _boxLookback.Value = value;
}
/// <summary>
/// Breakout distance above/below the box in price steps.
/// </summary>
public decimal BoxEntrySteps
{
get => _boxEntrySteps.Value;
set => _boxEntrySteps.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="MasterMmDroidStrategy"/>.
/// </summary>
public MasterMmDroidStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe", "General");
_rsiPeriod = Param(nameof(RsiPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("RSI Period", "RSI calculation period", "RSI")
.SetOptimize(7, 21, 7);
_rsiLowerLevel = Param(nameof(RsiLowerLevel), 25m)
.SetDisplay("RSI Oversold", "RSI oversold threshold", "RSI");
_rsiUpperLevel = Param(nameof(RsiUpperLevel), 75m)
.SetDisplay("RSI Overbought", "RSI overbought threshold", "RSI");
_rsiMaxEntries = Param(nameof(RsiMaxEntries), 2)
.SetGreaterThanZero()
.SetDisplay("Max Entries", "Maximum pyramiding steps", "RSI");
_rsiPyramidSteps = Param(nameof(RsiPyramidSteps), 250m)
.SetGreaterThanZero()
.SetDisplay("Pyramid Steps", "Price steps between entries", "RSI");
_stopLossSteps = Param(nameof(StopLossSteps), 500m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss Steps", "Stop-loss distance in price steps", "Risk");
_trailingSteps = Param(nameof(TrailingSteps), 700m)
.SetGreaterThanZero()
.SetDisplay("Trailing Steps", "Trailing distance in price steps", "Risk");
_boxLookback = Param(nameof(BoxLookback), 16)
.SetGreaterThanZero()
.SetDisplay("Box Lookback", "Candles for box high/low", "Box");
_boxEntrySteps = Param(nameof(BoxEntrySteps), 180m)
.SetGreaterThanZero()
.SetDisplay("Box Entry Steps", "Breakout distance in price steps", "Box");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousRsi = 0m;
_hasPreviousRsi = false;
_lastEntryPrice = null;
_entryCount = 0;
_activeStopPrice = null;
_bestPrice = 0m;
_boxHigh = 0m;
_boxLow = decimal.MaxValue;
_boxBarsCount = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_rsi = new RelativeStrengthIndex { Length = RsiPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_rsi, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _rsi);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal rsiValue)
{
if (candle.State != CandleStates.Finished)
return;
var step = Security?.PriceStep ?? 1m;
var enteredThisCandle = false;
// Update box tracking
UpdateBox(candle);
// Manage trailing stop
ManageTrailing(candle, step);
if (!IsFormedAndOnlineAndAllowTrading())
{
_previousRsi = rsiValue;
_hasPreviousRsi = true;
return;
}
// Check box breakout entries
if (Position == 0 && _boxBarsCount >= BoxLookback)
{
var boxOffset = BoxEntrySteps * step;
if (candle.ClosePrice > _boxHigh + boxOffset)
{
BuyMarket(Volume);
_lastEntryPrice = candle.ClosePrice;
_entryCount = 1;
_activeStopPrice = candle.ClosePrice - StopLossSteps * step;
_bestPrice = candle.ClosePrice;
enteredThisCandle = true;
}
else if (candle.ClosePrice < _boxLow - boxOffset)
{
SellMarket(Volume);
_lastEntryPrice = candle.ClosePrice;
_entryCount = 1;
_activeStopPrice = candle.ClosePrice + StopLossSteps * step;
_bestPrice = candle.ClosePrice;
enteredThisCandle = true;
}
}
// RSI crossover signals
if (!enteredThisCandle && _hasPreviousRsi && _rsi.IsFormed)
{
var rsiCrossUp = _previousRsi <= RsiLowerLevel && rsiValue > RsiLowerLevel;
var rsiCrossDown = _previousRsi >= RsiUpperLevel && rsiValue < RsiUpperLevel;
if (rsiCrossUp && Position <= 0)
{
var vol = Volume + (Position < 0 ? Math.Abs(Position) : 0);
BuyMarket(vol);
_lastEntryPrice = candle.ClosePrice;
_entryCount = 1;
_activeStopPrice = candle.ClosePrice - StopLossSteps * step;
_bestPrice = candle.ClosePrice;
}
else if (rsiCrossDown && Position >= 0)
{
var vol = Volume + (Position > 0 ? Position : 0);
SellMarket(vol);
_lastEntryPrice = candle.ClosePrice;
_entryCount = 1;
_activeStopPrice = candle.ClosePrice + StopLossSteps * step;
_bestPrice = candle.ClosePrice;
}
// Pyramiding
var pyramidDist = RsiPyramidSteps * step;
if (Position > 0 && _entryCount < RsiMaxEntries && _lastEntryPrice.HasValue)
{
if (candle.ClosePrice >= _lastEntryPrice.Value + pyramidDist)
{
BuyMarket(Volume);
_lastEntryPrice = candle.ClosePrice;
_entryCount++;
}
}
else if (Position < 0 && _entryCount < RsiMaxEntries && _lastEntryPrice.HasValue)
{
if (candle.ClosePrice <= _lastEntryPrice.Value - pyramidDist)
{
SellMarket(Volume);
_lastEntryPrice = candle.ClosePrice;
_entryCount++;
}
}
}
_previousRsi = rsiValue;
_hasPreviousRsi = true;
}
private void UpdateBox(ICandleMessage candle)
{
_boxBarsCount++;
if (_boxBarsCount <= BoxLookback)
{
_boxHigh = Math.Max(_boxHigh, candle.HighPrice);
_boxLow = Math.Min(_boxLow, candle.LowPrice);
}
else
{
// Shift the window - approximate by using recent candle
_boxHigh = Math.Max(_boxHigh, candle.HighPrice);
_boxLow = Math.Min(_boxLow, candle.LowPrice);
}
}
private void ManageTrailing(ICandleMessage candle, decimal step)
{
if (Position == 0)
{
_activeStopPrice = null;
return;
}
if (!_activeStopPrice.HasValue)
return;
var trailDist = TrailingSteps * step;
if (Position > 0)
{
if (candle.ClosePrice > _bestPrice)
_bestPrice = candle.ClosePrice;
var trailStop = _bestPrice - trailDist;
if (trailStop > _activeStopPrice.Value)
_activeStopPrice = trailStop;
if (candle.LowPrice <= _activeStopPrice.Value)
{
SellMarket(Position);
_activeStopPrice = null;
_lastEntryPrice = null;
_entryCount = 0;
}
}
else
{
if (candle.ClosePrice < _bestPrice || _bestPrice == 0m)
_bestPrice = candle.ClosePrice;
var trailStop = _bestPrice + trailDist;
if (trailStop < _activeStopPrice.Value)
_activeStopPrice = trailStop;
if (candle.HighPrice >= _activeStopPrice.Value)
{
BuyMarket(Math.Abs(Position));
_activeStopPrice = null;
_lastEntryPrice = null;
_entryCount = 0;
}
}
}
}
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.Indicators import RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
class master_mm_droid_strategy(Strategy):
def __init__(self):
super(master_mm_droid_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15)))
self._rsi_period = self.Param("RsiPeriod", 14)
self._rsi_lower_level = self.Param("RsiLowerLevel", 25.0)
self._rsi_upper_level = self.Param("RsiUpperLevel", 75.0)
self._rsi_max_entries = self.Param("RsiMaxEntries", 2)
self._rsi_pyramid_steps = self.Param("RsiPyramidSteps", 250.0)
self._stop_loss_steps = self.Param("StopLossSteps", 500.0)
self._trailing_steps = self.Param("TrailingSteps", 700.0)
self._box_lookback = self.Param("BoxLookback", 16)
self._box_entry_steps = self.Param("BoxEntrySteps", 180.0)
self._previous_rsi = 0.0
self._has_previous_rsi = False
self._last_entry_price = None
self._entry_count = 0
self._active_stop_price = None
self._best_price = 0.0
self._box_high = 0.0
self._box_low = 999999999.0
self._box_bars_count = 0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(master_mm_droid_strategy, self).OnStarted2(time)
self._previous_rsi = 0.0
self._has_previous_rsi = False
self._last_entry_price = None
self._entry_count = 0
self._active_stop_price = None
self._best_price = 0.0
self._box_high = 0.0
self._box_low = 999999999.0
self._box_bars_count = 0
self._rsi = RelativeStrengthIndex()
self._rsi.Length = int(self._rsi_period.Value)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._rsi, self.ProcessCandle).Start()
def ProcessCandle(self, candle, rsi_value):
if candle.State != CandleStates.Finished:
return
rsi_val = float(rsi_value)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
entered_this_candle = False
self._update_box(candle)
self._manage_trailing(candle, step)
if not self.IsFormedAndOnlineAndAllowTrading():
self._previous_rsi = rsi_val
self._has_previous_rsi = True
return
vol = float(self.Volume)
box_lookback = int(self._box_lookback.Value)
pos = float(self.Position)
if pos == 0 and self._box_bars_count >= box_lookback:
box_offset = float(self._box_entry_steps.Value) * step
if close > self._box_high + box_offset:
self.BuyMarket(vol)
self._last_entry_price = close
self._entry_count = 1
self._active_stop_price = close - float(self._stop_loss_steps.Value) * step
self._best_price = close
entered_this_candle = True
elif close < self._box_low - box_offset:
self.SellMarket(vol)
self._last_entry_price = close
self._entry_count = 1
self._active_stop_price = close + float(self._stop_loss_steps.Value) * step
self._best_price = close
entered_this_candle = True
if not entered_this_candle and self._has_previous_rsi and self._rsi.IsFormed:
rsi_lower = float(self._rsi_lower_level.Value)
rsi_upper = float(self._rsi_upper_level.Value)
rsi_cross_up = self._previous_rsi <= rsi_lower and rsi_val > rsi_lower
rsi_cross_down = self._previous_rsi >= rsi_upper and rsi_val < rsi_upper
pos = float(self.Position)
if rsi_cross_up and pos <= 0:
entry_vol = vol + (abs(pos) if pos < 0 else 0.0)
self.BuyMarket(entry_vol)
self._last_entry_price = close
self._entry_count = 1
self._active_stop_price = close - float(self._stop_loss_steps.Value) * step
self._best_price = close
elif rsi_cross_down and pos >= 0:
entry_vol = vol + (pos if pos > 0 else 0.0)
self.SellMarket(entry_vol)
self._last_entry_price = close
self._entry_count = 1
self._active_stop_price = close + float(self._stop_loss_steps.Value) * step
self._best_price = close
pyramid_dist = float(self._rsi_pyramid_steps.Value) * step
max_entries = int(self._rsi_max_entries.Value)
pos = float(self.Position)
if pos > 0 and self._entry_count < max_entries and self._last_entry_price is not None:
if close >= self._last_entry_price + pyramid_dist:
self.BuyMarket(vol)
self._last_entry_price = close
self._entry_count += 1
elif pos < 0 and self._entry_count < max_entries and self._last_entry_price is not None:
if close <= self._last_entry_price - pyramid_dist:
self.SellMarket(vol)
self._last_entry_price = close
self._entry_count += 1
self._previous_rsi = rsi_val
self._has_previous_rsi = True
def _update_box(self, candle):
high = float(candle.HighPrice)
low = float(candle.LowPrice)
self._box_bars_count += 1
if high > self._box_high:
self._box_high = high
if low < self._box_low:
self._box_low = low
def _manage_trailing(self, candle, step):
pos = float(self.Position)
if pos == 0:
self._active_stop_price = None
return
if self._active_stop_price is None:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
trail_dist = float(self._trailing_steps.Value) * step
if pos > 0:
if close > self._best_price:
self._best_price = close
trail_stop = self._best_price - trail_dist
if trail_stop > self._active_stop_price:
self._active_stop_price = trail_stop
if low <= self._active_stop_price:
self.SellMarket(pos)
self._active_stop_price = None
self._last_entry_price = None
self._entry_count = 0
else:
if close < self._best_price or self._best_price == 0.0:
self._best_price = close
trail_stop = self._best_price + trail_dist
if trail_stop < self._active_stop_price:
self._active_stop_price = trail_stop
if high >= self._active_stop_price:
self.BuyMarket(abs(pos))
self._active_stop_price = None
self._last_entry_price = None
self._entry_count = 0
def OnReseted(self):
super(master_mm_droid_strategy, self).OnReseted()
self._previous_rsi = 0.0
self._has_previous_rsi = False
self._last_entry_price = None
self._entry_count = 0
self._active_stop_price = None
self._best_price = 0.0
self._box_high = 0.0
self._box_low = 999999999.0
self._box_bars_count = 0
def CreateClone(self):
return master_mm_droid_strategy()