在 GitHub 上查看

UmnickTrader 策略

该策略从原版 MQL5 顾问 UmnickTrader 移植而来,属于自适应的逆势/摇摆交易系统。策略在任意时刻只保留一张头寸, 并根据上一笔交易的盈亏来切换做多或做空方向。它以 (O + H + L + C) / 4 的平均价格来衡量行情变化,只有当该平均 值相对上一次处理时发生至少 StopBase 的移动后才会评估新的信号。

核心逻辑

  • 对每根收盘的K线计算 (开盘价 + 最高价 + 最低价 + 收盘价) / 4 的平均价。
  • 当当前平均价与上一次处理的平均价之差的绝对值大于或等于 StopBase 时才会触发信号判断,模拟原策略等待足够大 波动的逻辑。
  • 若当前没有持仓,则根据两个循环缓冲区(各保存最近八笔盈利和亏损的幅度)来计算自适应的止盈与止损距离。
  • 盈利交易结束后,将持仓期间的最大有利波动减去点差垫值写入盈利缓冲区,同时将 StopBase + 7 * Spread 写入亏损 缓冲区。
  • 亏损交易结束后,将盈利缓冲区重置为 StopBase - 3 * Spread,亏损缓冲区记录最大回撤再加点差垫值,并把下一笔交 易的方向反转。

交易管理

  • 止盈与止损的基准距离都是 StopBase。如果缓冲区的累计值超过 StopBase / 2,则使用缓冲区的平均值来动态放宽或 收紧出场距离。
  • 入场采用市价单,策略内部保存预期的止盈价与止损价,当K线的最高价或最低价触及这些水平时主动平仓。
  • 持仓过程中,策略利用K线极值跟踪最大的顺势波动和最大回撤,这些统计值会在交易结束时写入缓冲区。
  • 任何时刻只允许一张头寸;如果上一笔交易尚未完成,则忽略新的信号。

参数

  • StopBase – 判断行情是否足够波动的基准距离,同时也是初始的止盈/止损距离,默认 0.017
  • TradeVolume – 市价单的交易量,默认 0.1
  • Spread – 更新自适应缓冲区时使用的点差补偿,默认 0.0005
  • CandleType – 用于计算的K线类型,默认 TimeSpan.FromMinutes(5).TimeFrame()

分类与筛选

  • 方向:双向(但同一时间只持有一个方向)。
  • 风格:自适应摇摆 / 逆势交易。
  • 指标:价格平均值,自定义的波动缓冲区。
  • 止损/止盈:由策略动态计算并管理。
  • 复杂度:中等——需要维护缓冲区并进行自适应计算。
  • 周期:由 CandleType 参数决定。
  • 季节性 / 新闻过滤:未使用。
  • 风险管理:仓位规模由 TradeVolume 固定,出场距离根据近期表现动态调整。
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>
/// Adaptive mean-reversion strategy converted from the UmnickTrader MQL5 expert advisor.
/// </summary>
public class UmnickTraderStrategy : Strategy
{
	// Number of trade results stored for adaptive calculations.
	private readonly StrategyParam<int> _bufferLength;

	private readonly StrategyParam<decimal> _stopBase;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<decimal> _spread;
	private readonly StrategyParam<int> _entryCooldownBars;
	private readonly StrategyParam<DataType> _candleType;

	// Adaptive buffers storing profit and loss distances observed recently.
	private decimal[] _profitBuffer = Array.Empty<decimal>();
	private decimal[] _lossBuffer = Array.Empty<decimal>();

	// Rolling state for signal detection and risk metrics.
	private decimal _lastAveragePrice;
	private decimal _entryPrice;
	private decimal _takeProfitPrice;
	private decimal _stopLossPrice;
	private decimal _maxProfit;
	private decimal _drawdown;
	private decimal _lastTradeProfit;

	private int _currentIndex;
	private int _currentDirection = 1;
	private int _cooldownRemaining;

	private bool _positionActive;
	private bool _isLongPosition;
	private bool _positionJustClosed;

