在 GitHub 上查看

期货投资组合到期管理策略

概述

该策略基于 StockSharp 高级 API 重现 MetaTrader 5 专家顾问 Futures Portfolio Control Expiration 的全部逻辑。它同时维护三个期货腿的持仓,保证每个腿都保持所需的多/空头寸,并且在合约剩余寿命低于预设阈值时自动将持仓滚动到下一到期合约。

实现流程如下:

  1. 根据配置的短代码(例如 MXIBR)识别当前可交易的具体期货合约。
  2. 将实际持仓调整到目标手数(正数=做多,负数=做空)。
  3. 通过订阅的节奏蜡烛,在每根完结蜡烛上检查距离到期时间。
  4. 关闭即将到期的合约,搜索同一品种的下一期货合约,并在新合约上恢复目标仓位。

参数

参数 说明 默认值
BoardCode 添加在期货标识符后的交易所代码,例如 FORTS。如果数据源不要求交易所后缀,可留空。 FORTS
Symbol1Symbol2Symbol3 三个期货品种的短代码。策略会按 代码-月.年 等格式依次尝试未来的合约。 MXIBRSBRF
Lot1Lot2Lot3 每个腿的目标手数,正数表示做多,负数表示做空。 -4-15
HoursBeforeExpiration 合约剩余多少小时开始执行滚动。 25
MonitoringCandleType 仅作为心跳使用的蜡烛类型(例如 1 小时蜡烛),用于触发检查。 1 小时周期

滚动与持仓控制

  • 合约发现。 每个腿都会向前扫描最多 12 个月,尝试多种可能的代码格式(CODE-M.YYCODE-MM.YYCODEMMYYCODEMYY),并在需要时附加 BoardCode。只有到期日晚于参考时间的合约才会被采纳。
  • 心跳更新。 对每个活跃合约订阅的蜡烛在收盘时触发回调,用于重新评估到期计时并校准仓位。
  • 滚动逻辑。 当剩余寿命小于或等于 HoursBeforeExpiration 时,策略会平掉当前合约的仓位,查找下一到期的合约,重新订阅心跳蜡烛,并在新合约上恢复目标手数。
  • 仓位同步。 每次心跳后都会比较真实仓位与目标手数的差异,通过市价单增加或减少头寸,使实际仓位始终等于目标值(包括为零的情况)。

使用建议

  1. 确保 SecurityProvider 已加载所有需要的期货合约。如果供应商使用 Si-9.23@FORTS 这样的格式,需要正确设置 BoardCode
  2. 以期望的参数启动策略。只有在策略在线且允许交易时才会下单。
  3. 所有合约映射、仓位调整和滚动事件都会写入日志,可用来核对短代码与真实合约的对应关系。
  4. 心跳蜡烛仅用于计时,可根据数据可用性选择任意稳定的时间框架。

实现要点

  • 代码完全使用高层 API(SubscribeCandlesStrategyParamBuyMarket/SellMarket),符合项目编码规范。
  • 不会存储自定义历史集合,只依赖最新蜡烛事件和当前仓位状态。
  • 源码中的注释全部为英文,方便跨团队维护和审阅。
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>
/// Monitors position and rebalances to maintain a target exposure.
/// Simplified from the multi-leg futures portfolio controller to single security.
/// </summary>
public class FuturesPortfolioControlExpirationStrategy : Strategy
{
	private readonly StrategyParam<int> _targetPosition;
	private readonly StrategyParam<int> _rebalancePeriod;
	private readonly StrategyParam<DataType> _candleType;

	private SimpleMovingAverage _sma;
	private int _barCount;

	/// <summary>
	/// Target position size. Positive for long, negative for short.
	/// </summary>
	public int TargetPosition
	{
		get => _targetPosition.Value;
		set => _targetPosition.Value = value;
	}

	/// <summary>
	/// Number of bars between rebalance checks.
	/// </summary>
	public int RebalancePeriod
	{
		get => _rebalancePeriod.Value;
		set => _rebalancePeriod.Value = value;
	}

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

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public FuturesPortfolioControlExpirationStrategy()
	{
		_targetPosition = Param(nameof(TargetPosition), 1)
			.SetDisplay("Target Position", "Desired position size (positive=long, negative=short)", "Portfolio");

		_rebalancePeriod = Param(nameof(RebalancePeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("Rebalance Period", "Number of bars between rebalance checks", "General");

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

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

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

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

		_sma = new SimpleMovingAverage { Length = 20 };

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

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

		if (!IsFormed)
			return;

		_barCount++;

		var price = candle.ClosePrice;
		var target = (decimal)TargetPosition;

		// Rebalance: ensure position matches target
		if (_barCount % RebalancePeriod == 0)
		{
			var current = Position;
			var diff = target - current;

			if (diff > 0)
				BuyMarket(Math.Abs(diff));
			else if (diff < 0)
				SellMarket(Math.Abs(diff));
		}

		// Trend reversal exit and re-entry
		if (Position > 0 && price < smaValue)
		{
			SellMarket(Math.Abs(Position));
		}
		else if (Position < 0 && price > smaValue)
		{
			BuyMarket(Math.Abs(Position));
		}
		else if (Position == 0)
		{
			if (target > 0 && price > smaValue)
				BuyMarket(Math.Abs(target));
			else if (target < 0 && price < smaValue)
				SellMarket(Math.Abs(target));
			else if (target > 0)
				BuyMarket(Math.Abs(target));
			else if (target < 0)
				SellMarket(Math.Abs(target));
		}
	}
}