GitHub で見る

Basket Close Utility

Overview

The Basket Close Utility strategy mirrors the behaviour of the MetaTrader expert "Basket Close 2". It continuously monitors the floating profit and loss of every open position in the connected portfolio. When either a configurable profit objective or a loss limit is reached, the strategy sends market orders to flatten all exposures in every instrument involved. Optionally, it can automatically open a small test position whenever the book is flat, which is useful inside backtests for validating that the protection logic works as expected.

Parameters

Name Description
LossMode Chooses whether the loss guard compares percentages or currency values.
LossPercentage Negative percentage drawdown (expressed in absolute value) that triggers the loss exit when LossMode is Percentage.
LossCurrency Floating loss in account currency that triggers the exit when LossMode is Currency.
ProfitMode Chooses whether the profit objective compares percentages or currency values.
ProfitPercentage Percentage gain that closes all positions when ProfitMode is Percentage.
ProfitCurrency Floating profit in account currency that closes all positions when ProfitMode is Currency.
CandleType Timeframe used to trigger periodic checks of the floating profit and loss.
EnableTestOrders When enabled the strategy sends a single market buy order whenever no positions are open.
TestOrderVolume Trade size used when the optional test order is active.

Trading Logic

  1. Subscribe to the configured candle series and run the evaluation only when a candle is fully finished, matching the behaviour of the original EA that works on closed bars.
  2. Aggregate the floating profit and loss of every open position. If the portfolio object exposes a combined floating profit it is used; otherwise the strategy sums the PnL of each position.
  3. Compute the percentage change relative to the current account balance captured at start-up.
  4. Trigger the loss routine when the floating PnL breaches the configured limit. Trigger the profit routine when the floating PnL or the percentage gain reaches the profit target.
  5. Once triggered, keep sending market orders until every open position across the entire portfolio is closed. This includes the main security as well as positions opened by child strategies.
  6. Optionally send a market order to reopen exposure (for testing) after the book becomes flat.

Notes

  • The MetaTrader expert displayed textual information on the chart. In StockSharp the important figures are logged through LogInfo instead.
  • Swap and commission adjustments from the original script are implicitly included inside the floating PnL reported by the portfolio or individual positions.
  • The percentage thresholds use the account balance captured when the strategy starts. Adjust the limits when running long sessions if the equity base changes substantially.
  • When the optional test order is enabled, the helper order is reissued whenever the previous exposure has been closed by the profit or loss guard.
namespace StockSharp.Samples.Strategies;

using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;

/// <summary>
/// Basket Close strategy: EMA trend following with profit/loss close thresholds.
/// Enters on EMA direction, closes when accumulated P&L hits target or stop.
/// </summary>
public class BasketCloseStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _emaPeriod;

	private decimal _entryPrice;
	private bool _wasBullish;
	private bool _hasPrevSignal;

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

	public BasketCloseStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");
		_emaPeriod = Param(nameof(EmaPeriod), 50)
			.SetGreaterThanZero()
			.SetDisplay("EMA Period", "EMA period", "Indicators");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_entryPrice = 0m;
		_wasBullish = false;
		_hasPrevSignal = false;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_entryPrice = 0;
		_hasPrevSignal = false;
		var ema = new ExponentialMovingAverage { Length = EmaPeriod };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ema, ProcessCandle).Start();
	}

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

		var close = candle.ClosePrice;
		var isBullish = close > emaValue;

		if (_hasPrevSignal && isBullish != _wasBullish)
		{
			if (isBullish && Position <= 0)
			{
				BuyMarket();
				_entryPrice = close;
			}
			else if (!isBullish && Position >= 0)
			{
				SellMarket();
				_entryPrice = close;
			}
		}

		_wasBullish = isBullish;
		_hasPrevSignal = true;
	}
}