在 GitHub 上查看

按账户货币的盈利或亏损平仓

该策略移植自 MetaTrader 专家顾问 Close_on_PROFIT_or_LOSS_inAccont_Currency。它持续监控所连接投资组合的当前权益值,一旦达到设定的盈利目标或跌破亏损底线,就会取消所有挂单并平掉全部持仓。实现基于 StockSharp 的高级 API:蜡烛订阅提供“心跳”,CancelActiveOrders() 撤销挂单,而 ClosePosition() 通过市价单完成平仓。

工作流程

  1. 每当心跳蜡烛收盘时,读取 Portfolio.CurrentValue
  2. 若权益值大于或等于 Positive Closure,触发完整的退出流程。
  3. 若权益值小于或等于 Negative Closure,执行同样的流程以限制亏损。
  4. 在退出过程中,策略会撤销挂单、提交反向市价单并停止自身运行(对应原版中的 ExpertRemove())。

注意: 阈值以账户货币表示。为了避免启动后立即触发,请将 Positive Closure 设置在当前权益之上,并将 Negative Closure 设置在其下方。

参数

名称 说明 默认值
PositiveClosureInAccountCurrency 当权益达到或超过该值时立即平仓。 0
NegativeClosureInAccountCurrency 当权益跌至该值或更低时立即平仓。 0
CandleType 提供心跳的蜡烛周期。若需更快响应,可选择更短的时间框。 1 分钟

说明

  • 启动时会调用 StartProtection(),以保持原策略的防护逻辑。
  • 策略仅处理自身管理的仓位和订单,请将其附加到需要保护的投资组合。
  • 未单独提供点差或滑点参数,因为 StockSharp 的市价单会根据连接器自动处理这些因素。
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.Candles;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Closes all positions when portfolio equity reaches configured profit or loss thresholds.
/// Pending orders are cancelled before liquidating the exposure.
/// </summary>
public class CloseOnProfitOrLossInAccountCurrencyStrategy : Strategy
{
	private readonly StrategyParam<decimal> _positiveClosure;
	private readonly StrategyParam<decimal> _negativeClosure;
	private readonly StrategyParam<DataType> _candleType;

	private bool _closeRequested;
	private SimpleMovingAverage _smaFast;
	private SimpleMovingAverage _smaSlow;

	/// <summary>
	/// Equity level in account currency that triggers closing all positions when exceeded.
	/// </summary>
	public decimal PositiveClosureInAccountCurrency
	{
		get => _positiveClosure.Value;
		set => _positiveClosure.Value = value;
	}

	/// <summary>
	/// Equity level in account currency that triggers closing all positions when reached on drawdown.
	/// </summary>
	public decimal NegativeClosureInAccountCurrency
	{
		get => _negativeClosure.Value;
		set => _negativeClosure.Value = value;
	}

	/// <summary>
	/// Candle type used as a heartbeat to evaluate portfolio equity.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="CloseOnProfitOrLossInAccountCurrencyStrategy"/> class.
	/// </summary>
	public CloseOnProfitOrLossInAccountCurrencyStrategy()
	{
		_positiveClosure = Param(nameof(PositiveClosureInAccountCurrency), 0m)
			.SetDisplay("Positive Closure", "Equity level that triggers full liquidation", "Risk");

		_negativeClosure = Param(nameof(NegativeClosureInAccountCurrency), 0m)
			.SetDisplay("Negative Closure", "Equity floor that forces liquidation", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Heartbeat Candle", "Candle type that triggers equity checks", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		if (Security == null)
			return Array.Empty<(Security, DataType)>();

		return new (Security, DataType)[] { (Security, CandleType) };
	}

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

		if (Portfolio == null)
			throw new InvalidOperationException("Portfolio cannot be null.");

		if (Security == null)
			throw new InvalidOperationException("Security must be set to subscribe for candles.");

		_smaFast = new SimpleMovingAverage { Length = 10 };
		_smaSlow = new SimpleMovingAverage { Length = 30 };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_smaFast, _smaSlow, ProcessCandleWithIndicators)
			.Start();
	}

	private void ProcessCandleWithIndicators(ICandleMessage candle, decimal fast, decimal slow)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (fast > slow && Position <= 0)
		{
			if (Position < 0)
				BuyMarket();
			BuyMarket();
		}
		else if (fast < slow && Position >= 0)
		{
			if (Position > 0)
				SellMarket();
			SellMarket();
		}
	}

	private void RequestCloseAll(string reason)
	{
		if (_closeRequested)
			return;

		_closeRequested = true;

		LogInfo(reason);

		// Cancel any pending orders to avoid unexpected executions during liquidation.
		CancelActiveOrders();

		foreach (var position in Positions.ToArray())
		{
			var value = GetPositionValue(position.Security, Portfolio) ?? 0m;

			if (value == 0m)
				continue;

			// Submit a market order opposite to the current exposure.
			ClosePosition(position.Security);
		}

		// Stop the strategy after sending exit orders, mirroring ExpertRemove behavior.
		Stop();
	}
}