Открыть на GitHub

Стратегия управления фьючерсным портфелем перед экспирацией

Описание

Стратегия воссоздаёт советник MetaTrader 5 Futures Portfolio Control Expiration с использованием высокоуровневого API StockSharp. Она поддерживает три фьючерсные ноги портфеля, удерживает заданный объём по каждой ноге и автоматически переводит позицию на следующий контракт, когда до экспирации текущего остаётся меньше заданного количества часов.

Рабочий цикл выглядит следующим образом:

  1. По короткому коду (например, MXI, BR) определяется актуальный торгуемый контракт соответствующего семейства.
  2. Фактическая позиция синхронизируется с целевым объёмом (положительное значение — длинная позиция, отрицательное — короткая).
  3. На каждой завершённой свече «сердцебиения» проверяется время до экспирации.
  4. Если контракт скоро истекает, позиция закрывается, ищется следующий контракт того же семейства, и целевой объём восстанавливается уже на нём.

Параметры

Параметр Назначение Значение по умолчанию
BoardCode Код биржевой площадки, добавляемый к идентификатору фьючерса (например, FORTS). Оставьте пустым, если провайдер не требует суффикса. FORTS
Symbol1, Symbol2, Symbol3 Короткие коды трёх семейств фьючерсов. Стратегия перебирает будущие экспирации, формируя идентификаторы вида CODE-M.YY. MXI, BR, SBRF
Lot1, Lot2, Lot3 Целевой объём по каждой ноге. Положительные значения формируют длинную позицию, отрицательные — короткую. -4, -1, 5
HoursBeforeExpiration За сколько часов до экспирации начинать процесс ролловера. 25
MonitoringCandleType Тип свечей, используемых как таймер для проверки экспирации (например, часовые свечи). Таймфрейм 1 час

Роллирование и контроль позиций

  • Поиск контрактов. Для каждой ноги просматривается до 12 будущих месяцев. Используются несколько форматов кода (CODE-M.YY, CODE-MM.YY, CODEMMYY, CODEMYY) с возможным добавлением BoardCode. В расчёт берутся только контракты, у которых дата экспирации позже текущего времени.
  • Сердцебиение. Подписка на свечи активного контракта служит триггером для проверки времени до экспирации и выравнивания объёма позиции.
  • Алгоритм ролла. Когда до экспирации остаётся меньше или равно HoursBeforeExpiration, текущая позиция закрывается, находится следующий контракт с более поздней датой, выполняется переподписка и восстанавливается целевой объём на новом контракте.
  • Синхронизация позиций. После каждого «технического» тика фактическая позиция сравнивается с целевой, и при необходимости стратегия увеличивает или уменьшает объём рыночными ордерами (в том числе до нуля).

Рекомендации по использованию

  1. Проверьте, что SecurityProvider содержит все фьючерсные серии выбранных семейств. Если используются идентификаторы вида Si-9.23@FORTS, укажите корректный BoardCode.
  2. Запускайте стратегию с желаемыми параметрами. Торговые команды отправляются только тогда, когда стратегия в онлайне и торговля разрешена.
  3. Все назначения контрактов, корректировки позиций и роллы протоколируются в логах — используйте их для контроля соответствия коротких кодов реальным инструментам.
  4. Так как подписка на свечи нужна только для тайминга, можно выбирать любой надёжно доступный таймфрейм.

Особенности реализации

  • Используются высокоуровневые методы (SubscribeCandles, StrategyParam, BuyMarket/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));
		}
	}
}