在 GitHub 上查看

多品种利润平仓策略

概述

多品种利润平仓策略 重现了原始的 MetaTrader 脚本:持续监控一组货币对,在组合浮动利润达到目标或总浮亏超过容忍范围时立即平掉所有仓位。该转换基于 StockSharp 的高级 API,实现了利润跟踪、持仓最小时间控制以及跨多个标的的集中平仓。

策略流程

  1. 从逗号分隔的 WatchedSymbols 参数解析需要监控的标的,若留空则退回到主 Security
  2. 为每个标的订阅所选的 K 线类型(默认 1 分钟)。每根收盘的 K 线都会触发一次组合利润检查。
  3. 对于每个标的,策略保存:
    • Positions[i].PnL 提供的当前浮动盈亏。
    • 首次持仓出现的时间,用于满足 MinAgeSeconds 的最小持仓时长。
  4. 累计全部标的的净利润后执行判断:
    • 净利润达到 ProfitTarget 时,调用 BuyMarket / SellMarket 平掉所有达到最小时长的仓位。
    • 净利润跌破 -MaxLoss 时,视为风险控制,同样执行集中平仓。
  5. 日志输出会列出每个标的的盈亏及组合总盈亏,方便对比原脚本的 Comment 信息。

参数

参数 说明 默认值
WatchedSymbols 需要监控的证券 ID 列表(逗号分隔)。为空时使用主 Security "GBPUSD,USDCAD,USDCHF,USDSEK"
ProfitTarget 触发平仓的组合净利润(账户货币)。 60
MaxLoss 触发保护性平仓的最大可承受亏损(账户货币)。 60
Slippage 与原脚本一致的滑点设置。由于使用市价单平仓,该参数仅作信息展示。 10
MinAgeSeconds 允许平仓前的最小持仓秒数。 60
CandleType 用于定期检查的 K 线类型(默认 1 分钟)。 1 minute

注意事项

  • 策略直接使用 StockSharp Positions 集合提供的盈亏数据,无需额外的成交历史。
  • 如果策略启动时已经有持仓,则以启动时刻作为首次观察时间,需等待 MinAgeSeconds 后才会被强制平仓。
  • 所有退出均通过市价单完成,以确保尽快离场;Slippage 参数主要用于保留原脚本配置。
  • 在日志中可看到每个标的的盈亏以及组合净值,便于实时监控。

使用要求

  • 需要一个可解析 WatchedSymbols 中标的的 SecurityProvider 或连接器。
  • 请确保每个标的的下单数量设置合理,使得平仓市价单可以完全对冲当前仓位。
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>
/// Closes the current position when floating PnL reaches a profit target or maximum loss.
/// Simplified from the multi-pair closer utility to work with a single security.
/// </summary>
public class MultiPairCloserStrategy : Strategy
{
	private readonly StrategyParam<decimal> _profitTarget;
	private readonly StrategyParam<decimal> _maxLoss;
	private readonly StrategyParam<int> _minAgeSeconds;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _smaPeriod;

	private SimpleMovingAverage _sma;
	private decimal _entryPrice;
	private DateTimeOffset? _entryTime;

	/// <summary>
	/// Profit target in price units.
	/// </summary>
	public decimal ProfitTarget
	{
		get => _profitTarget.Value;
		set => _profitTarget.Value = value;
	}

	/// <summary>
	/// Maximum tolerated loss in price units.
	/// </summary>
	public decimal MaxLoss
	{
		get => _maxLoss.Value;
		set => _maxLoss.Value = value;
	}

	/// <summary>
	/// Minimum age of an open position in seconds before exit is permitted.
	/// </summary>
	public int MinAgeSeconds
	{
		get => _minAgeSeconds.Value;
		set => _minAgeSeconds.Value = value;
	}

	/// <summary>
	/// Candle type for price monitoring.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// SMA period for entry signals.
	/// </summary>
	public int SmaPeriod
	{
		get => _smaPeriod.Value;
		set => _smaPeriod.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public MultiPairCloserStrategy()
	{
		_profitTarget = Param(nameof(ProfitTarget), 5m)
			.SetNotNegative()
			.SetDisplay("Profit Target", "Close position when floating profit reaches this value", "Risk Management");

		_maxLoss = Param(nameof(MaxLoss), 10m)
			.SetNotNegative()
			.SetDisplay("Maximum Loss", "Close position when floating loss reaches this value", "Risk Management");

		_minAgeSeconds = Param(nameof(MinAgeSeconds), 60)
			.SetNotNegative()
			.SetDisplay("Min Age (s)", "Minimum holding time before exit is allowed", "Execution");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Candle series for monitoring", "General");

		_smaPeriod = Param(nameof(SmaPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("SMA Period", "Moving average period for entry signal", "Indicators");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_sma = null;
		_entryPrice = 0m;
		_entryTime = null;
	}

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

		_sma = new SimpleMovingAverage { Length = SmaPeriod };

		SubscribeCandles(CandleType)
			.Bind(_sma, ProcessCandle)
			.Start();
	}

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

		if (!IsFormed)
			return;

		var price = candle.ClosePrice;
		var time = candle.CloseTime;

		// Check exit conditions for open position
		if (Position != 0 && _entryPrice > 0m)
		{
			var pnl = Position > 0
				? price - _entryPrice
				: _entryPrice - price;

			var canClose = MinAgeSeconds <= 0 ||
				(_entryTime.HasValue && (time - _entryTime.Value).TotalSeconds >= MinAgeSeconds);

			if (canClose)
			{
				if ((ProfitTarget > 0m && pnl >= ProfitTarget) ||
					(MaxLoss > 0m && pnl <= -MaxLoss))
				{
					if (Position > 0)
						SellMarket(Math.Abs(Position));
					else
						BuyMarket(Math.Abs(Position));

					_entryPrice = 0m;
					_entryTime = null;
					return;
				}
			}
		}

		// Entry logic: trend following with SMA
		if (Position == 0)
		{
			if (price > smaValue)
			{
				BuyMarket();
				_entryPrice = price;
				_entryTime = time;
			}
			else if (price < smaValue)
			{
				SellMarket();
				_entryPrice = price;
				_entryTime = time;
			}
		}
	}
}