在 GitHub 上查看

Kolier SuperTrend X2 策略

概述

该策略复刻原始的 MetaTrader 专家顾问,通过两个不同周期的 SuperTrend 滤波器协同工作。高周期 SuperTrend 用于识别市场主趋势,低周期 SuperTrend 负责在趋势方向出现同步反转时触发交易信号。移植到 StockSharp 时使用了高级 API 绑定,因此指标直接接收K线并维护内部状态。

交易逻辑

  • 趋势过滤: 高周期 SuperTrend 需要确认多头或空头走势。TrendSignalShift 控制确认的延迟,TrendMode 决定是只要一根K线(NewWay)还是需要两根连续K线(其余模式)。
  • 进场信号: 低周期 SuperTrend 等待与当前趋势一致的方向切换。EntrySignalShift 用于延迟到完全收盘的K线,EntryMode 控制策略在切换后立即响应(NewWay)还是等待确认(其他模式)。
  • 做多:EnableBuyEntriestrue、趋势过滤显示多头且低周期 SuperTrend 按选定模式翻转向上时,平掉可能存在的空头头寸,再以 Volume + |Position| 的量开多。
  • 做空:EnableSellEntriestrue、趋势过滤显示空头且低周期 SuperTrend 翻转向下时,先平掉多头再开空。
  • 离场:
    • 高周期趋势反向时,可根据 CloseBuyOnTrendFlipCloseSellOnTrendFlip 平掉相应方向仓位。
    • 低周期方向切换,在启用 CloseBuyOnEntryFlip/CloseSellOnEntryFlip 时也会触发平仓。
    • 可选的固定止损/止盈(StopLossPointsTakeProfitPoints)按 Security.PriceStep 的倍数计算。

指标

  • 两个 StockSharp SuperTrend 实例(分别用于趋势过滤与入场信号)。

参数

  • TrendCandleType – 趋势过滤所用K线周期。
  • EntryCandleType – 入场信号所用K线周期。
  • TrendAtrPeriodTrendAtrMultiplier – 趋势 SuperTrend 的 ATR 设置。
  • EntryAtrPeriodEntryAtrMultiplier – 入场 SuperTrend 的 ATR 设置。
  • TrendModeEntryMode – 确认模式:NewWay 一根K线即可,其余模式需要两根连续K线(在本移植中 Visual 与 ExpertSignal 与经典模式一致)。
  • TrendSignalShiftEntrySignalShift – 使用指标值前需要等待的已收盘K线数量。
  • EnableBuyEntriesEnableSellEntries – 是否允许做多/做空。
  • CloseBuyOnTrendFlipCloseSellOnTrendFlip – 趋势过滤反向时的离场选项。
  • CloseBuyOnEntryFlipCloseSellOnEntryFlip – 入场周期方向反向时的离场选项。
  • StopLossPointsTakeProfitPoints – 以价格跳动(PriceStep)为单位的止损/止盈距离(0 表示禁用)。
  • Volume – 新开仓的基础数量。
  • Slippage – 为兼容原始专家保留的占位参数。

说明

  • 该移植遵循 StockSharp 的高级流程:通过 SubscribeCandles 订阅K线、使用 BindEx 绑定指标,只保存必要的少量状态(趋势方向和当前止损/止盈)。
  • 调用了 StartProtection() 来启用 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>
/// Kolier SuperTrend X2 strategy that combines higher timeframe trend filtering and lower timeframe entries.
/// </summary>
public class KolierSuperTrendX2Strategy : Strategy
{
	private readonly StrategyParam<DataType> _trendCandleType;
	private readonly StrategyParam<DataType> _entryCandleType;
	private readonly StrategyParam<int> _trendAtrPeriod;
	private readonly StrategyParam<decimal> _trendAtrMultiplier;
	private readonly StrategyParam<int> _entryAtrPeriod;
	private readonly StrategyParam<decimal> _entryAtrMultiplier;
	private readonly StrategyParam<KolierTrendModes> _trendMode;
	private readonly StrategyParam<KolierTrendModes> _entryMode;
	private readonly StrategyParam<int> _trendSignalShift;
	private readonly StrategyParam<int> _entrySignalShift;
	private readonly StrategyParam<bool> _enableBuyEntries;
	private readonly StrategyParam<bool> _enableSellEntries;
	private readonly StrategyParam<bool> _closeBuyOnTrendFlip;
	private readonly StrategyParam<bool> _closeSellOnTrendFlip;
	private readonly StrategyParam<bool> _closeBuyOnEntryFlip;
	private readonly StrategyParam<bool> _closeSellOnEntryFlip;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;

