Overall Statistics
namespace QuantConnect.Algorithm.CSharp
{
    public class _20201223_AF_es_mean_reverting_A : QCAlgorithm
    {
        // Parameters to optimize in walk-forwards testing
        private int _LookbackDays = 5;
        private int _ExitDays = 3;

        // Other parameters
        private int _DaysBeforeExpiryToRollover = 1;
        private ConsolidatorManager _consolidatorManager;
        
        #region Managed Rollover Fields
        private FuturesContract _activeContract;
        private bool _areFuturesInitialized = false;
        private bool _triggerRollover = false;
        #endregion Managed Rollover Fields

        public override void Initialize()
        {
            SetStartDate(2019, 1, 1);
            SetCash(100000);

            AddFuture(Futures.Indices.SP500EMini, Resolution.Minute)
                .SetFilter(TimeSpan.Zero, TimeSpan.FromDays(240));

            // optional, helps reduce margin calls
            Settings.FreePortfolioValuePercentage = 0.3m;
        }

        private void InitializeFutures(Slice slice)
        {
            // QC API can't initialize futures in Initialize() because slice.FuturesChains only exists in OnData()
            // https://www.quantconnect.com/forum/discussion/10185/how-to-initialize-futures-using-history-in-the-initialize-method/p1/comment-29314

            SetActiveContract(slice);
            ScheduleRolloverTrigger();

            _areFuturesInitialized = true;

            // custom entry/exit ("alpha") logic goes here

            // TODO - History would be nice to use for initialization, but I'm not sure if it works with consolidators?
            // you could make your own consolidator for history, but that won't have a significant impact in backtesting
            // versus just using a warm-up period...

            //var bars = History<TradeBar>(_activeContract.Symbol, TimeSpan.FromMinutes(_LookbackDays), Resolution.Minute);
            //Log($"{bars.Count()}");
            //foreach (var bar in bars)
            //{
            //    Log($"{bar.Time}: HIGH IS {bar.High}, LOW IS {bar.Low}");
            //}

            SetHoldings(_activeContract.Symbol, 1);
            Log($"Futures initialized, new contract purchased: {_activeContract.Symbol.Value}");
        }

        override public void OnData(Slice slice)
        {
            if (!_areFuturesInitialized)
            {
                InitializeFutures(slice);
                return;
            }

            if (_triggerRollover)
                SetActiveContractAndRollover(slice);

            // custom entry/exit ("alpha") logic goes here
            // ...
        }

        public void OnDay(object sender, TradeBar bar)
        {
            Log($"{bar.Time}: Trading contract {bar.Symbol.Value}, Open is {bar.Open}, Close is {bar.Close}");
        }

        #region Managed Futures Rollover
        private void SetActiveContractAndRollover(Slice slice)
        {
            SubscriptionManager.RemoveConsolidator(_activeContract.Symbol, _consolidatorManager.TradeBarConsolidator);
            Log($"Subscription REMOVED from consolidator {nameof(_consolidatorManager.TradeBarConsolidator)} for contract {_activeContract.Symbol.Value}");

            SetActiveContract(slice);
            Rollover();
            _triggerRollover = false;
            ScheduleRolloverTrigger();
        }

        private void SetActiveContract(Slice slice)
        {
            if (slice.FutureChains.Count == 0)
            {
                var exceptionMessage = $"ERROR - No contracts in var {nameof(slice.FutureChains)}";
                Log(exceptionMessage);
                throw new Exception(exceptionMessage);
            }

            var futureChain = slice.FutureChains.First();
            var recentContracts = futureChain.Value
                .Where(x => (x.Expiry - Time.Date).TotalDays > _DaysBeforeExpiryToRollover + 1)
                .OrderBy(x => (x.Expiry - Time.Date).TotalDays)
                .ToList();

            if (recentContracts.Count == 0)
            {
                _activeContract = null;
                Liquidate();

                var exceptionMessage = $"ERROR - No contracts in var {nameof(recentContracts)}, no active contract assigned, no scheduled rollover. Liquidating portfolio.";
                Log(exceptionMessage);
                throw new Exception(exceptionMessage);
            }
            Log($"list of sorted recent contracts: {string.Join(", ", recentContracts.Select(x => x.Symbol.Value).ToList())}");

            var frontContract = recentContracts.First();
            _activeContract = frontContract;

            Log($"New active contract set to {_activeContract.Symbol.Value}");

            // custom entry/exit ("alpha") logic goes here
            // using a QC Consolidator to get Daily bars since Futures can't use Resolution.Daily
            _consolidatorManager = new ConsolidatorManager();
            _consolidatorManager.TradeBarConsolidator = new TradeBarConsolidator(TimeSpan.FromDays(1));
            _consolidatorManager.TradeBarConsolidator.DataConsolidated += OnDay;

            SubscriptionManager.AddConsolidator(_activeContract.Symbol, _consolidatorManager.TradeBarConsolidator);
            Log($"Subscription ADDED for consolidator {nameof(_consolidatorManager.TradeBarConsolidator)} for contract {_activeContract.Symbol.Value}");
        }

        private void Rollover()
        {
            if (Portfolio.Invested)
            {
                Liquidate();
                Log($"Portfolio liquidated.");

                SetHoldings(_activeContract.Symbol, 1);
                Log($"New contract purchased: {_activeContract.Symbol.Value}");
            }
        }

        private void ScheduleRolloverTrigger()
        {
            Action callback = () => { _triggerRollover = true; };

            Schedule.On(DateRules.On(_activeContract.Expiry.AddDays(-1 * _DaysBeforeExpiryToRollover)),
                TimeRules.BeforeMarketClose(_activeContract.Symbol, 120),
                callback);

            Log($"Contract {_activeContract.Symbol.Value} has will have rollover triggered on: {_activeContract.Expiry.AddDays(-1 * _DaysBeforeExpiryToRollover).ToShortDateString()}");
        }
        #endregion Managed Futures Rollover
    }

    public class ConsolidatorManager
    {
        public TradeBarConsolidator TradeBarConsolidator;
    }
}