在 GitHub 上查看

Omni Trend 策略

概述

Omni Trend 策略是 MetaTrader 专家顾问 "Exp_Omni_Trend" 的 StockSharp 版本。策略由移动均线与基于 ATR 的波动通道组成,价格突破前一根柱子的保护带时即视为趋势反转,系统会平掉相反方向的持仓并在新的方向上建立仓位。实现保留了原始顾问的所有关键选项,包括信号执行延迟 (SignalBar) 以及针对多空的独立开仓/平仓开关。

策略订阅选定的 K 线序列,仅对已完成的 K 线进行计算:

  • 移动均线 (MaType, MaLength, AppliedPrice) 给出趋势中心;
  • ATR (AtrLength) 与 VolatilityFactorMoneyRisk 一起生成自适应上下轨,这些轨道在逻辑上等同于跟踪止损。

当价格的最高价突破上一根柱子的上轨时,趋势切换为多头;最低价跌破下轨时,趋势切换为空头。趋势发生变化时会立即发出与新方向一致的开仓指令,同时不断要求关闭所有与趋势相反的头寸(前提是对应开关启用)。

可选的 StopLossPointsTakeProfitPoints 以价格最小变动单位为度量,用于在指标信号之外附加硬性止损/止盈。仓位大小由策略自身的 Volume 属性控制,默认值为 1

交易流程

  1. 根据所选价格字段计算移动均线。
  2. 计算 ATR 并生成上下轨道。
  3. HighPrice 突破上一根柱子的上轨:
    • 趋势切换为向上;
    • 如启用 EnableSellClose,立即平掉所有空单;
    • 如启用 EnableBuyOpen,在队列中记录一个多单信号,延迟 SignalBar 根柱子执行。
  4. LowPrice 跌破上一根柱子的下轨:
    • 趋势切换为向下;
    • 如启用 EnableBuyClose,立即平掉所有多单;
    • 如启用 EnableSellOpen,在队列中记录一个空单信号,按照设定延迟执行。
  5. 在趋势维持期间,对应方向的平仓信号会持续出现,确保仓位始终顺应当前趋势。
  6. 每根完成的 K 线都会触发风险管理检查:当价格触及止损或止盈水平(以最小价格单位表示)时立即平仓,并清除记忆的入场价。

信号通过 FIFO 队列调度。SignalBar = 0 时,信号在当前 K 线收盘时执行;大于零时,执行发生在完成延迟的那根 K 线开盘价附近,与原顾问使用上一根柱子信号、下一根柱子执行的模式一致。

参数

参数 说明 默认值
CandleType 计算所用的 K 线类型/周期。 4 小时
MaLength 移动均线周期。 13
MaType 移动均线方法:Simple、Exponential、Smoothed、LinearWeighted。 Exponential
AppliedPrice 移动均线使用的价格字段:Close、Open、High、Low、Median、Typical、Weighted。 Close
AtrLength ATR 周期。 11
VolatilityFactor ATR 乘数,用于生成原始通道。 1.3
MoneyRisk 通道相对均线的位移系数。 0.15
SignalBar 信号执行前需要等待的已完成 K 线数量。 1
EnableBuyOpen 允许开多。 true
EnableSellOpen 允许开空。 true
EnableBuyClose 允许在趋势转空时平掉多头。 true
EnableSellClose 允许在趋势转多时平掉空头。 true
StopLossPoints 止损距离(价格最小变动单位),0 表示禁用。 1000
TakeProfitPoints 止盈距离(价格最小变动单位),0 表示禁用。 2000
Volume 策略持仓数量属性。 1

使用建议

  • SignalBar = 1 能够复刻原顾问的默认行为:信号产生后在下一根 K 线开盘附近成交。设置为 0 时表示在当根 K 线收盘执行。
  • 使用止损或止盈前请确认标的提供有效的 PriceStep
  • 策略会在图表上绘制 K 线、所选移动均线以及自身成交,便于快速验证逻辑。
  • 可以通过关闭 Enable* 开关让策略只做多或只做空,或由人工接管平仓。
  • 策略仅发送市价单(BuyMarket/SellMarket),与 MQL 版本直接下单的方式一致。

