GitHub で見る

Carbophos グリッド戦略

概要

Carbophos グリッド戦略は、MetaTrader 5エキスパートアドバイザー「Carbophos」の直接変換です。現在のbid/ask価格周辺に買いと売りの指値注文の対称的な梯子を継続的に維持します。戦略はグリッド全体の集計された浮動利益を監視し、望ましい利益目標またはt最大許容ドローダウンに達したときにすべてのエクスポージャーを閉じます。ポジションが平坦化され、有効な注文が残っていない後、梯子は自動的に再構築されます。

取引ロジック

  1. 戦略が開始し、有効な注文やオープンポジションがない場合、設定されたpipsのステップと銘柄の価格精度に基づいて価格単位でグリッドの間隔を計算します。5つ(設定可能)の売り指値注文が最良bidの上に置かれ、同数の買い指値注文が最良askの下に置かれます。
  2. どの注文が約定されると、結果として生じるポジションはLevel1データを使用してティックごとに監視されます。浮動PnLは現在の決済価格(ロングポジションのbid、ショートポジションのask)とボリューム加重平均エントリー価格の差から計算されます。
  3. 浮動利益が設定された目標を超えるか、または浮動損失が保護しきい値を違反すると、戦略はオープンポジションを閉じるための成行注文を発行し、残りの指値注文をすべてキャンセルします。状態フラグは次の価格更新で梯子が再構築されるようにクリアされます。
  4. すべての注文が約定されるが純ポジションがゼロに戻る場合(例えば、市場がグリッドを通じて反転するため)、次のLevel1更新が新しい梯子の配置をトリガーします。

パラメーター

パラメーター 説明
ProfitTarget グリッド全体の閉鎖をトリガーする浮動利益(金額)。
MaxLoss 緊急決済を強制する浮動損失(金額)。
StepPips pipsで表された連続するグリッドレベル間の距離。シンボルのティックサイズと小数点精度を使用して内部で価格単位に変換されます。
OrdersPerSide 現在の市場価格の上下に配置される指値注文の数。
OrderVolume 各グリッド注文のボリューム。

すべてのパラメーターはStockSharpオプティマイザーでの実験を簡素化するための最適化範囲をサポートしています。

リスク管理と保護

戦略は組み込みのStartProtection()フックを使用し、戦略レベルでハードな金額ストップ/プロフィットレベルを適用します。浮動PnL計算は銘柄のPriceStepStepPriceの設定に依存します。どちらかのしきい値に達すると、戦略は成行注文でポジションを閉じ、内部グリッドフラグをリセットする前に有効な指値注文をすべてキャンセルします。

変換メモ

  • 元のMQL5エキスパートアドバイザーは3桁と5桁のFX銘柄のpip値を調整しました。StockSharpポートは、銘柄が3または5桁の小数を持つときに取引所PriceStepを10倍することでこの動作を再現します。
  • MetaTraderはマジックナンバーごとにポジション利益、手数料、スワップを集計します。StockSharpでは浮動PnLは加重平均エントリー価格と現在のbid/ask価格から再計算されるため、明示的な手数料処理は必要ありません。
  • 注文の配置、キャンセル、ポジション管理はプロジェクトガイドラインで要求される高レベルStrategyAPI(BuyLimitSellLimitCancelActiveOrdersBuyMarketSellMarket)を通じて実装されます。
  • グリッドはLevel1更新からのみ更新され、カスタムタイマーやコレクションを導入せずに元のコードの「OnTick」動作を再現します。

使用方法

  1. 起動する前に戦略インスタンスに目的のSecurityPortfolioを割り当てます。
  2. オプションでパラメーターを目的の銘柄のボラティリティとリスク許容度に合わせて調整します。
  3. 戦略を起動します。すぐにLevel1データをサブスクライブし、bidとask価格の両方が利用可能になったら最初のグリッドを構築し、自動的にエクスポージャーを管理し続けます。
  4. グリッドがリセットされたときを知るために「Profit target reached」や「Maximum loss reached」などのメッセージのログを監視します。

選択した銘柄が最良bidとask価格を含むLevel1更新を提供することを確認してください; そうでなければ梯子は構築されません。

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>
/// Grid strategy converted from the Carbophos MetaTrader 5 expert advisor.
/// Simulates symmetric grid levels and manages profit and loss on the aggregated position.
/// </summary>
public class CarbophosGridStrategy : Strategy
{
	private readonly StrategyParam<decimal> _profitTarget;
	private readonly StrategyParam<decimal> _maxLoss;
	private readonly StrategyParam<int> _stepPips;
	private readonly StrategyParam<int> _ordersPerSide;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _entryPrice;
	private decimal _gridCenterPrice;
	private bool _gridPlaced;
	private int _cooldownRemaining;

	private readonly List<decimal> _buyLevels = new();
	private readonly List<decimal> _sellLevels = new();

	/// <summary>
	/// Floating profit level (in absolute price * volume) that triggers closing of all positions.
	/// </summary>
	public decimal ProfitTarget
	{
		get => _profitTarget.Value;
		set => _profitTarget.Value = value;
	}

	/// <summary>
	/// Maximum allowed floating loss before the grid is closed.
	/// </summary>
	public decimal MaxLoss
	{
		get => _maxLoss.Value;
		set => _maxLoss.Value = value;
	}

	/// <summary>
	/// Distance between grid levels expressed in pips.
	/// </summary>
	public int StepPips
	{
		get => _stepPips.Value;
		set => _stepPips.Value = value;
	}

	/// <summary>
	/// Number of limit orders to place above and below the market price.
	/// </summary>
	public int OrdersPerSide
	{
		get => _ordersPerSide.Value;
		set => _ordersPerSide.Value = value;
	}