	private SuperTrend _trendSuperTrend = null!;
	private SuperTrend _entrySuperTrend = null!;

	private readonly List<int> _trendDirections = new();
	private readonly List<int> _entryDirections = new();

	private int _trendDirection;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;

	/// <summary>
	/// Initializes a new instance of <see cref="KolierSuperTrendX2Strategy"/>.
	/// </summary>
	public KolierSuperTrendX2Strategy()
	{

		_trendCandleType = Param(nameof(TrendCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Trend Timeframe", "Timeframe for trend SuperTrend", "Data");

		_entryCandleType = Param(nameof(EntryCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Entry Timeframe", "Timeframe for entry SuperTrend", "Data");

		_trendAtrPeriod = Param(nameof(TrendAtrPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("Trend ATR Period", "ATR period for trend SuperTrend", "Trend");

		_trendAtrMultiplier = Param(nameof(TrendAtrMultiplier), 3m)
			.SetGreaterThanZero()
			.SetDisplay("Trend ATR Multiplier", "ATR multiplier for trend SuperTrend", "Trend");

		_entryAtrPeriod = Param(nameof(EntryAtrPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("Entry ATR Period", "ATR period for entry SuperTrend", "Entry");

		_entryAtrMultiplier = Param(nameof(EntryAtrMultiplier), 3m)
			.SetGreaterThanZero()
			.SetDisplay("Entry ATR Multiplier", "ATR multiplier for entry SuperTrend", "Entry");

		_trendMode = Param(nameof(TrendMode), KolierTrendModes.NewWay)
			.SetDisplay("Trend Mode", "Mode used for trend confirmation", "Trend");

		_entryMode = Param(nameof(EntryMode), KolierTrendModes.NewWay)
			.SetDisplay("Entry Mode", "Mode used for entry detection", "Entry");

		_trendSignalShift = Param(nameof(TrendSignalShift), 1)
			.SetRange(0, 10)
			.SetDisplay("Trend Signal Shift", "Bars to delay trend confirmation", "Trend");

		_entrySignalShift = Param(nameof(EntrySignalShift), 1)
			.SetRange(0, 10)
			.SetDisplay("Entry Signal Shift", "Bars to delay entry confirmation", "Entry");

		_enableBuyEntries = Param(nameof(EnableBuyEntries), true)
			.SetDisplay("Enable Long Entries", "Allow opening long positions", "Trading");

		_enableSellEntries = Param(nameof(EnableSellEntries), true)
			.SetDisplay("Enable Short Entries", "Allow opening short positions", "Trading");

		_closeBuyOnTrendFlip = Param(nameof(CloseBuyOnTrendFlip), true)
			.SetDisplay("Close Long On Trend Flip", "Close longs when higher timeframe turns bearish", "Exits");

		_closeSellOnTrendFlip = Param(nameof(CloseSellOnTrendFlip), true)
			.SetDisplay("Close Short On Trend Flip", "Close shorts when higher timeframe turns bullish", "Exits");

		_closeBuyOnEntryFlip = Param(nameof(CloseBuyOnEntryFlip), false)
			.SetDisplay("Close Long On Entry Flip", "Close longs when entry SuperTrend flips down", "Exits");

		_closeSellOnEntryFlip = Param(nameof(CloseSellOnEntryFlip), false)
			.SetDisplay("Close Short On Entry Flip", "Close shorts when entry SuperTrend flips up", "Exits");

		_stopLossPoints = Param(nameof(StopLossPoints), 1000m)
			.SetDisplay("Stop Loss (steps)", "Stop loss distance in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000m)
			.SetDisplay("Take Profit (steps)", "Take profit distance in price steps", "Risk");
	}


	/// <summary>
	/// Candle type used for the trend filter.
	/// </summary>
	public DataType TrendCandleType
	{
		get => _trendCandleType.Value;
		set => _trendCandleType.Value = value;
	}

	/// <summary>
	/// Candle type used for entry signals.
	/// </summary>
	public DataType EntryCandleType
	{
		get => _entryCandleType.Value;
		set => _entryCandleType.Value = value;
	}

	/// <summary>
	/// ATR period for the trend SuperTrend filter.
	/// </summary>
	public int TrendAtrPeriod
	{
		get => _trendAtrPeriod.Value;
		set => _trendAtrPeriod.Value = value;
	}

	/// <summary>
	/// ATR multiplier for the trend SuperTrend filter.
	/// </summary>
	public decimal TrendAtrMultiplier
	{
		get => _trendAtrMultiplier.Value;
		set => _trendAtrMultiplier.Value = value;
	}

	/// <summary>
	/// ATR period for the entry SuperTrend filter.
	/// </summary>
	public int EntryAtrPeriod
	{
		get => _entryAtrPeriod.Value;
		set => _entryAtrPeriod.Value = value;
	}

	/// <summary>
	/// ATR multiplier for the entry SuperTrend filter.
	/// </summary>
	public decimal EntryAtrMultiplier
	{
		get => _entryAtrMultiplier.Value;
		set => _entryAtrMultiplier.Value = value;
	}

	/// <summary>
	/// Confirmation mode for the trend timeframe.
	/// </summary>
	public KolierTrendModes TrendMode
	{
		get => _trendMode.Value;
		set => _trendMode.Value = value;
	}

	/// <summary>
	/// Confirmation mode for the entry timeframe.
	/// </summary>
	public KolierTrendModes EntryMode
	{
		get => _entryMode.Value;
		set => _entryMode.Value = value;
	}

	/// <summary>
	/// Bars to shift when confirming trend direction.
	/// </summary>
	public int TrendSignalShift
	{
		get => _trendSignalShift.Value;
		set => _trendSignalShift.Value = value;
	}

	/// <summary>
	/// Bars to shift when confirming entry signals.
	/// </summary>
	public int EntrySignalShift
	{
		get => _entrySignalShift.Value;
		set => _entrySignalShift.Value = value;
	}

	/// <summary>
	/// Allow opening long positions.
	/// </summary>
	public bool EnableBuyEntries
	{
		get => _enableBuyEntries.Value;
		set => _enableBuyEntries.Value = value;
	}

	/// <summary>
	/// Allow opening short positions.
	/// </summary>
	public bool EnableSellEntries
	{
		get => _enableSellEntries.Value;
		set => _enableSellEntries.Value = value;
	}

	/// <summary>
	/// Close long positions when the higher timeframe turns bearish.
	/// </summary>
	public bool CloseBuyOnTrendFlip
	{
		get => _closeBuyOnTrendFlip.Value;
		set => _closeBuyOnTrendFlip.Value = value;
	}

	/// <summary>
	/// Close short positions when the higher timeframe turns bullish.
	/// </summary>
	public bool CloseSellOnTrendFlip
	{
		get => _closeSellOnTrendFlip.Value;
		set => _closeSellOnTrendFlip.Value = value;
	}

	/// <summary>
	/// Close long positions when the entry SuperTrend flips bearish.
	/// </summary>
	public bool CloseBuyOnEntryFlip
	{
		get => _closeBuyOnEntryFlip.Value;
		set => _closeBuyOnEntryFlip.Value = value;
	}

	/// <summary>
	/// Close short positions when the entry SuperTrend flips bullish.
	/// </summary>
	public bool CloseSellOnEntryFlip
	{
		get => _closeSellOnEntryFlip.Value;
		set => _closeSellOnEntryFlip.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in price steps.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in price steps.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, EntryCandleType);

		if (!TrendCandleType.Equals(EntryCandleType))
		{
			yield return (Security, TrendCandleType);
		}
	}

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

		_trendDirections.Clear();
		_entryDirections.Clear();
		_trendDirection = 0;
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}

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

		_trendSuperTrend = new SuperTrend
		{
			Length = TrendAtrPeriod,
			Multiplier = TrendAtrMultiplier
		};

		_entrySuperTrend = new SuperTrend
		{
			Length = EntryAtrPeriod,
			Multiplier = EntryAtrMultiplier
		};

		var entrySubscription = SubscribeCandles(EntryCandleType);

		if (TrendCandleType.Equals(EntryCandleType))
		{
			entrySubscription
				.BindEx(_trendSuperTrend, ProcessTrendCandle)
				.BindEx(_entrySuperTrend, ProcessEntryCandle)
				.Start();
		}
		else
		{
			entrySubscription
				.BindEx(_entrySuperTrend, ProcessEntryCandle)
				.Start();

			var trendSubscription = SubscribeCandles(TrendCandleType);
			trendSubscription
				.BindEx(_trendSuperTrend, ProcessTrendCandle)
				.Start();
		}

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, entrySubscription);
			DrawIndicator(area, _entrySuperTrend);
			DrawOwnTrades(area);
		}

	}

	private void ProcessTrendCandle(ICandleMessage candle, IIndicatorValue value)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!value.IsFormed || value is not SuperTrendIndicatorValue trendValue)
			return;

		if (!_trendSuperTrend.IsFormed)
			return;

		var direction = trendValue.IsUpTrend ? 1 : -1;
		if (direction == 0)
			return;

		UpdateHistory(_trendDirections, direction, TrendSignalShift + 3);

		var current = GetDirection(_trendDirections, TrendSignalShift);
		var previous = GetDirection(_trendDirections, TrendSignalShift + 1);

		if (current is null || previous is null)
			return;

		switch (TrendMode)
		{
			case KolierTrendModes.NewWay:
				_trendDirection = current.Value;
				break;
			default:
				if (current == previous)
				{
					_trendDirection = current.Value;
				}
				break;
		}
	}

	private void ProcessEntryCandle(ICandleMessage candle, IIndicatorValue value)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!value.IsFormed || value is not SuperTrendIndicatorValue entryValue)
			return;

		if (!_entrySuperTrend.IsFormed)
			return;

		var direction = entryValue.IsUpTrend ? 1 : -1;
		if (direction == 0)
			return;

		UpdateHistory(_entryDirections, direction, EntrySignalShift + 3);

		if (HandleStops(candle))
			return;

		var current = GetDirection(_entryDirections, EntrySignalShift);
		var previous = GetDirection(_entryDirections, EntrySignalShift + 1);

		if (current is null || previous is null)
			return;

		var flipToUp = IsFlipTo(1, current, previous);
		var flipToDown = IsFlipTo(-1, current, previous);

		var closeLong = Position > 0 && ((CloseBuyOnTrendFlip && _trendDirection < 0) || (CloseBuyOnEntryFlip && current < 0));
		var closeShort = Position < 0 && ((CloseSellOnTrendFlip && _trendDirection > 0) || (CloseSellOnEntryFlip && current > 0));

		if (closeLong)
		{
			SellMarket();
			ResetStops();
		}

		if (closeShort)
		{
			BuyMarket();
			ResetStops();
		}

		if (EnableBuyEntries && _trendDirection > 0 && flipToUp && Position <= 0)
		{
			BuyMarket();
			UpdateStops(candle.ClosePrice, true);
		}
		else if (EnableSellEntries && _trendDirection < 0 && flipToDown && Position >= 0)
		{
			SellMarket();
			UpdateStops(candle.ClosePrice, false);
		}
	}

