在 GitHub 上查看

Trend RDS

Trend RDS 策略专注于价格行为中的方向性结构。当三根已完成的 K 线呈现严格递增的低点时,被视为新的多头动能;三根严格递减的高点则被视为空头机会。若同一组三根 K 线同时出现更高低点和更低高点(通常意味着收敛三角形),信号会被过滤掉,以避免在无明显趋势时入场。Reverse 参数可以按需反转多空方向。

策略只在可配置的时间窗口内交易(默认 09:00–12:00)。在窗口开启且出现有效信号时,会先平掉反向持仓,再按照信号 K 线的收盘价市价入场,并按照点数设置止损与止盈。点值由标的的最小报价步长换算,复现原始 MetaTrader 系统的处理方式。可选的移动止损在价格走出“止损距离 + 止损步长”后推进保护位,且仅在交易窗口内更新。

每次入场都会重新计算仓位。策略以 RiskPercent 指定的权益比例作为风险预算,再除以当前止损对应的货币风险,从而得到动态的下单数量,同时保证不低于 Volume 的最小值。若某个风险参数为零,则关闭该功能,可按固定手数或无保护单进场。

详情

  • 入场条件:三根连续 K 线形成更高低点时做多(启用 Reverse 时改为做空);三根连续 K 线形成更低高点时做空(反向模式下改为做多)。若同一组三根 K 线同时满足两个条件,则忽略信号。
  • 多空方向:支持双向交易,可通过 Reverse 切换。
  • 离场条件:当跟踪的止损、止盈或移动止损被触发时以市价离场。
  • 止损设置:以点数定义的固定止损和止盈,可选的逐步移动止损(两个移动止损参数都需为正)。
  • 交易时段:仅在 StartTimeEndTime 之间下单(默认 09:00–12:00,按交易所时间)。
  • 仓位管理:按 RiskPercent 占用的账户权益与止损距离计算下单数量(无法计算时退回到 Volume)。
  • 默认参数
    • StopLossPips = 30
    • TakeProfitPips = 65
    • TrailingStopPips = 0
    • TrailingStepPips = 5
    • RiskPercent = 3
    • StartTime = 09:00
    • EndTime = 12:00
    • Reverse = false
  • 筛选标签
    • 分类:Trend
    • 方向:双向
    • 指标:价格行为(高点/低点)
    • 止损:是
    • 复杂度:中等
    • 周期:日内
    • 季节性:否
    • 神经网络:否
    • 背离:否
    • 风险等级:中等
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>
/// Trend recognition strategy based on three consecutive candles.
/// </summary>
public class TrendRdsStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<TimeSpan> _startTime;
	private readonly StrategyParam<TimeSpan> _endTime;
	private readonly StrategyParam<bool> _reverse;

	private decimal _prevHigh1;
	private decimal _prevHigh2;
	private decimal _prevHigh3;
	private decimal _prevLow1;
	private decimal _prevLow2;
	private decimal _prevLow3;
	private int _historyCount;

	private decimal _entryPrice;
	private decimal _stopLossPrice;
	private decimal _takeProfitPrice;

	/// <summary>
	/// Type of candles to analyze.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Stop loss distance measured in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take profit distance measured in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance measured in pips.
	/// </summary>
	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Trailing step measured in pips.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Percent of account equity to risk per trade.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Trading session start time (inclusive).
	/// </summary>
	public TimeSpan StartTime
	{
		get => _startTime.Value;
		set => _startTime.Value = value;
	}

	/// <summary>
	/// Trading session end time (exclusive).
	/// </summary>
	public TimeSpan EndTime
	{
		get => _endTime.Value;
		set => _endTime.Value = value;
	}

	/// <summary>
	/// Reverse the trade direction when enabled.
	/// </summary>
	public bool Reverse
	{
		get => _reverse.Value;
		set => _reverse.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="TrendRdsStrategy"/> class.
	/// </summary>
	public TrendRdsStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to use", "General");

		_stopLossPips = Param(nameof(StopLossPips), 30)
			.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk")
			;

		_takeProfitPips = Param(nameof(TakeProfitPips), 65)
			.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk")
			;

		_trailingStopPips = Param(nameof(TrailingStopPips), 0)
			.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk")
			;

		_trailingStepPips = Param(nameof(TrailingStepPips), 5)
			.SetDisplay("Trailing Step (pips)", "Trailing step increment", "Risk")
			;

		_riskPercent = Param(nameof(RiskPercent), 3m)
			.SetDisplay("Risk %", "Percent of equity to risk", "Risk")
			.SetRange(0m, 100m);

		_startTime = Param(nameof(StartTime), new TimeSpan(0, 0, 0))
			.SetDisplay("Session Start", "Trading session start time", "Session");

		_endTime = Param(nameof(EndTime), new TimeSpan(23, 59, 0))
			.SetDisplay("Session End", "Trading session end time", "Session");

		_reverse = Param(nameof(Reverse), false)
			.SetDisplay("Reverse", "Trade in the opposite direction", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_prevHigh1 = 0m;
		_prevHigh2 = 0m;
		_prevHigh3 = 0m;
		_prevLow1 = 0m;
		_prevLow2 = 0m;
		_prevLow3 = 0m;
		_historyCount = 0;
		_entryPrice = 0m;
		_stopLossPrice = 0m;
		_takeProfitPrice = 0m;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		if (TrailingStopPips > 0 && TrailingStepPips <= 0)
		{
			throw new InvalidOperationException("Trailing step must be greater than zero when trailing stop is enabled.");
		}

		if (StartTime >= EndTime)
		{
			throw new InvalidOperationException("Session start time must be earlier than end time.");
		}

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Skip unfinished candles to work on closed bars only.
		if (candle.State != CandleStates.Finished)
			return;

		var pip = GetPipSize();

		// Handle protective exits even outside of the session window.
		if (HandleActivePositionExits(candle))
		{
			UpdateHistory(candle);
			return;
		}

		var candleTime = candle.OpenTime.TimeOfDay;
		var inSession = candleTime >= StartTime && candleTime < EndTime;

		if (inSession && _historyCount >= 3)
		{
			var higherLows = _prevLow1 > _prevLow2 && _prevLow2 > _prevLow3;
			var lowerHighs = _prevHigh1 < _prevHigh2 && _prevHigh2 < _prevHigh3;
			var conflict = higherLows && lowerHighs;

			var longSignal = false;
			var shortSignal = false;

			if (!conflict)
			{
				if (higherLows)
				{
					if (Reverse)
						shortSignal = true;
					else
						longSignal = true;
				}

				if (lowerHighs)
				{
					if (Reverse)
						longSignal = true;
					else
						shortSignal = true;
				}
			}

			if (longSignal && Position <= 0)
			{
				EnterLong(candle, pip);
			}
			else if (shortSignal && Position >= 0)
			{
				EnterShort(candle, pip);
			}

			// Update trailing logic after potential entries.
			ApplyTrailing(candle, pip);
		}

		UpdateHistory(candle);
	}

	private void EnterLong(ICandleMessage candle, decimal pip)
	{
		var stopOffset = StopLossPips > 0 ? StopLossPips * pip : 0m;
		var takeOffset = TakeProfitPips > 0 ? TakeProfitPips * pip : 0m;

		var volume = CalculateVolume(stopOffset);
		if (Position < 0)
		{
			volume += Math.Abs(Position);
		}

		BuyMarket(volume);

		_entryPrice = candle.ClosePrice;
		_stopLossPrice = stopOffset > 0m ? _entryPrice - stopOffset : 0m;
		_takeProfitPrice = takeOffset > 0m ? _entryPrice + takeOffset : 0m;
	}

	private void EnterShort(ICandleMessage candle, decimal pip)
	{
		var stopOffset = StopLossPips > 0 ? StopLossPips * pip : 0m;
		var takeOffset = TakeProfitPips > 0 ? TakeProfitPips * pip : 0m;

		var volume = CalculateVolume(stopOffset);
		if (Position > 0)
		{
			volume += Math.Abs(Position);
		}

		SellMarket(volume);

		_entryPrice = candle.ClosePrice;
		_stopLossPrice = stopOffset > 0m ? _entryPrice + stopOffset : 0m;
		_takeProfitPrice = takeOffset > 0m ? _entryPrice - takeOffset : 0m;
	}

	private void ApplyTrailing(ICandleMessage candle, decimal pip)
	{
		if (TrailingStopPips <= 0)
			return;

		var trailingStop = TrailingStopPips * pip;
		var trailingStep = TrailingStepPips * pip;

		if (trailingStop <= 0m || trailingStep <= 0m || _entryPrice == 0m)
			return;

		var price = candle.ClosePrice;

		if (Position > 0)
		{
			var profit = price - _entryPrice;
			if (profit > trailingStop + trailingStep)
			{
				var threshold = price - (trailingStop + trailingStep);
				if (_stopLossPrice == 0m || _stopLossPrice < threshold)
				{
					_stopLossPrice = price - trailingStop;
				}
			}
		}
		else if (Position < 0)
		{
			var profit = _entryPrice - price;
			if (profit > trailingStop + trailingStep)
			{
				var threshold = price + (trailingStop + trailingStep);
				if (_stopLossPrice == 0m || _stopLossPrice > threshold)
				{
					_stopLossPrice = price + trailingStop;
				}
			}
		}
	}

	private bool HandleActivePositionExits(ICandleMessage candle)
	{
		var positionVolume = Math.Abs(Position);
		if (positionVolume == 0m)
			return false;

		if (Position > 0)
		{
			if (_stopLossPrice > 0m && candle.LowPrice <= _stopLossPrice)
			{
				SellMarket(positionVolume);
				ResetPositionState();
				return true;
			}

			if (_takeProfitPrice > 0m && candle.HighPrice >= _takeProfitPrice)
			{
				SellMarket(positionVolume);
				ResetPositionState();
				return true;
			}
		}
		else
		{
			if (_stopLossPrice > 0m && candle.HighPrice >= _stopLossPrice)
			{
				BuyMarket(positionVolume);
				ResetPositionState();
				return true;
			}

			if (_takeProfitPrice > 0m && candle.LowPrice <= _takeProfitPrice)
			{
				BuyMarket(positionVolume);
				ResetPositionState();
				return true;
			}
		}

		return false;
	}

	private decimal CalculateVolume(decimal stopOffset)
	{
		var baseVolume = Volume > 0m ? Volume : 1m;
		var equity = Portfolio?.CurrentValue ?? 0m;

		if (stopOffset <= 0m || equity <= 0m)
			return baseVolume;

		var step = Security?.PriceStep ?? 0m;
		var stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? 1m;

		if (step <= 0m)
			return baseVolume;

		var stepsToStop = stopOffset / step;
		if (stepsToStop <= 0m)
			return baseVolume;

		var riskAmount = equity * RiskPercent / 100m;
		if (riskAmount <= 0m)
			return baseVolume;

		var riskPerUnit = stepsToStop * stepPrice;
		if (riskPerUnit <= 0m)
			return baseVolume;

		var quantity = riskAmount / riskPerUnit;
		if (quantity <= 0m)
			return baseVolume;

		return Math.Max(quantity, baseVolume);
	}

	private void UpdateHistory(ICandleMessage candle)
	{
		_prevHigh3 = _prevHigh2;
		_prevHigh2 = _prevHigh1;
		_prevHigh1 = candle.HighPrice;

		_prevLow3 = _prevLow2;
		_prevLow2 = _prevLow1;
		_prevLow1 = candle.LowPrice;

		if (_historyCount < 3)
		{
			_historyCount++;
		}
	}

	private void ResetPositionState()
	{
		_entryPrice = 0m;
		_stopLossPrice = 0m;
		_takeProfitPrice = 0m;
	}

	private decimal GetPipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return 1m;

		var pip = step;
		var decimals = GetScale(step);

		if (decimals == 3 || decimals == 5)
		{
			pip *= 10m;
		}

		return pip;
	}

	private static int GetScale(decimal value)
	{
		var bits = decimal.GetBits(value);
		return (bits[3] >> 16) & 0xFF;
	}
}