GitHub で見る

Amstell グリッド・マネージャー戦略

双方向の平均化グリッドを実行するMetaTraderエキスパート「exp_Amstell-SL」の高レベルポートです。戦略は各サイドの最新の約定価格を追跡し、価格が十分に離れたときに追加の成行注文を発し、固定のテイクプロフィットまたはストップロス距離に達したときにオープンバッチを清算します。実装はStockSharpのローソク足サブスクリプションと高レベル注文ヘルパーを使用しているため、単一の銘柄のローソク足データを提供する任意の環境に接続できます。

翻訳されたロジックはStockSharpのネットポートフォリオモデルに若干適応されています。ロングとショートのグリッドは依然として別々に管理されますが、同時には保持されません。ロンググリッドはネットポジションが非負の間アクティブであり、すべてのロングエクスポージャーが平坦化された後にのみショートグリッドが引き継ぎます。

動作方法

マーケットデータと実行フロー

  • 設定された CandleType(デフォルト: 1分足ローソク足)をサブスクライブし、完成したローソク足のみを処理します。
  • 銘柄の PriceStep からpipsベースのオフセットを計算します。ステップが3桁または5桁の場合、MetaTraderの3/5桁pip調整を模倣するために10倍されます。
  • すべての取引は BuyMarket/SellMarket ヘルパーを通じて配置されます。ペンディング注文は使用しません。

ロングサイドの管理

  • 既存のロングエクスポージャーがなく、戦略がショートを閉じる最中でない場合に最初のロングポジション(OrderVolume)を開きます。
  • 最新のロング約定価格とアクティブなロングバッチの出来高加重平均エントリー価格を追跡します。
  • 終値が最後のロング約定価格より少なくとも BuyDistancePips(価格単位に変換)下落した場合、常に OrderVolume サイズの追加ロング注文を発します。

ショートサイドの管理

  • ロングバッチが完全に閉じられ、ネットポジションが非正になったら、戦略はショートエントリーを許可します。
  • ショートエクスポージャーがない場合に最初のショート注文を発します。価格が前のショート約定価格より BuyDistancePips * SellDistanceMultiplier 上昇した後に追加のショートが開かれます。
  • アクティブなショートバッチの最新のショート約定価格と出来高加重平均エントリー価格を維持します。

エグジットルール

  • 各方向について、平均約定に対する未実現利益を計算します。
  • 利益が TakeProfitPips pipsに達するか、ドローダウンが StopLossPips pipsに達したときに、成行売りでロングバッチ全体を閉じます。
  • 利益が TakeProfitPips pipsに達するか、不利な動きが StopLossPips pipsに達したときに、成行買いでショートバッチ全体を閉じます。
  • 清算後、キャッシュされたすべての価格とボリュームがリセットされ、次のローソク足で新しいグリッドを開始できます。

オリジナルMQLエキスパートとの違い

  • StockSharpバージョンは個別のティックではなくローソク足のクローズで動作します。
  • ロングとショートのグリッドはStockSharpのデフォルトネッティングモードに合わせて、同時ではなく順次実行されます。
  • すべての保護距離は各チケットを個別にではなく平均エントリー価格に対して確認され、集計されたネットポジションの動作を反映します。

パラメーター

パラメーター デフォルト 最適化範囲 説明
OrderVolume 0.01 0.010.10 (ステップ 0.01) 各グリッド注文で送信される数量。正である必要があります。
TakeProfitPips 30 10150 (ステップ 10) アクティブバッチの利益目標(pips単位)。
StopLossPips 30 10150 (ステップ 10) バッチを諦める前の最大不利な動き。
BuyDistancePips 10 560 (ステップ 5) 別の買いを追加するための最後のロング約定からの最小下落。TPとSLの両方より小さい必要があります。
SellDistanceMultiplier 10 215 (ステップ 1) ショートエントリーを間隔を置く際のロング距離に適用される乗数。
CandleType 1分足時間軸 シグナル生成に使用するローソク足シリーズ。

