Стратегия управления фьючерсным портфелем перед экспирацией
Описание
Стратегия воссоздаёт советник MetaTrader 5 Futures Portfolio Control Expiration с использованием высокоуровневого API StockSharp. Она поддерживает три фьючерсные ноги портфеля, удерживает заданный объём по каждой ноге и автоматически переводит позицию на следующий контракт, когда до экспирации текущего остаётся меньше заданного количества часов.
Рабочий цикл выглядит следующим образом:
По короткому коду (например, MXI, BR) определяется актуальный торгуемый контракт соответствующего семейства.
Фактическая позиция синхронизируется с целевым объёмом (положительное значение — длинная позиция, отрицательное — короткая).
На каждой завершённой свече «сердцебиения» проверяется время до экспирации.
Если контракт скоро истекает, позиция закрывается, ищется следующий контракт того же семейства, и целевой объём восстанавливается уже на нём.
Параметры
Параметр
Назначение
Значение по умолчанию
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, текущая позиция закрывается, находится следующий контракт с более поздней датой, выполняется переподписка и восстанавливается целевой объём на новом контракте.
Синхронизация позиций. После каждого «технического» тика фактическая позиция сравнивается с целевой, и при необходимости стратегия увеличивает или уменьшает объём рыночными ордерами (в том числе до нуля).
Рекомендации по использованию
Проверьте, что SecurityProvider содержит все фьючерсные серии выбранных семейств. Если используются идентификаторы вида Si-9.23@FORTS, укажите корректный BoardCode.
Запускайте стратегию с желаемыми параметрами. Торговые команды отправляются только тогда, когда стратегия в онлайне и торговля разрешена.
Все назначения контрактов, корректировки позиций и роллы протоколируются в логах — используйте их для контроля соответствия коротких кодов реальным инструментам.
Так как подписка на свечи нужна только для тайминга, можно выбирать любой надёжно доступный таймфрейм.
Особенности реализации
Используются высокоуровневые методы (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));
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class futures_portfolio_control_expiration_strategy(Strategy):
"""
Futures Portfolio Control Expiration: rebalances to target position
at regular intervals with SMA trend reversal exits.
"""
def __init__(self):
super(futures_portfolio_control_expiration_strategy, self).__init__()
self._target_position = self.Param("TargetPosition", 1) \
.SetDisplay("Target Position", "Desired position size (positive=long, negative=short)", "Portfolio")
self._rebalance_period = self.Param("RebalancePeriod", 10) \
.SetDisplay("Rebalance Period", "Number of bars between rebalance checks", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Candle series for monitoring", "General")
self._bar_count = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(futures_portfolio_control_expiration_strategy, self).OnReseted()
self._bar_count = 0
def OnStarted2(self, time):
super(futures_portfolio_control_expiration_strategy, self).OnStarted2(time)
sma = SimpleMovingAverage()
sma.Length = 20
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(sma, self._process_candle).Start()
def _process_candle(self, candle, sma_value):
if candle.State != CandleStates.Finished:
return
if not self.IsFormed:
return
self._bar_count += 1
price = float(candle.ClosePrice)
sma = float(sma_value)
target = self._target_position.Value
# Rebalance at intervals
if self._bar_count % self._rebalance_period.Value == 0:
current = self.Position
diff = target - current
if diff > 0:
self.BuyMarket()
elif diff < 0:
self.SellMarket()
# Trend reversal exit
if self.Position > 0 and price < sma:
self.SellMarket()
elif self.Position < 0 and price > sma:
self.BuyMarket()
elif self.Position == 0:
if target > 0 and price > sma:
self.BuyMarket()
elif target < 0 and price < sma:
self.SellMarket()
elif target > 0:
self.BuyMarket()
elif target < 0:
self.SellMarket()
def CreateClone(self):
return futures_portfolio_control_expiration_strategy()