GitHub で見る

OpenTiks戦略

概要

OpenTiks 戦略は、従来の MetaTrader エキスパート アドバイザー OpenTiks.mq4 を StockSharp エコシステムに移植します。オリジナルロボット 初期のブレイクアウトを検出するために、厳密に単調な高値と開口部を持つローソク足の階段を探しました。信号が現れると、 成行注文を開き、オプションで保護ストップを付け、その後徐々に利益を確定しながらポジションを追跡した 露出を半分にすることを繰り返します。 StockSharp バージョンは、高レベルの API 呼び出し、キャンドル サブスクリプション、 組み込みの順序ヘルパーにより、ロジックが Designer、Runner、または任意のカスタム S# アプリケーション内で実行されます。

パターン検出

4 つの連続するローソク足が次のパターンのいずれかを満たした場合、取引を開始できます。

  • 強気ブレイクアウト – 現在のローソク足と前の 3 つのバー: 各 High は前のローソク足よりも厳密に高い High、および各 Open は、その前の Open よりも厳密に高くなります。
  • 弱気ブレイクアウト – 同じ 4 本足ウィンドウの場合: 各 High は前の High よりも厳密に低く、各 Open 以前の Open よりも厳密に低くなります。

シグナルは、構成された CandleType によって配信される完了したローソク足で評価されます。ブレークアウト条件が満たされると、 戦略は、設定された数量 (証券の VolumeStep に正規化され、MinVolume によって制限される) で成行注文を送信します。 および MaxVolume)。 MaxOrders パラメータは、同時に存在できるエントリの数を制限します。値をゼロにするとチェックが無効になります。 一方、正の数値は、絶対ネットポジションを正規化された注文量で割った値がその値に達すると、新しい取引をブロックします。 限界。

リスクと出口の管理

  • ストップロスStopLossPoints がゼロより大きい場合、戦略は最新のローソク足の価格反転を監視します。ロング ローソク足の安値が entryPrice - StopLossPoints × PriceStep を突破すると、ポジションは清算されます。ショートポジションが終了する場合 ハイタッチ entryPrice + StopLossPoints × PriceStep
  • トレーリングストップ – 価格がエントリーを超えて少なくとも TrailingStopPoints × PriceStep 前進すると、トレーリングストップが作動します クローズの後ろ(ロングの場合)または前(ショートの場合)と同じ距離にあります。トレーリングレベルが向上するたびに、 残りのポジションはオプションで減ります。
  • プログレッシブ利益確定UsePartialClose が有効な場合、戦略は毎回現在のエクスポージャーの半分をクローズします トレーリングストップは前進します。音量は楽器の VolumeStep に四捨五入されます。半分のサイズを下回った場合 MinVolume、代わりにポジション全体がクローズされ、MetaTrader のエキスパートの行動と一致します。

すべてのストップとトレーリングの計算は終了したローソク足で実行されるため、終了は毎回ではなく次のバーの終値で発生します。 入ってくるティック。これにより、元の実装に近い状態を維持しながら、実装は StockSharp の高レベルの API との一貫性が保たれます。 新しいバーに反応するというアイデア。

パラメーター

名前 種類 デフォルト 説明
OrderVolume decimal 0.1 各市場参入の基本ロットサイズ。この戦略は、証券のボリュームステップと制限に合わせてそれを正規化します。
StopLossPoints decimal 0 価格ポイント (価格ステップ) で表される保護停止距離。値をゼロにすると、停止が無効になります。
TrailingStopPoints decimal 30 ポジションが利益に移行した後にトレーリングストップによって維持される距離 (価格ポイントでも)。
MaxOrders int 1 同時にオープンするエントリの最大数。ゼロを指定すると制限が解除されます。
UsePartialClose bool true トレーリングストップが進むたびにゲインを固定する半減ロジックを有効にします。
CandleType DataType 1 minute 時間枠 シグナル評価とトレーリングチェックに使用されるプライマリローソク足サブスクリプション。

