GitHub で見る

UmnickTrader戦略

オリジナルのUmnickTrader MQL5エキスパートアドバイザーから変換されたアダプティブ平均回帰システムです。この戦略は一度に1つのポジションで動作し、前の取引の結果に応じてロングとショートのバイアスを交互に切り替えます。始値、高値、安値、終値の平均を使用して価格の動きを評価し、その平均値が設定されたStopBase距離以上移動した場合にのみアクションを取ります。

コアロジック

  • すべての完成したローソク足について、平均価格(O + H + L + C) / 4が計算されます。
  • 現在の平均と以前に処理された平均の絶対差がStopBase以上である場合にのみシグナルが処理されます。これは、十分に大きな動きを待つという元のEAの動作を模倣します。
  • ポジションがオープンしていない場合、最近の8つの利益と損失の偏差を保存する2つの循環バッファーを使用して、アダプティブなテイクプロフィットとストップロス距離を計算します。
  • 利益のある取引の後、ポジションがオープンしている間に観察された最大の有利な偏差が利益バッファーに保存され(スプレッドパディングを差し引いたもの)、損失バッファーはStopBase + 7 * Spreadを受け取ります。
  • 損失取引の後、利益バッファーはStopBase - 3 * Spreadにリセットされ、損失バッファーは記録されたドローダウンにスプレッドパディングを加えて更新され、取引方向は反転して次のセットアップが反対側を取引します。

取引管理

  • テイクプロフィットとストップロスの両方のデフォルト距離はStopBaseです。累積した利益または損失バッファーがStopBase / 2を超えた場合、それぞれの平均値がデフォルト距離に置き換わり、エグジットレベルを適応的に広げたり絞ったりします。
  • エントリーには成行注文が使用されます。期待されるテイクプロフィットとストップロス価格は戦略自体によって保存および管理されるため、ローソク足の高値または安値が対応するレベルに触れるとポジションがクローズされます。
  • ポジションがアクティブな間、最も有利な動きと最も深いドローダウンがイントラバーの極値を使用して追跡されます。これらの統計は取引がクローズしたときにバッファーに入力されます。
  • 任意の時点で存在できるのは1つのポジションのみです。前の取引が完了していない場合、新しいシグナルは無視されます。

パラメーター

  • StopBase – 動きを重要なものとして扱うために必要な基本距離(価格単位)とデフォルトのTP/SL距離。デフォルト:0.017
  • TradeVolume – 成行注文のボリューム。デフォルト:0.1
  • Spread – アダプティブバッファーを更新する際に適用されるスプレッド補償。デフォルト:0.0005
  • CandleType – 平均値を評価するために使用するローソク足サブスクリプション。デフォルト: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;
	}
}