	private bool HandleStops(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_stopLossPrice is decimal sl && candle.LowPrice <= sl)
			{
				SellMarket();
				ResetStops();
				return true;
			}

			if (_takeProfitPrice is decimal tp && candle.HighPrice >= tp)
			{
				SellMarket();
				ResetStops();
				return true;
			}
		}
		else if (Position < 0)
		{
			var absPos = Math.Abs(Position);

			if (_stopLossPrice is decimal sl && candle.HighPrice >= sl)
			{
				BuyMarket(absPos);
				ResetStops();
				return true;
			}

			if (_takeProfitPrice is decimal tp && candle.LowPrice <= tp)
			{
				BuyMarket(absPos);
				ResetStops();
				return true;
			}
		}

		return false;
	}

	private void UpdateStops(decimal entryPrice, bool isLong)
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
		{
			_stopLossPrice = null;
			_takeProfitPrice = null;
			return;
		}

		_stopLossPrice = StopLossPoints > 0m
			? isLong ? entryPrice - StopLossPoints * step : entryPrice + StopLossPoints * step
			: null;

		_takeProfitPrice = TakeProfitPoints > 0m
			? isLong ? entryPrice + TakeProfitPoints * step : entryPrice - TakeProfitPoints * step
			: null;
	}

	private void ResetStops()
	{
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}

	private static void UpdateHistory(List<int> history, int direction, int maxLength)
	{
		history.Insert(0, direction);

		while (history.Count > maxLength && history.Count > 0)
		{
			try { history.RemoveAt(history.Count - 1); }
			catch { break; }
		}
	}

	private static int? GetDirection(List<int> history, int offset)
	{
		return history.Count > offset ? history[offset] : null;
	}

	private bool IsFlipTo(int targetDirection, int? current, int? previous)
	{
		if (current != targetDirection)
			return false;

		return EntryMode switch
		{
			KolierTrendModes.NewWay => previous != targetDirection,
			_ => previous == -targetDirection,
		};
	}

	/// <summary>
	/// Enumeration mirroring the original MQL SuperTrend modes.
	/// </summary>
	public enum KolierTrendModes
	{
		/// <summary>
		/// Classic SuperTrend confirmation requiring consecutive candles.
		/// </summary>
		SuperTrend,

		/// <summary>
		/// Faster confirmation that reacts after a single candle flip.
		/// </summary>
		NewWay,

		/// <summary>
		/// Visual mode (treated like classic confirmation in this port).
		/// </summary>
		Visual,

		/// <summary>
		/// Expert signal mode (treated like classic confirmation in this port).
		/// </summary>
		ExpertSignal,
	}
}