実装メモ

  • StockSharp は ネッティング ポジションで動作するため、設定された証券のすべての注文が単一のロングまたはショートに蓄積されます 露出。したがって、MaxOrders パラメータは、個々の MetaTrader チケットではなく、集計された位置に作用します。
  • ローソク足ベースのトレーリングは、完了したバーごとにストップチェックが 1 回行われることを意味します。ティックレベルの保護が必要なトレーダーは、 ローソク足のサイズを変更したり、ロジックを拡張して取引を購読したりできます。
  • 部分クロージャは、注文の拒否を回避するために商品メタデータ (VolumeStepMinVolumeMaxVolume) を尊重します。
  • インラインの英語のコメントは主な決定ポイントを強調表示するため、ファイルはアイデアを採用する際の教育資料としても機能します。 他のブレイクアウトや資金管理の実験に。

使い方のヒント

  1. 元の MetaTrader 設定で使用されたタイムフレームと一致するローソク足のタイプを選択します (M1 または M5 など)。
  2. 機器のステップとロットの設定を確認します。デフォルトの OrderVolume または 0.1 は外国為替スタイルの契約に適していますが、 先物、株式、または暗号通貨シンボルに合わせて調整されます。
  3. TrailingStopPointsUsePartialClose を試して、積極的な利益の確保と利益の確保の間のバランスを見つけてください。 勝者は走ります。
  4. この戦略と StockSharp チャートを組み合わせて、階段パターンを視覚的に確認し、部分的な出口を実際に観察します 時間。
namespace StockSharp.Samples.Strategies;

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;

using StockSharp.Algo;