	/// <summary>
	/// Number of trade results stored for adaptive calculations.
	/// </summary>
	public int BufferLength
	{
		get => _bufferLength.Value;
		set
		{
			if (_bufferLength.Value == value)
				return;

			_bufferLength.Value = value;
			ResizeBuffers();
		}
	}

	public decimal StopBase
	{
		get => _stopBase.Value;
		set => _stopBase.Value = value;
	}

	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	public decimal Spread
	{
		get => _spread.Value;
		set => _spread.Value = value;
	}

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

	public int EntryCooldownBars
	{
		get => _entryCooldownBars.Value;
		set => _entryCooldownBars.Value = value;
	}

	public UmnickTraderStrategy()
	{
		_bufferLength = Param(nameof(BufferLength), 8)
		.SetGreaterThanZero()
		.SetDisplay("Buffer Length", "Number of trade results stored for adaptive calculations.", "Parameters")
		
		.SetOptimize(4, 32, 1);

		ResizeBuffers();

		_stopBase = Param(nameof(StopBase), 0.017m)
			.SetDisplay("Base Stop Distance", "Minimum average price move required to trigger evaluation.", "Parameters")
			
			.SetOptimize(0.005m, 0.05m, 0.005m);

		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
			.SetDisplay("Trade Volume", "Order volume for each position.", "Parameters")
			
			.SetOptimize(0.05m, 1m, 0.05m);

		_spread = Param(nameof(Spread), 0.0005m)
			.SetDisplay("Spread Padding", "Spread compensation used when updating adaptive buffers.", "Parameters")
			
			.SetOptimize(0.0001m, 0.002m, 0.0001m);

		_entryCooldownBars = Param(nameof(EntryCooldownBars), 6)
			.SetGreaterThanZero()
			.SetDisplay("Entry Cooldown", "Bars to wait after a position is closed.", "Parameters");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Source candle series.", "General");
	}

	private void ResizeBuffers()
	{
		var length = BufferLength;

		_profitBuffer = new decimal[length];
		_lossBuffer = new decimal[length];
		_currentIndex = 0;
	}

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

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

		_lastAveragePrice = 0m;
		_entryPrice = 0m;
		_takeProfitPrice = 0m;
		_stopLossPrice = 0m;
		_maxProfit = 0m;
		_drawdown = 0m;
		_lastTradeProfit = 0m;
		_currentIndex = 0;
		_currentDirection = 1;
		_cooldownRemaining = 0;
		_positionActive = false;
		_isLongPosition = false;
		_positionJustClosed = false;