文件结构

  • CS/OmniTrendStrategy.cs — 策略的 C# 实现。
  • README.md, README_ru.md, README_zh.md — 英语、俄语、中文说明文档。

根据任务要求,本次未提供 Python 版本。

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-following strategy that replicates the Omni Trend MetaTrader expert.
/// </summary>
public class OmniTrendStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _maLength;
	private readonly StrategyParam<MovingAverageMethods> _maType;
	private readonly StrategyParam<AppliedPriceTypes> _appliedPrice;
	private readonly StrategyParam<int> _atrLength;
	private readonly StrategyParam<decimal> _volatilityFactor;
	private readonly StrategyParam<decimal> _moneyRisk;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<bool> _enableBuyOpen;
	private readonly StrategyParam<bool> _enableSellOpen;
	private readonly StrategyParam<bool> _enableBuyClose;
	private readonly StrategyParam<bool> _enableSellClose;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private readonly List<SignalInfo> _pendingSignals = new();

	private IIndicator _ma;
	private AverageTrueRange _atr;
	private decimal _previousSmin;
	private decimal _previousSmax;
	private decimal _previousTrendUp;
	private decimal _previousTrendDown;
	private int _previousTrend;
	private bool _isInitialized;
	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;

	public OmniTrendStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used to build Omni Trend signals", "General")
			;

		_maLength = Param(nameof(MaLength), 13)
			.SetDisplay("MA Length", "Moving average period", "Indicators")
			.SetGreaterThanZero()
			;

		_maType = Param(nameof(MaType), MovingAverageMethods.Exponential)
			.SetDisplay("MA Type", "Moving average calculation method", "Indicators")
			;

		_appliedPrice = Param(nameof(AppliedPrice), AppliedPriceTypes.Close)
			.SetDisplay("Applied Price", "Price field used by the moving average", "Indicators")
			;

		_atrLength = Param(nameof(AtrLength), 11)
			.SetDisplay("ATR Length", "ATR period for volatility bands", "Indicators")
			.SetGreaterThanZero()
			;

		_volatilityFactor = Param(nameof(VolatilityFactor), 1.3m)
			.SetDisplay("Volatility Factor", "Multiplier applied to ATR", "Indicators")
			.SetGreaterThanZero()
			;

		_moneyRisk = Param(nameof(MoneyRisk), 0.15m)
			.SetDisplay("Money Risk", "Offset factor used to position trend bands", "Indicators")
			.SetGreaterThanZero()
			;

		_signalBar = Param(nameof(SignalBar), 0)
			.SetDisplay("Signal Bar", "Delay in bars before acting on a signal", "Trading")
			;

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

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

		_enableBuyClose = Param(nameof(EnableBuyClose), true)
			.SetDisplay("Enable Long Exits", "Allow closing long positions", "Trading");

		_enableSellClose = Param(nameof(EnableSellClose), true)
			.SetDisplay("Enable Short Exits", "Allow closing short positions", "Trading");

		_stopLossPoints = Param(nameof(StopLossPoints), 0)
			.SetDisplay("Stop Loss (points)", "Protective stop distance expressed in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 0)
			.SetDisplay("Take Profit (points)", "Profit target distance expressed in price steps", "Risk");

		Volume = 1m;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int MaLength
	{
		get => _maLength.Value;
		set => _maLength.Value = Math.Max(1, value);
	}

	public MovingAverageMethods MaType
	{
		get => _maType.Value;
		set => _maType.Value = value;
	}

	public AppliedPriceTypes AppliedPrice
	{
		get => _appliedPrice.Value;
		set => _appliedPrice.Value = value;
	}

	public int AtrLength
	{
		get => _atrLength.Value;
		set => _atrLength.Value = Math.Max(1, value);
	}

	public decimal VolatilityFactor
	{
		get => _volatilityFactor.Value;
		set => _volatilityFactor.Value = value;
	}

	public decimal MoneyRisk
	{
		get => _moneyRisk.Value;
		set => _moneyRisk.Value = value;
	}

	public int SignalBar
	{
		get => Math.Max(0, _signalBar.Value);
		set => _signalBar.Value = Math.Max(0, value);
	}

	public bool EnableBuyOpen
	{
		get => _enableBuyOpen.Value;
		set => _enableBuyOpen.Value = value;
	}

	public bool EnableSellOpen
	{
		get => _enableSellOpen.Value;
		set => _enableSellOpen.Value = value;
	}

	public bool EnableBuyClose
	{
		get => _enableBuyClose.Value;
		set => _enableBuyClose.Value = value;
	}

	public bool EnableSellClose
	{
		get => _enableSellClose.Value;
		set => _enableSellClose.Value = value;
	}

	public int StopLossPoints
	{
		get => Math.Max(0, _stopLossPoints.Value);
		set => _stopLossPoints.Value = Math.Max(0, value);
	}

	public int TakeProfitPoints
	{
		get => Math.Max(0, _takeProfitPoints.Value);
		set => _takeProfitPoints.Value = Math.Max(0, value);
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();

		_pendingSignals.Clear();
		_ma = null;
		_atr = null;
		_previousSmin = 0m;
		_previousSmax = 0m;
		_previousTrendUp = 0m;
		_previousTrendDown = 0m;
		_previousTrend = 0;
		_isInitialized = false;
		_longEntryPrice = null;
		_shortEntryPrice = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_ma = CreateMovingAverage(MaType, MaLength);
		_atr = new AverageTrueRange
		{
			Length = AtrLength,
		};

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

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

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_ma is null || _atr is null)
			return;

		var atrValue = _atr.Process(new CandleIndicatorValue(_atr, candle));
		var appliedPrice = GetAppliedPrice(candle, AppliedPrice);
		var maValue = _ma.Process(new DecimalIndicatorValue(_ma, appliedPrice, candle.OpenTime) { IsFinal = true });

		if (!atrValue.IsFinal || !maValue.IsFinal)
			return;

		CheckRiskManagement(candle);

		var atr = atrValue.GetValue<decimal>();
		var ma = maValue.GetValue<decimal>();
		var signal = CalculateSignal(candle, ma, atr);

		_pendingSignals.Add(signal);
		while (_pendingSignals.Count > SignalBar)
		{
			var pending = _pendingSignals[0];
			try { _pendingSignals.RemoveAt(0); } catch { break; }
			ExecuteSignal(candle, pending);
		}
	}

	private SignalInfo CalculateSignal(ICandleMessage candle, decimal ma, decimal atr)
	{
		var smax = ma + VolatilityFactor * atr;
		var smin = ma - VolatilityFactor * atr;

		if (!_isInitialized)
		{
			_previousSmax = smax;
			_previousSmin = smin;
			_previousTrendUp = 0m;
			_previousTrendDown = 0m;
			_previousTrend = 0;
			_isInitialized = true;
			return SignalInfo.Empty;
		}

		var trend = _previousTrend;
		if (candle.HighPrice > _previousSmax)
			trend = 1;
		else if (candle.LowPrice < _previousSmin)
			trend = -1;

		decimal? trendUp = null;
		decimal? trendDown = null;

		if (trend > 0)
		{
			if (smin < _previousSmin)
				smin = _previousSmin;

			var candidate = smin - (MoneyRisk - 1m) * atr;
			if (_previousTrend > 0 && _previousTrendUp > 0m && candidate < _previousTrendUp)
				candidate = _previousTrendUp;

			trendUp = candidate;
		}
		else if (trend < 0)
		{
			if (smax > _previousSmax)
				smax = _previousSmax;

			var candidate = smax + (MoneyRisk - 1m) * atr;
			if (_previousTrend < 0 && _previousTrendDown > 0m && candidate > _previousTrendDown)
				candidate = _previousTrendDown;

			trendDown = candidate;
		}

		var signal = SignalInfo.Empty;

		if (trend > 0)
		{
			if (_previousTrend <= 0 && trendUp.HasValue && EnableBuyOpen)
				signal.BuyOpen = true;

			if (trendUp.HasValue && EnableSellClose)
				signal.SellClose = true;
		}
		else if (trend < 0)
		{
			if (_previousTrend >= 0 && trendDown.HasValue && EnableSellOpen)
				signal.SellOpen = true;

			if (trendDown.HasValue && EnableBuyClose)
				signal.BuyClose = true;
		}

		_previousTrend = trend;
		_previousSmax = smax;
		_previousSmin = smin;
		_previousTrendUp = trendUp ?? 0m;
		_previousTrendDown = trendDown ?? 0m;

		return signal;
	}

	private void ExecuteSignal(ICandleMessage candle, SignalInfo signal)
	{
		if (signal.BuyClose && Position > 0)
		{
			var volume = Math.Abs(Position);
			if (volume > 0)
				SellMarket();
			_longEntryPrice = null;
		}

		if (signal.SellClose && Position < 0)
		{
			var volume = Math.Abs(Position);
			if (volume > 0)
				BuyMarket();
			_shortEntryPrice = null;
		}

		var executionPrice = SignalBar == 0 ? candle.ClosePrice : candle.OpenPrice;

		if (signal.BuyOpen && Position <= 0)
		{
			if (Position < 0)
			{
				var volume = Math.Abs(Position);
				BuyMarket();
				_shortEntryPrice = null;
			}

			BuyMarket();
			_longEntryPrice = executionPrice;
		}

		if (signal.SellOpen && Position >= 0)
		{
			if (Position > 0)
			{
				var volume = Math.Abs(Position);
				SellMarket();
				_longEntryPrice = null;
			}

			SellMarket();
			_shortEntryPrice = executionPrice;
		}
	}

	private void CheckRiskManagement(ICandleMessage candle)
	{
		if (Security is null)
			return;

		var step = Security?.PriceStep ?? 0.01m;
		if (step <= 0m)
			return;

		if (Position > 0)
		{
			if (StopLossPoints > 0 && _longEntryPrice.HasValue)
			{
				var stopPrice = _longEntryPrice.Value - StopLossPoints * step;
				if (candle.LowPrice <= stopPrice || candle.ClosePrice <= stopPrice)
				{
					SellMarket();
					_longEntryPrice = null;
					return;
				}
			}

			if (TakeProfitPoints > 0 && _longEntryPrice.HasValue)
			{
				var targetPrice = _longEntryPrice.Value + TakeProfitPoints * step;
				if (candle.HighPrice >= targetPrice || candle.ClosePrice >= targetPrice)
				{
					SellMarket();
					_longEntryPrice = null;
					return;
				}
			}
		}
		else if (Position < 0)
		{
			if (StopLossPoints > 0 && _shortEntryPrice.HasValue)
			{
				var stopPrice = _shortEntryPrice.Value + StopLossPoints * step;
				if (candle.HighPrice >= stopPrice || candle.ClosePrice >= stopPrice)
				{
					BuyMarket();
					_shortEntryPrice = null;
					return;
				}
			}

			if (TakeProfitPoints > 0 && _shortEntryPrice.HasValue)
			{
				var targetPrice = _shortEntryPrice.Value - TakeProfitPoints * step;
				if (candle.LowPrice <= targetPrice || candle.ClosePrice <= targetPrice)
				{
					BuyMarket();
					_shortEntryPrice = null;
					return;
				}
			}
		}
	}

	private static decimal GetAppliedPrice(ICandleMessage candle, AppliedPriceTypes type)
	{
		return type switch
		{
			AppliedPriceTypes.Open => candle.OpenPrice,
			AppliedPriceTypes.High => candle.HighPrice,
			AppliedPriceTypes.Low => candle.LowPrice,
			AppliedPriceTypes.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			AppliedPriceTypes.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			AppliedPriceTypes.Weighted => (candle.HighPrice + candle.LowPrice + 2m * candle.ClosePrice) / 4m,
			_ => candle.ClosePrice
		};
	}

	private static IIndicator CreateMovingAverage(MovingAverageMethods type, int length)
	{
		return type switch
		{
			MovingAverageMethods.Simple => new SMA { Length = length },
			MovingAverageMethods.Exponential => new EMA { Length = length },
			MovingAverageMethods.Smoothed => new EMA { Length = length },
			MovingAverageMethods.LinearWeighted => new SMA { Length = length },
			_ => new EMA { Length = length }
		};
	}

	private struct SignalInfo
	{
		public static readonly SignalInfo Empty = new();
		public bool BuyOpen;
		public bool BuyClose;
		public bool SellOpen;
		public bool SellClose;
	}

	public enum MovingAverageMethods
	{
		Simple,
		Exponential,
		Smoothed,
		LinearWeighted
	}

	public enum AppliedPriceTypes
	{
		Close,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted
	}
}