Master MM Droid 策略
概述
Master MM Droid 是将 MetaTrader 5 专家顾问迁移到 StockSharp 平台的多模块策略。实现完全基于高层 API:通过订阅蜡烛、绑定指标以及使用高层下单方法复制原有 EA 的结构。四个资金管理模块可以单独启用/禁用,从而自由组合动量入场、时间盒突破与周度缺口交易。
模块说明
- RSI 模块
- 在所选蜡烛类型上计算 14 周期 RSI。
- RSI 从超卖区向上穿越触发做多,从超买区向下穿越触发做空。
- 支持按固定点差叠加仓位,并可限制最大叠加次数。
- 进入后立即设置初始止损,并交由跟踪止损管理。
- 箱体突破模块
- 每日三次重新计算箱体范围(默认平移后的 6、12、20 点)。
- 在箱体高点上方和低点下方按照设定偏移放置 Buy Stop / Sell Stop。
- 在 0、10、16 点重置时取消所有挂单并平仓,保持与原版 EA 相同的节奏。
- 周度突破模块
- 记录周一开始阶段的最高价与最低价。
- 在
StartHour到WeeklySetupEndHour的窗口内放置对称的止损突破单,以 OCO 方式打开新的一周。 - 周五晚间强制平仓并撤单,避免周末持仓风险。
- 缺口模块
- 对比新交易日的开盘价与前一日的最高/最低价(同样应用时区平移)。
- 当开盘价低于前日最低价时买入,开盘价高于前日最高价时卖出。
- 以点差参数计算保护性止损,并交由公共跟踪模块继续管理。
参数总览
| 参数 | 说明 |
|---|---|
CandleType |
指标与时间调度使用的蜡烛类型。 |
TimeShiftHours |
相对于 UTC 的时间平移,用于对齐交易时段。 |
StartHour |
周度模块的基础起始小时(尚未平移前的值)。 |
Enable*Module |
控制四个模块的启用状态。 |
Rsi* |
RSI 周期、阈值以及加仓逻辑。 |
BoxEntryPoints、BoxTrailingPoints |
箱体突破的偏移与跟踪距离。 |
WeeklyEntryPoints、WeeklySetupEndHour、WeeklyTrailingPoints |
周度突破的触发设置。 |
GapStopLossPoints、GapTrailingPoints |
缺口交易的初始止损与跟踪距离。 |
所有以“点”为单位的参数都会乘以品种的 TickSize,从而适配不同报价精度。
交易逻辑
- 指标绑定:将 RSI 指标绑定到蜡烛订阅,
ProcessCandle在每根完整蜡烛结束时调用四个模块。 - 日内状态:维护当前日期的开盘价、最高价、最低价以及前一天的区间,为缺口与周度模块提供数据。
- 下单流程:统一使用
BuyMarket、SellMarket、BuyStop、SellStop等高层方法;在重新布置箱体或周度挂单前会取消旧挂单。 - 跟踪止损:
_activeTrailingPoints保存最新的跟踪距离,只允许止损朝有利方向移动。
风险控制
- RSI 与缺口模块在开仓时立即给出固定点差的初始止损。
- 箱体和周度突破交由跟踪止损管理,可根据需要叠加额外的组合风控。
- 使用
ClosePosition()平仓,便于与 StockSharp 的风险保护功能集成。
使用建议
- 策略针对单一标的,订单数量取自全局
Volume。若需按资产比例控制仓位,请结合组合级风控使用。 - 所有时间判断均在应用
TimeShiftHours后执行。例如默认值为 2,箱体在“0 点”重置实为服务器时间 02:00。 - StockSharp 采用净持仓模式,与 MT5 的对冲账户不同,因此无法同时保持方向相反的多个仓位。这是相对于原策略的主要差异。
监控
_boxOrdersPlaced、_weeklyOrdersPlaced等标志可帮助定位当前活动的模块。- 如需更丰富的可视化或日志,可使用 StockSharp 自带的图表与日志工具进行扩展。
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()