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:
Identify the currently tradable contract for each futures family based on a short code (for example MXI or BR).
Open or adjust the position so that the actual portfolio volume matches the configured lot value (positive = long, negative = short).
Monitor the expiry time on every finished candle of a heartbeat subscription.
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
Ensure the SecurityProvider knows all future symbols for the selected families. Configure BoardCode if your data source requires identifiers like Si-9.23@FORTS.
Start the strategy with the desired portfolio parameters. Positions are opened only when the strategy is online and trading is allowed.
The strategy logs every assignment, adjustment, and roll event. Use these messages to verify the mapping between short codes and actual futures.
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));
}
}
}
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()