Ver en GitHub

Futures Portfolio Control Expiration Strategy

Overview

This strategy rebuilds the MetaTrader 5 expert advisor Futures Portfolio Control Expiration on top of the StockSharp high-level API. It maintains a three-leg futures portfolio, keeps the desired long/short exposure for each leg, and automatically rolls every contract to the next expiry when the remaining lifetime drops below a configurable threshold.

The implementation replicates the original workflow:

  1. Identify the currently tradable contract for each futures family based on a short code (for example MXI or BR).
  2. Open or adjust the position so that the actual portfolio volume matches the configured lot value (positive = long, negative = short).
  3. Monitor the expiry time on every finished candle of a heartbeat subscription.
  4. Close the expiring contract, discover the next expiry in the same family, and recreate the target exposure on the new contract.

Parameters

Name Description Default
BoardCode Exchange board appended to futures identifiers (for example FORTS). Leave empty if the provider does not require a board suffix. FORTS
Symbol1, Symbol2, Symbol3 Short codes of the three futures families. The strategy iterates future expiries by constructing identifiers like CODE-M.YY. MXI, BR, SBRF
Lot1, Lot2, Lot3 Target position size per leg. Positive values create long exposure, negative values create short exposure. -4, -1, 5
HoursBeforeExpiration Number of hours before contract expiration when the roll should start. 25
MonitoringCandleType Candle type used only as a heartbeat to trigger expiration checks (for example hourly candles). 1H timeframe

Rolling and position management

  • Contract discovery. For each leg the strategy scans up to twelve consecutive calendar months. It tries multiple identifier formats (CODE-M.YY, CODE-MM.YY, CODEMMYY, CODEMYY) and optionally appends the configured BoardCode. Only securities with an expiration date later than the reference time are eligible.
  • Heartbeat updates. A candle subscription on each active contract provides a finished-candle callback that re-evaluates expiration timers and synchronises the portfolio exposure.
  • Rolling logic. When the remaining lifetime is less than or equal to HoursBeforeExpiration, the strategy closes any open position on the current contract, locates the next future with a later expiration, re-subscribes to heartbeat candles, and restores the target lot on the new contract.
  • Position synchronisation. After every heartbeat the actual position is compared against the target lot. The strategy increases or decreases exposure with market orders so that the live position always matches the requested volume (including zero).

Usage notes

  1. Ensure the SecurityProvider knows all future symbols for the selected families. Configure BoardCode if your data source requires identifiers like Si-9.23@FORTS.
  2. Start the strategy with the desired portfolio parameters. Positions are opened only when the strategy is online and trading is allowed.
  3. The strategy logs every assignment, adjustment, and roll event. Use these messages to verify the mapping between short codes and actual futures.
  4. Because the heartbeat subscription is only a timer, you can choose any candle type that is consistently available for the traded instruments.

Implementation details

  • High-level API components (SubscribeCandles, StrategyParam, BuyMarket/SellMarket) keep the code concise and adhere to the project guidelines.
  • No custom collections of historical data are stored; the strategy only works with the latest candle event and the position state.
  • English comments inside the code describe every important step for easier maintenance.
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));
		}
	}
}