		Array.Clear(_profitBuffer, 0, _profitBuffer.Length);
		Array.Clear(_lossBuffer, 0, _lossBuffer.Length);
	}

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

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

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

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

		// Update metrics for an active position before generating new signals.
		UpdateOpenPosition(candle);

		if (_cooldownRemaining > 0)
			_cooldownRemaining--;

		// Average of OHLC replicates the MQL5 price smoothing logic.
		var averagePrice = (candle.OpenPrice + candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 4m;
		if (!ShouldProcessAverage(averagePrice))
			return;

		if (Position != 0)
			return;

		var limitDistance = StopBase;
		var stopDistance = StopBase;

		decimal sumProfit = 0m;
		decimal sumLoss = 0m;
		var bufferLength = _profitBuffer.Length;

		if (bufferLength == 0)
			return;

		for (var i = 0; i < bufferLength; i++)
		{
			sumProfit += _profitBuffer[i];
			sumLoss += _lossBuffer[i];
		}

		// Recalculate adaptive take-profit and stop-loss distances.
		if (sumProfit > StopBase / 2m)
			limitDistance = sumProfit / bufferLength;

		if (sumLoss > StopBase / 2m)
			stopDistance = sumLoss / bufferLength;

		if (_positionJustClosed)
		{
			_positionJustClosed = false;

			// Store the most recent excursion metrics.
			if (_lastTradeProfit > 0m)
			{
				_profitBuffer[_currentIndex] = _maxProfit - Spread * 3m;
				_lossBuffer[_currentIndex] = StopBase + Spread * 7m;
			}
			else
			{
				_profitBuffer[_currentIndex] = StopBase - Spread * 3m;
				_lossBuffer[_currentIndex] = _drawdown + Spread * 7m;
				_currentDirection = -_currentDirection;
			}

			_currentIndex++;
			if (_currentIndex >= bufferLength)
				_currentIndex = 0;

			_cooldownRemaining = EntryCooldownBars;
			return;
		}

		if (limitDistance <= 0m || stopDistance <= 0m)
			return;

		if (_cooldownRemaining > 0)
			return;

		var volume = TradeVolume;
		if (volume <= 0m)
			return;

		// Enter in the current direction using market orders.
		if (_currentDirection > 0)
			OpenLong(candle.ClosePrice, limitDistance, stopDistance, volume);
		else
			OpenShort(candle.ClosePrice, limitDistance, stopDistance, volume);
	}

	private bool ShouldProcessAverage(decimal averagePrice)
	{
		if (_lastAveragePrice == 0m)
		{
			_lastAveragePrice = averagePrice;
			return true;
		}

		var difference = Math.Abs(averagePrice - _lastAveragePrice);
		if (difference >= StopBase)
		{
			_lastAveragePrice = averagePrice;
			return true;
		}

		return false;
	}

	private void UpdateOpenPosition(ICandleMessage candle)
	{
		if (!_positionActive)
			return;

		// Track intrabar extremes to measure maximum favorable and adverse excursions.
		if (_isLongPosition)
		{
			var profitMove = candle.HighPrice - _entryPrice;
			if (profitMove > _maxProfit)
				_maxProfit = profitMove;

			var lossMove = _entryPrice - candle.LowPrice;
			if (lossMove > _drawdown)
				_drawdown = lossMove;

			if (candle.LowPrice <= _stopLossPrice)
			{
				CloseCurrentPosition(_stopLossPrice);
				return;
			}

			if (candle.HighPrice >= _takeProfitPrice)
			{
				CloseCurrentPosition(_takeProfitPrice);
				return;
			}
		}
		else
		{
			var profitMove = _entryPrice - candle.LowPrice;
			if (profitMove > _maxProfit)
				_maxProfit = profitMove;

			var lossMove = candle.HighPrice - _entryPrice;
			if (lossMove > _drawdown)
				_drawdown = lossMove;

			if (candle.HighPrice >= _stopLossPrice)
			{
				CloseCurrentPosition(_stopLossPrice);
				return;
			}

			if (candle.LowPrice <= _takeProfitPrice)
			{
				CloseCurrentPosition(_takeProfitPrice);
				return;
			}
		}
	}

	private void CloseCurrentPosition(decimal exitPrice)
	{
		// Close the position and record realized profit for buffer updates.
		var profit = _isLongPosition ? exitPrice - _entryPrice : _entryPrice - exitPrice;
		_positionActive = false;
		_isLongPosition = false;
		_entryPrice = 0m;
		_takeProfitPrice = 0m;
		_stopLossPrice = 0m;

		_lastTradeProfit = profit;
		_positionJustClosed = true;

		if (Position > 0)
			SellMarket();
		else if (Position < 0)
			BuyMarket();
	}

	private void OpenLong(decimal price, decimal limitDistance, decimal stopDistance, decimal volume)
	{
		BuyMarket(volume);

		// Store trade parameters for managing exits on subsequent candles.
		_entryPrice = price;
		_takeProfitPrice = price + limitDistance;
		_stopLossPrice = price - stopDistance;
		_positionActive = true;
		_isLongPosition = true;
		_lastTradeProfit = 0m;
		_maxProfit = 0m;
		_drawdown = 0m;
	}

	private void OpenShort(decimal price, decimal limitDistance, decimal stopDistance, decimal volume)
	{
		SellMarket(volume);

		// Store trade parameters for managing exits on subsequent candles.
		_entryPrice = price;
		_takeProfitPrice = price - limitDistance;
		_stopLossPrice = price + stopDistance;
		_positionActive = true;
		_isLongPosition = false;
		_lastTradeProfit = 0m;
		_maxProfit = 0m;
		_drawdown = 0m;
	}
}