Doctor 策略
将 MQL 中的策略 15233 "Doctor" 转换为 StockSharp 的实现。
概述
策略结合多种经典指标以识别趋势和动量:
- 趋势方向:通过 40 周期加权移动平均线 (WMA) 的斜率判断。
- 相对位置:将长期 WMA(400) 与最近三根K线的高低点比较。
- 动量确认:使用 RSI(14) 和 RSI(5)。
- 趋势反转过滤:来自 Parabolic SAR。
当所有多头条件满足时开多仓,所有空头条件满足时开空仓。出现反向信号或触及保护水平时平仓。可选的追踪止损在价格盈利超过止损距离的一半后上移止损。
参数
StopLossTicks– 止损距离(以跳动单位计)。TakeProfitTicks– 止盈距离(以跳动单位计)。TrailingStop– 是否启用追踪止损。CandleType– 使用的K线周期,默认30分钟。
交易规则
- 做多 当:
- WMA(40) 斜率向上;
- WMA(400) 位于最近三根K线的最高点之上;
- RSI(14) 高于50且 RSI(5) 低于 RSI(14);
- 无已开的多单。
- 做空 当:
- WMA(40) 斜率向下;
- WMA(400) 位于最近三根K线的最低点之下;
- RSI(14) 低于50且 RSI(5) 高于 RSI(14);
- 无已开的空单。
- 离场 当出现反向条件或价格触及止损/止盈。追踪止损在利润达到一半距离后移动。
指标
- 加权移动平均 (40, 400)
- 相对强弱指数 (14, 5)
- 抛物线转向指标 (Parabolic SAR)
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>
/// Doctor strategy ported from MQL. Combines WMA slope, MA position, RSI and PSAR.
/// </summary>
public class DoctorStrategy : Strategy
{
private readonly StrategyParam<int> _stopLossTicks;
private readonly StrategyParam<int> _takeProfitTicks;
private readonly StrategyParam<bool> _trailingStop;
private readonly StrategyParam<DataType> _candleType;
private readonly decimal[] _wma40 = new decimal[2];
private readonly decimal[] _wma400 = new decimal[4];
private readonly decimal[] _high = new decimal[4];
private readonly decimal[] _low = new decimal[4];
private decimal _entryPrice;
private decimal _stopPrice;
private decimal _takePrice;
/// <summary>
/// Stop-loss distance in ticks.
/// </summary>
public int StopLossTicks
{
get => _stopLossTicks.Value;
set => _stopLossTicks.Value = value;
}
/// <summary>
/// Take-profit distance in ticks.
/// </summary>
public int TakeProfitTicks
{
get => _takeProfitTicks.Value;
set => _takeProfitTicks.Value = value;
}
/// <summary>
/// Enable trailing stop logic.
/// </summary>
public bool TrailingStop
{
get => _trailingStop.Value;
set => _trailingStop.Value = value;
}
/// <summary>
/// Candle type for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
private WeightedMovingAverage _wmaSlope = null!;
private WeightedMovingAverage _wmaTrend = null!;
private RelativeStrengthIndex _rsi14 = null!;
private RelativeStrengthIndex _rsi5 = null!;
private ParabolicSar _psar = null!;
/// <summary>
/// Initialize <see cref="DoctorStrategy"/>.
/// </summary>
public DoctorStrategy()
{
_stopLossTicks = Param(nameof(StopLossTicks), 70)
.SetGreaterThanZero()
.SetDisplay("Stop Loss", "Stop-loss distance in ticks", "Risk");
_takeProfitTicks = Param(nameof(TakeProfitTicks), 40)
.SetGreaterThanZero()
.SetDisplay("Take Profit", "Take-profit distance in ticks", "Risk");
_trailingStop = Param(nameof(TrailingStop), true)
.SetDisplay("Trailing Stop", "Use trailing stop", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for candles", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
Array.Clear(_wma40);
Array.Clear(_wma400);
Array.Clear(_high);
Array.Clear(_low);
_entryPrice = 0m;
_stopPrice = 0m;
_takePrice = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_wmaSlope = new WeightedMovingAverage { Length = 10 };
_wmaTrend = new WeightedMovingAverage { Length = 50 };
_rsi14 = new RelativeStrengthIndex { Length = 14 };
_rsi5 = new RelativeStrengthIndex { Length = 5 };
_psar = new ParabolicSar();
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_wmaSlope, _wmaTrend, _rsi14, _rsi5, _psar, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _wmaSlope);
DrawIndicator(area, _wmaTrend);
DrawIndicator(area, _psar);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal wma40, decimal wma400, decimal rsi14, decimal rsi5, decimal psar)
{
if (candle.State != CandleStates.Finished)
return;
// Shift history arrays (always, even during warmup)
_wma40[1] = _wma40[0];
_wma40[0] = wma40;
for (var i = 3; i > 0; i--)
{
_wma400[i] = _wma400[i - 1];
_high[i] = _high[i - 1];
_low[i] = _low[i - 1];
}
_wma400[0] = wma400;
_high[0] = candle.HighPrice;
_low[0] = candle.LowPrice;
if (_wma40[1] == 0m || _wma400[3] == 0m)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
// Determine slope direction
var slope = _wma40[0] > _wma40[1] ? 2 : 1;
// Check long-term MA relative to recent bars
var maBelow = _wma400[1] < _low[1] && _wma400[2] < _low[2] && _wma400[3] < _low[3];
var maAbove = _wma400[1] > _high[1] && _wma400[2] > _high[2] && _wma400[3] > _high[3];
var maLinear = maAbove ? 2 : maBelow ? 1 : 0;
// RSI relations
var rsiState = rsi14 < 50m && rsi5 > rsi14 ? 1 : rsi14 > 50m && rsi5 < rsi14 ? 2 : 0;
// Parabolic SAR position
var psarState = psar <= candle.LowPrice ? 1 : psar >= candle.HighPrice ? 2 : 0;
var step = Security?.PriceStep ?? 1m;
var stopDistance = StopLossTicks * step;
var takeDistance = TakeProfitTicks * step;
// Close positions on opposite signals
if (Position > 0 && slope == 1 && (maLinear == 1 || rsiState == 1 || psarState == 2))
{
SellMarket();
}
else if (Position < 0 && slope == 2 && (maLinear == 2 || rsiState == 2 || psarState == 1))
{
BuyMarket();
}
// Trailing and protective exits
if (Position > 0)
{
if (TrailingStop && candle.ClosePrice - _entryPrice > stopDistance / 2m)
_stopPrice = Math.Max(_stopPrice, candle.ClosePrice - stopDistance);
if (candle.LowPrice <= _stopPrice || candle.HighPrice >= _takePrice)
SellMarket();
}
else if (Position < 0)
{
if (TrailingStop && _entryPrice - candle.ClosePrice > stopDistance / 2m)
_stopPrice = Math.Min(_stopPrice, candle.ClosePrice + stopDistance);
if (candle.HighPrice >= _stopPrice || candle.LowPrice <= _takePrice)
BuyMarket();
}
// Entry conditions
if (slope == 2 && (maLinear == 2 || rsiState == 2) && Position <= 0)
{
_entryPrice = candle.ClosePrice;
_stopPrice = _entryPrice - stopDistance;
_takePrice = _entryPrice + takeDistance;
BuyMarket();
}
else if (slope == 1 && (maLinear == 1 || rsiState == 1) && Position >= 0)
{
_entryPrice = candle.ClosePrice;
_stopPrice = _entryPrice + stopDistance;
_takePrice = _entryPrice - takeDistance;
SellMarket();
}
}
}
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 WeightedMovingAverage, RelativeStrengthIndex, ParabolicSar
from StockSharp.Algo.Strategies import Strategy
class doctor_strategy(Strategy):
"""
Doctor strategy ported from MQL. Combines WMA slope, MA position, RSI and PSAR.
"""
def __init__(self):
super(doctor_strategy, self).__init__()
self._stop_loss_ticks = self.Param("StopLossTicks", 70) \
.SetDisplay("Stop Loss", "Stop-loss distance in ticks", "Risk")
self._take_profit_ticks = self.Param("TakeProfitTicks", 40) \
.SetDisplay("Take Profit", "Take-profit distance in ticks", "Risk")
self._trailing_stop = self.Param("TrailingStop", True) \
.SetDisplay("Trailing Stop", "Use trailing stop", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe for candles", "General")
self._wma40 = [0.0, 0.0]
self._wma400 = [0.0, 0.0, 0.0, 0.0]
self._high = [0.0, 0.0, 0.0, 0.0]
self._low = [0.0, 0.0, 0.0, 0.0]
self._entry_price = 0.0
self._stop_price = 0.0
self._take_price = 0.0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(doctor_strategy, self).OnReseted()
self._wma40 = [0.0, 0.0]
self._wma400 = [0.0, 0.0, 0.0, 0.0]
self._high = [0.0, 0.0, 0.0, 0.0]
self._low = [0.0, 0.0, 0.0, 0.0]
self._entry_price = 0.0
self._stop_price = 0.0
self._take_price = 0.0
def OnStarted2(self, time):
super(doctor_strategy, self).OnStarted2(time)
wma_slope = WeightedMovingAverage()
wma_slope.Length = 10
wma_trend = WeightedMovingAverage()
wma_trend.Length = 50
rsi14 = RelativeStrengthIndex()
rsi14.Length = 14
rsi5 = RelativeStrengthIndex()
rsi5.Length = 5
psar = ParabolicSar()
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(wma_slope, wma_trend, rsi14, rsi5, psar, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, wma_slope)
self.DrawIndicator(area, wma_trend)
self.DrawIndicator(area, psar)
self.DrawOwnTrades(area)
def _process_candle(self, candle, wma40, wma400, rsi14, rsi5, psar):
if candle.State != CandleStates.Finished:
return
wma40 = float(wma40)
wma400 = float(wma400)
rsi14 = float(rsi14)
rsi5 = float(rsi5)
psar = float(psar)
self._wma40[1] = self._wma40[0]
self._wma40[0] = wma40
for i in range(3, 0, -1):
self._wma400[i] = self._wma400[i - 1]
self._high[i] = self._high[i - 1]
self._low[i] = self._low[i - 1]
self._wma400[0] = wma400
self._high[0] = float(candle.HighPrice)
self._low[0] = float(candle.LowPrice)
if self._wma40[1] == 0.0 or self._wma400[3] == 0.0:
return
slope = 2 if self._wma40[0] > self._wma40[1] else 1
ma_below = (self._wma400[1] < self._low[1] and
self._wma400[2] < self._low[2] and
self._wma400[3] < self._low[3])
ma_above = (self._wma400[1] > self._high[1] and
self._wma400[2] > self._high[2] and
self._wma400[3] > self._high[3])
ma_linear = 2 if ma_above else (1 if ma_below else 0)
rsi_state = 0
if rsi14 < 50 and rsi5 > rsi14:
rsi_state = 1
elif rsi14 > 50 and rsi5 < rsi14:
rsi_state = 2
psar_state = 0
if psar <= float(candle.LowPrice):
psar_state = 1
elif psar >= float(candle.HighPrice):
psar_state = 2
step = 1.0
if self.Security is not None and self.Security.PriceStep is not None:
step = float(self.Security.PriceStep)
if step <= 0:
step = 1.0
stop_distance = self._stop_loss_ticks.Value * step
take_distance = self._take_profit_ticks.Value * step
if self.Position > 0 and slope == 1 and (ma_linear == 1 or rsi_state == 1 or psar_state == 2):
self.SellMarket()
elif self.Position < 0 and slope == 2 and (ma_linear == 2 or rsi_state == 2 or psar_state == 1):
self.BuyMarket()
close = float(candle.ClosePrice)
if self.Position > 0:
if self._trailing_stop.Value and close - self._entry_price > stop_distance / 2.0:
self._stop_price = max(self._stop_price, close - stop_distance)
if float(candle.LowPrice) <= self._stop_price or float(candle.HighPrice) >= self._take_price:
self.SellMarket()
elif self.Position < 0:
if self._trailing_stop.Value and self._entry_price - close > stop_distance / 2.0:
self._stop_price = min(self._stop_price, close + stop_distance)
if float(candle.HighPrice) >= self._stop_price or float(candle.LowPrice) <= self._take_price:
self.BuyMarket()
if slope == 2 and (ma_linear == 2 or rsi_state == 2) and self.Position <= 0:
self._entry_price = close
self._stop_price = self._entry_price - stop_distance
self._take_price = self._entry_price + take_distance
self.BuyMarket()
elif slope == 1 and (ma_linear == 1 or rsi_state == 1) and self.Position >= 0:
self._entry_price = close
self._stop_price = self._entry_price + stop_distance
self._take_price = self._entry_price - take_distance
self.SellMarket()
def CreateClone(self):
return doctor_strategy()