	/// <summary>
	/// Volume for each grid level order.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Candle type used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="CarbophosGridStrategy"/>.
	/// </summary>
	public CarbophosGridStrategy()
	{
		_profitTarget = Param(nameof(ProfitTarget), 500m)
			.SetGreaterThanZero()
			.SetDisplay("Profit Target", "Floating profit target in money", "Risk")
			.SetOptimize(100m, 1000m, 50m);

		_maxLoss = Param(nameof(MaxLoss), 100m)
			.SetGreaterThanZero()
			.SetDisplay("Max Loss", "Maximum floating loss before closing", "Risk")
			.SetOptimize(50m, 500m, 25m);

		_stepPips = Param(nameof(StepPips), 2000)
			.SetGreaterThanZero()
			.SetDisplay("Step (pips)", "Distance between grid levels in pips", "Grid")
			.SetOptimize(10, 150, 10);

		_ordersPerSide = Param(nameof(OrdersPerSide), 1)
			.SetGreaterThanZero()
			.SetDisplay("Orders Per Side", "Number of pending orders on each side", "Grid")
			.SetOptimize(1, 10, 1);

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume for each pending order", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to use", "General");
	}

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

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

		_entryPrice = null;
		_gridCenterPrice = 0m;
		_gridPlaced = false;
		_cooldownRemaining = 0;
		_buyLevels.Clear();
		_sellLevels.Clear();
	}

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

		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;

		var currentPrice = candle.ClosePrice;

		// Check if any grid levels were hit by this candle
		CheckGridFills(candle);

		// Check profit/loss on position
		if (Position != 0 && _entryPrice is decimal entry)
		{
			var floatingPnL = (currentPrice - entry) * Position;

			if (floatingPnL >= ProfitTarget)
			{
				CloseAll("Profit target reached.");
				return;
			}

			if (floatingPnL <= -MaxLoss)
			{
				CloseAll("Maximum loss reached.");
				return;
			}
		}

		// Cooldown after closing
		if (_cooldownRemaining > 0)
		{
			_cooldownRemaining--;
			return;
		}

		// Place grid if none is active
		if (!_gridPlaced || (Position == 0 && _buyLevels.Count == 0 && _sellLevels.Count == 0))
		{
			PlaceGrid(currentPrice);
		}
	}

	private void PlaceGrid(decimal centerPrice)
	{
		_buyLevels.Clear();
		_sellLevels.Clear();

		var stepSize = GetGridStep();
		if (stepSize <= 0m || centerPrice <= 0m)
			return;

		for (var i = 1; i <= OrdersPerSide; i++)
		{
			var offset = stepSize * i;
			var buyPrice = centerPrice - offset;
			var sellPrice = centerPrice + offset;

			if (buyPrice > 0m)
				_buyLevels.Add(buyPrice);

			_sellLevels.Add(sellPrice);
		}

		_gridCenterPrice = centerPrice;
		_gridPlaced = true;
	}

	private void CheckGridFills(ICandleMessage candle)
	{
		// Check buy levels (price goes down to the level)
		for (var i = _buyLevels.Count - 1; i >= 0; i--)
		{
			if (i >= _buyLevels.Count) continue;
			if (candle.LowPrice <= _buyLevels[i])
			{
				var level = _buyLevels[i];
				BuyMarket();
				UpdateEntryPrice(level, OrderVolume, true);
				try { _buyLevels.RemoveAt(i); } catch { }
			}
		}

		// Check sell levels (price goes up to the level)
		for (var i = _sellLevels.Count - 1; i >= 0; i--)
		{
			if (i >= _sellLevels.Count) continue;
			if (candle.HighPrice >= _sellLevels[i])
			{
				var level = _sellLevels[i];
				SellMarket();
				UpdateEntryPrice(level, OrderVolume, false);
				try { _sellLevels.RemoveAt(i); } catch { }
			}
		}
	}

	private void UpdateEntryPrice(decimal fillPrice, decimal volume, bool isBuy)
	{
		if (_entryPrice is not decimal existingEntry || Position == 0)
		{
			_entryPrice = fillPrice;
			return;
		}

		// Weighted average entry price calculation
		var existingPos = Position;
		var newPos = isBuy ? existingPos + volume : existingPos - volume;

		if (newPos == 0)
		{
			_entryPrice = null;
			return;
		}

		// Only update if adding to position in same direction
		if ((isBuy && existingPos > 0) || (!isBuy && existingPos < 0))
		{
			var totalVolume = Math.Abs(existingPos) + volume;
			_entryPrice = (existingEntry * Math.Abs(existingPos) + fillPrice * volume) / totalVolume;
		}
		else
		{
			// Reducing position - keep same entry price
			if (Math.Abs(newPos) > 0)
				_entryPrice = existingEntry;
			else
				_entryPrice = null;
		}
	}

	private void CloseAll(string reason)
	{
		if (Position > 0)
			SellMarket();
		else if (Position < 0)
			BuyMarket();

		_buyLevels.Clear();
		_sellLevels.Clear();
		_gridPlaced = false;
		_entryPrice = null;
		_cooldownRemaining = 10;

		LogInfo(reason);
	}

	private decimal GetGridStep()
	{
		var security = Security;

		var priceStep = security?.PriceStep ?? 0m;
		if (priceStep <= 0m)
			priceStep = 0.01m;

		var decimals = security?.Decimals ?? 2;
		var multiplier = (decimals == 3 || decimals == 5) ? 10m : 1m;
		return StepPips * priceStep * multiplier;
	}
}