在 GitHub 上查看

篮子平仓工具

概述

篮子平仓工具策略还原了 MetaTrader 专家顾问“Basket Close 2”的逻辑。策略会持续监控投资组合中所有持仓的浮动盈亏,当达到设定的盈利目标或亏损阈值时,它会向涉及的每个品种发送市价单,从而完全平掉所有仓位。为了在回测中验证风控是否正常运作,还可以在账户为空仓时自动下达一笔测试订单。

参数

名称 说明
LossMode 选择按百分比还是按货币金额来判断亏损阈值。
LossPercentage LossModePercentage 时,达到该百分比亏损(取绝对值)便会触发平仓。
LossCurrency LossModeCurrency 时,浮动亏损达到该金额便会触发平仓。
ProfitMode 选择按百分比还是按货币金额来判断盈利目标。
ProfitPercentage ProfitModePercentage 时,浮动盈利达到该百分比即平掉全部仓位。
ProfitCurrency ProfitModeCurrency 时,浮动盈利达到该金额即平掉全部仓位。
CandleType 用于触发周期性检查的K线周期,与原始 EA 在收盘价上运作的方式一致。
EnableTestOrders 启用后,在无持仓时自动发送一笔测试性市价买单。
TestOrderVolume 启用测试订单时所使用的下单手数。

交易逻辑

  1. 订阅指定周期的K线,只在K线收盘后执行检查,以模拟原始 EA 的“收盘触发”机制。
  2. 汇总所有持仓的浮动盈亏。若投资组合对象提供汇总浮盈亏,则直接使用;否则遍历并累加每个持仓的 PnL。
  3. 以策略启动时记录的账户余额为基准,计算浮动盈亏对应的百分比。
  4. 当浮动 PnL 触及亏损阈值时触发亏损流程;当浮动 PnL 或百分比达到盈利目标时触发盈利流程。
  5. 触发后持续发送市价单,直到投资组合中所有品种的持仓全部归零,包括由子策略开立的仓位。
  6. 若启用了测试订单,则在仓位全部平掉后再次发送测试性市价单,以便在回测中反复验证策略。

备注

  • 原始 EA 会在图表上显示文字信息,本策略改为通过 LogInfo 记录关键数据。
  • 原脚本单独累加的掉期与佣金,在 StockSharp 中已经包含在投资组合或持仓报告的浮动盈亏里。
  • 百分比阈值使用策略启动时的账户余额作为基数。若运行时间较长且权益发生较大变化,请适当调整阈值。
  • 启用测试订单后,每当风控机制把仓位平掉,策略都会重新发送测试单,以保持验证流程。
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;
	}
}