在 GitHub 上查看

Doctor 策略

将 MQL 中的策略 15233 "Doctor" 转换为 StockSharp 的实现。

概述

策略结合多种经典指标以识别趋势和动量:

  • 趋势方向:通过 40 周期加权移动平均线 (WMA) 的斜率判断。
  • 相对位置:将长期 WMA(400) 与最近三根K线的高低点比较。
  • 动量确认:使用 RSI(14) 和 RSI(5)。
  • 趋势反转过滤:来自 Parabolic SAR。

当所有多头条件满足时开多仓,所有空头条件满足时开空仓。出现反向信号或触及保护水平时平仓。可选的追踪止损在价格盈利超过止损距离的一半后上移止损。

参数

  • StopLossTicks – 止损距离(以跳动单位计)。
  • TakeProfitTicks – 止盈距离(以跳动单位计)。
  • TrailingStop – 是否启用追踪止损。
  • CandleType – 使用的K线周期,默认30分钟。

交易规则

  1. 做多 当:
    • WMA(40) 斜率向上;
    • WMA(400) 位于最近三根K线的最高点之上;
    • RSI(14) 高于50且 RSI(5) 低于 RSI(14);
    • 无已开的多单。
  2. 做空 当:
    • WMA(40) 斜率向下;
    • WMA(400) 位于最近三根K线的最低点之下;
    • RSI(14) 低于50且 RSI(5) 高于 RSI(14);
    • 无已开的空单。
  3. 离场 当出现反向条件或价格触及止损/止盈。追踪止损在利润达到一半距离后移动。

指标

  • 加权移动平均 (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();
		}
	}
}