/// <summary>
/// Reimplementation of the MetaTrader expert advisor "OpenTiks" for StockSharp.
/// Detects four consecutive candles with strictly monotonic opens and highs to trigger entries,
/// then manages the position with optional stop-loss, trailing stop and progressive partial exits.
/// </summary>
public class OpenTiksStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<int> _maxOrders;
	private readonly StrategyParam<bool> _usePartialClose;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _priceStep;
	private decimal _volumeStep;
	private decimal _minVolumeLimit;
	private decimal _maxVolumeLimit;

	private decimal? _high1;
	private decimal? _high2;
	private decimal? _high3;

	private decimal? _open1;
	private decimal? _open2;
	private decimal? _open3;

	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;
	private decimal? _longTrailingStop;
	private decimal? _shortTrailingStop;

	private SimpleMovingAverage _dummySma;
	private decimal _previousPosition;
	private decimal? _lastTradePrice;

	/// <summary>
	/// Initializes a new instance of the <see cref="OpenTiksStrategy"/> class.
	/// </summary>
	public OpenTiksStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume of each market entry in lots.", "Trading");

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

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 30m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (points)", "Trailing distance expressed in price points.", "Risk");

		_maxOrders = Param(nameof(MaxOrders), 1)
			.SetNotNegative()
			.SetDisplay("Max Orders", "Maximum number of simultaneously open entries. Zero disables the limit.", "Trading");

		_usePartialClose = Param(nameof(UsePartialClose), true)
			.SetDisplay("Use Partial Close", "Close half of the position whenever the trailing stop advances.", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe used for pattern detection.", "General");
	}

	/// <summary>
	/// Order volume used for every market entry.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set
		{
			_orderVolume.Value = value;
			Volume = value;
		}
	}

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

	/// <summary>
	/// Trailing stop distance expressed in price points.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Maximum number of simultaneously open entries.
	/// </summary>
	public int MaxOrders
	{
		get => _maxOrders.Value;
		set => _maxOrders.Value = value;
	}

	/// <summary>
	/// Enables progressive partial exits when true.
	/// </summary>
	public bool UsePartialClose
	{
		get => _usePartialClose.Value;
		set => _usePartialClose.Value = value;
	}

	/// <summary>
	/// Candle type requested from the market data feed.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

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

		_priceStep = 0;
		_volumeStep = 0;
		_minVolumeLimit = 0;
		_maxVolumeLimit = 0;
		_high1 = null;
		_high2 = null;
		_high3 = null;
		_open1 = null;
		_open2 = null;
		_open3 = null;
		_longEntryPrice = null;
		_shortEntryPrice = null;
		_longTrailingStop = null;
		_shortTrailingStop = null;
		_dummySma = null;
		_previousPosition = 0m;
		_lastTradePrice = null;
	}

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

		var security = Security;
		_priceStep = security?.PriceStep ?? 1m;
		if (_priceStep <= 0m)
			_priceStep = 1m;

		_volumeStep = security?.VolumeStep ?? 0m;
		_minVolumeLimit = security?.MinVolume ?? 0m;
		_maxVolumeLimit = security?.MaxVolume ?? 0m;

		Volume = NormalizeEntryVolume(OrderVolume);

		_dummySma = new SimpleMovingAverage { Length = 2 };

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

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		_lastTradePrice = trade.Trade?.Price ?? trade.Order.Price;
	}

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		var delta = Position - _previousPosition;

		if (Position > 0m)
		{
			if (_previousPosition <= 0m)
			{
				_longEntryPrice = _lastTradePrice;
				_longTrailingStop = null;
				_shortEntryPrice = null;
				_shortTrailingStop = null;
			}
			else if (delta > 0m && _lastTradePrice is decimal priceLong)
			{
				var previousVolume = Math.Max(0m, _previousPosition);
				var currentVolume = Math.Max(0m, Position);
				if (currentVolume > 0m)
				{
					var currentEntry = _longEntryPrice ?? priceLong;
					_longEntryPrice = (currentEntry * previousVolume + priceLong * delta) / currentVolume;
				}
			}
		}
		else if (Position < 0m)
		{
			if (_previousPosition >= 0m)
			{
				_shortEntryPrice = _lastTradePrice;
				_shortTrailingStop = null;
				_longEntryPrice = null;
				_longTrailingStop = null;
			}
			else if (delta < 0m && _lastTradePrice is decimal priceShort)
			{
				var previousVolume = Math.Max(0m, Math.Abs(_previousPosition));
				var currentVolume = Math.Max(0m, Math.Abs(Position));
				if (currentVolume > 0m)
				{
					var currentEntry = _shortEntryPrice ?? priceShort;
					_shortEntryPrice = (currentEntry * previousVolume + priceShort * Math.Abs(delta)) / currentVolume;
				}
			}
		}
		else
		{
			_longEntryPrice = null;
			_shortEntryPrice = null;
			_longTrailingStop = null;
			_shortTrailingStop = null;
		}

		_previousPosition = Position;
	}

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

		UpdateTrailing(candle);

		var buySignal = false;
		var sellSignal = false;

		if (_high1 is decimal h1 && _high2 is decimal h2 && _high3 is decimal h3 &&
		_open1 is decimal o1 && _open2 is decimal o2 && _open3 is decimal o3)
		{
		var high = candle.HighPrice;
		var open = candle.OpenPrice;

		buySignal = high > h1 && h1 > h2 && h2 > h3 &&
		open > o1 && o1 > o2 && o2 > o3;

		sellSignal = high < h1 && h1 < h2 && h2 < h3 &&
		open < o1 && o1 < o2 && o2 < o3;
		}

		_high3 = _high2;
		_high2 = _high1;
		_high1 = candle.HighPrice;

		_open3 = _open2;
		_open2 = _open1;
		_open1 = candle.OpenPrice;

		if (buySignal)
			TryEnterLong();

		if (sellSignal)
			TryEnterShort();
	}

	private void TryEnterLong()
	{
		if (MaxOrders > 0 && EstimateOrdersCount(Position) >= MaxOrders)
			return;

		var volume = NormalizeEntryVolume(OrderVolume);
		if (volume <= 0m)
			return;

		BuyMarket(volume);
	}

	private void TryEnterShort()
	{
		if (MaxOrders > 0 && EstimateOrdersCount(Position) >= MaxOrders)
			return;

		var volume = NormalizeEntryVolume(OrderVolume);
		if (volume <= 0m)
			return;

		SellMarket(volume);
	}

	private int EstimateOrdersCount(decimal positionVolume)
	{
		var baseVolume = NormalizeEntryVolume(OrderVolume);
		if (baseVolume <= 0m)
			return positionVolume != 0m ? 1 : 0;

		var ratio = Math.Abs(positionVolume) / baseVolume;
		if (ratio <= 0m)
			return 0;

		return (int)Math.Ceiling(ratio);
	}

	private void UpdateTrailing(ICandleMessage candle)
	{
		var close = candle.ClosePrice;
		var low = candle.LowPrice;
		var high = candle.HighPrice;

		var stopDistance = StopLossPoints * _priceStep;
		var trailingDistance = TrailingStopPoints * _priceStep;

		if (Position > 0m && _longEntryPrice is decimal entryLong)
		{
			if (stopDistance > 0m && low <= entryLong - stopDistance)
			{
			SellMarket(Position);
			return;
			}

			if (trailingDistance > 0m && close - entryLong >= trailingDistance)
			{
			var desiredStop = close - trailingDistance;
			if (_longTrailingStop is not decimal currentStop || desiredStop > currentStop)
			{
			_longTrailingStop = desiredStop;
			TryReduceLongPosition();
			}

			if (_longTrailingStop is decimal trailingStop && low <= trailingStop)
			SellMarket(Position);
			}
		}
		else if (Position < 0m && _shortEntryPrice is decimal entryShort)
		{
			var positionVolume = Math.Abs(Position);

			if (stopDistance > 0m && high >= entryShort + stopDistance)
			{
			BuyMarket(positionVolume);
			return;
			}

			if (trailingDistance > 0m && entryShort - close >= trailingDistance)
			{
			var desiredStop = close + trailingDistance;
			if (_shortTrailingStop is not decimal currentStop || desiredStop < currentStop)
			{
			_shortTrailingStop = desiredStop;
			TryReduceShortPosition();
			}

			if (_shortTrailingStop is decimal trailingStop && high >= trailingStop)
			BuyMarket(positionVolume);
			}
		}
	}

	private void TryReduceLongPosition()
	{
		if (!UsePartialClose)
			return;

		if (Position <= 0m)
			return;

		var positionVolume = Position;
		var half = positionVolume / 2m;
		var normalizedHalf = NormalizeExitVolume(half, positionVolume);

		if (_minVolumeLimit > 0m && normalizedHalf < _minVolumeLimit)
		{
			SellMarket(positionVolume);
			return;
		}

		if (normalizedHalf > 0m)
		SellMarket(normalizedHalf);
	}

	private void TryReduceShortPosition()
	{
		if (!UsePartialClose)
			return;

		if (Position >= 0m)
			return;

		var positionVolume = Math.Abs(Position);
		var half = positionVolume / 2m;
		var normalizedHalf = NormalizeExitVolume(half, positionVolume);

		if (_minVolumeLimit > 0m && normalizedHalf < _minVolumeLimit)
		{
			BuyMarket(positionVolume);
			return;
		}

		if (normalizedHalf > 0m)
		BuyMarket(normalizedHalf);
	}

	private decimal NormalizeEntryVolume(decimal volume)
	{
		if (volume <= 0m)
		return 0m;

		if (_volumeStep > 0m)
		{
			var steps = Math.Round(volume / _volumeStep, MidpointRounding.AwayFromZero);
			if (steps <= 0m)
			steps = 1m;
			volume = steps * _volumeStep;
		}

		if (_minVolumeLimit > 0m && volume < _minVolumeLimit)
		volume = _minVolumeLimit;

		if (_maxVolumeLimit > 0m && volume > _maxVolumeLimit)
		volume = _maxVolumeLimit;

		return volume;
	}

	private decimal NormalizeExitVolume(decimal desired, decimal currentPosition)
	{
		if (desired <= 0m || currentPosition <= 0m)
		return 0m;

		var volume = desired;

		if (_volumeStep > 0m)
		{
			var steps = Math.Round(volume / _volumeStep, MidpointRounding.AwayFromZero);
			if (steps <= 0m)
			steps = 1m;
			volume = steps * _volumeStep;
		}

		if (volume > currentPosition)
		volume = currentPosition;

		return volume;
	}
}