実装に関する注意事項

  • BuyDistancePipsTakeProfitPipsStopLossPips の両方より厳密に小さくなければなりません。そうでない場合、戦略は起動時に例外をスローし、MetaTraderの検証を再現します。
  • pip サイズは銘柄の PriceStep から導出されます。楽器が非標準のティックサイズを使用している場合はパラメーターを調整してください。
  • すべての内部状態は OnReseted でクリアされ、残りのグリッドデータなしに戦略を再起動できます。
  • カラーカスタマイズや手動のインジケーター登録は使用されず、このリポジトリの高レベルAPIガイドラインに準拠しています。
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>
/// Amstell averaging grid strategy that opens new entries when price drifts away
/// from the last fill and closes exposure once profit or loss thresholds are reached.
/// </summary>
public class AmstellGridManagerStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _buyDistancePips;
	private readonly StrategyParam<decimal> _sellDistanceMultiplier;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _longVolume;
	private decimal _shortVolume;
	private decimal? _averageLongPrice;
	private decimal? _averageShortPrice;
	private decimal? _lastBuyPrice;
	private decimal? _lastSellPrice;
	private decimal _pipValue;
	private decimal _takeProfitOffset;
	private decimal _stopLossOffset;
	private decimal _buyDistanceOffset;
	private decimal _sellDistanceOffset;
	private bool _closingLong;
	private bool _closingShort;

	/// <summary>
	/// Quantity per market order.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Profit target in pips for each grid leg.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Maximum tolerated loss in pips for each grid leg.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Price distance in pips required to add another long position.
	/// </summary>
	public int BuyDistancePips
	{
		get => _buyDistancePips.Value;
		set => _buyDistancePips.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the long distance when stacking short entries.
	/// </summary>
	public decimal SellDistanceMultiplier
	{
		get => _sellDistanceMultiplier.Value;
		set => _sellDistanceMultiplier.Value = value;
	}

	/// <summary>
	/// Candle data type used for decision making.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes the strategy parameters.
	/// </summary>
	public AmstellGridManagerStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Quantity submitted with each grid order", "Trading")
			
			.SetOptimize(0.01m, 0.1m, 0.01m);

		_takeProfitPips = Param(nameof(TakeProfitPips), 30)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk")
			
			.SetOptimize(10, 150, 10);

		_stopLossPips = Param(nameof(StopLossPips), 30)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk")
			
			.SetOptimize(10, 150, 10);

		_buyDistancePips = Param(nameof(BuyDistancePips), 10)
			.SetGreaterThanZero()
			.SetDisplay("Buy Distance (pips)", "Distance before adding another long", "Entries")
			
			.SetOptimize(5, 60, 5);

		_sellDistanceMultiplier = Param(nameof(SellDistanceMultiplier), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Sell Distance Multiplier", "Multiplier applied to long distance when adding shorts", "Entries")
			
			.SetOptimize(2m, 15m, 1m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for processing", "General");
	}

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

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

		_longVolume = 0m;
		_shortVolume = 0m;
		_averageLongPrice = null;
		_averageShortPrice = null;
		_lastBuyPrice = null;
		_lastSellPrice = null;
		_pipValue = 0m;
		_takeProfitOffset = 0m;
		_stopLossOffset = 0m;
		_buyDistanceOffset = 0m;
		_sellDistanceOffset = 0m;
		_closingLong = false;
		_closingShort = false;
	}

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

		if (BuyDistancePips >= TakeProfitPips || BuyDistancePips >= StopLossPips)
			throw new InvalidOperationException("Buy distance must be less than take profit and stop loss distances.");

		UpdatePriceOffsets();

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

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

		// no indicators bound via .Bind()

		var close = candle.ClosePrice;

		if (!_closingLong && _longVolume > 0m && _averageLongPrice is decimal longAvg)
		{
			var profit = close - longAvg;
			if (profit >= _takeProfitOffset || -profit >= _stopLossOffset)
			{
				SellMarket();
				_closingLong = true;
				return;
			}
		}

		if (!_closingShort && _shortVolume > 0m && _averageShortPrice is decimal shortAvg)
		{
			var profit = shortAvg - close;
			if (profit >= _takeProfitOffset || -profit >= _stopLossOffset)
			{
				BuyMarket();
				_closingShort = true;
				return;
			}
		}

		var openedLong = false;

		if (!_closingLong && Position >= 0m)
		{
			if (_longVolume <= 0m)
			{
				BuyMarket();
				openedLong = true;
			}
			else if (_lastBuyPrice is decimal lastBuy && lastBuy - close >= _buyDistanceOffset)
			{
				BuyMarket();
				openedLong = true;
			}
		}

		if (openedLong)
			return;

		if (!_closingShort && Position <= 0m)
		{
			if (_shortVolume <= 0m)
			{
				SellMarket();
			}
			else if (_lastSellPrice is decimal lastSell && close - lastSell >= _sellDistanceOffset)
			{
				SellMarket();
			}
		}
	}

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

		if (trade.Order == null)
			return;

		var tradeVolume = trade.Trade.Volume;
		var price = trade.Trade.Price;

		if (trade.Order.Side == Sides.Buy)
		{
			if (_shortVolume > 0m)
			{
				var closingVolume = Math.Min(tradeVolume, _shortVolume);
				_shortVolume -= closingVolume;
				tradeVolume -= closingVolume;
				if (_shortVolume <= 0m)
				{
					_shortVolume = 0m;
					_averageShortPrice = null;
					_lastSellPrice = null;
				}
			}

			if (tradeVolume > 0m)
			{
				var newVolume = _longVolume + tradeVolume;
				var totalCost = (_averageLongPrice ?? 0m) * _longVolume + price * tradeVolume;
				_longVolume = newVolume;
				_averageLongPrice = totalCost / newVolume;
				_lastBuyPrice = price;
				_closingLong = false;
			}
		}
		else if (trade.Order.Side == Sides.Sell)
		{
			if (_longVolume > 0m)
			{
				var closingVolume = Math.Min(tradeVolume, _longVolume);
				_longVolume -= closingVolume;
				tradeVolume -= closingVolume;
				if (_longVolume <= 0m)
				{
					_longVolume = 0m;
					_averageLongPrice = null;
					_lastBuyPrice = null;
				}
			}

			if (tradeVolume > 0m)
			{
				var newVolume = _shortVolume + tradeVolume;
				var totalCost = (_averageShortPrice ?? 0m) * _shortVolume + price * tradeVolume;
				_shortVolume = newVolume;
				_averageShortPrice = totalCost / newVolume;
				_lastSellPrice = price;
				_closingShort = false;
			}
		}

		if (_longVolume <= 0m && Position <= 0m)
			_closingLong = false;

		if (_shortVolume <= 0m && Position >= 0m)
			_closingShort = false;

		if (Position == 0m)
		{
			_longVolume = 0m;
			_shortVolume = 0m;
			_averageLongPrice = null;
			_averageShortPrice = null;
			_lastBuyPrice = null;
			_lastSellPrice = null;
			_closingLong = false;
			_closingShort = false;
		}
	}

	private void UpdatePriceOffsets()
	{
		var step = Security?.PriceStep ?? 1m;
		if (step <= 0m)
			step = 1m;

		var decimals = GetDecimalPlaces(step);
		_pipValue = decimals == 3 || decimals == 5 ? step * 10m : step;

		_takeProfitOffset = TakeProfitPips * _pipValue;
		_stopLossOffset = StopLossPips * _pipValue;
		_buyDistanceOffset = BuyDistancePips * _pipValue;
		_sellDistanceOffset = _buyDistanceOffset * SellDistanceMultiplier;
	}

	private static int GetDecimalPlaces(decimal value)
	{
		if (value == 0m)
			return 0;

		var bits = decimal.GetBits(value);
		return (bits[3] >> 16) & 0x7F;
	}
}