Overall Statistics
Total Orders
19
Average Win
12.10%
Average Loss
-9.58%
Compounding Annual Return
20.033%
Drawdown
32.200%
Expectancy
0.509
Start Equity
100000
End Equity
146332
Net Profit
46.332%
Sharpe Ratio
0.457
Sortino Ratio
0.495
Probabilistic Sharpe Ratio
25.550%
Loss Rate
33%
Win Rate
67%
Profit-Loss Ratio
1.26
Alpha
0.09
Beta
0.28
Annual Standard Deviation
0.26
Annual Variance
0.068
Information Ratio
0.054
Tracking Error
0.275
Treynor Ratio
0.425
Total Fees
$40.85
Estimated Strategy Capacity
$63000000000.00
Lowest Capacity Asset
ES YVXOP65RE0HT
Portfolio Turnover
5.48%
Drawdown Recovery
116
# region imports
from AlgorithmImports import *
from datetime import timedelta
# end region


class ManualFutureRolloverAlgorithm(QCAlgorithm):
    def Initialize(self):
        self.set_start_date(2023, 9, 1)
        self.set_end_date(2025, 9, 30)
        self.set_cash(100000)

        self._future_chain = self.add_future(Futures.Indices.SP_500_E_MINI, Resolution.DAILY)
        self._future_chain.set_filter(self.FutureFilter)
        self.es_symbol = self._future_chain.symbol
        self.active_contract = None
        self.next_rollover_date = None
        self._sma = SimpleMovingAverage(int(self.get_parameter("ma_lookback")))
        # Schedule Rollover Check
        self.schedule.on(
            self.date_rules.every_day(),
            self.time_rules.before_market_close(self.es_symbol, minutes_before_close=30),
            self.RollCheck
        )
        self.set_warm_up(timedelta(days=5))
        self.qty = 1
        self.rollover_liquidated = False
        self.contracts_available = None
    def FutureFilter(self, universe: FutureFilterUniverse):
        # Filter for contracts expiring within 90 days
        
        return universe.expiration_cycle([3,6,9,12])


    def on_data(self, slice: Slice):

        if self.is_warming_up:
            self.log("### Warming Up...")
            return
            # slice.future_chains
        chain = slice.future_chains.get(self.es_symbol)
        if not chain: return
        
        # Get current front contract by earliest expiry
        contracts = sorted([c for c in chain], key=lambda x: x.expiry)
        self.contracts_available = contracts
        if not contracts: return
        
        if self.active_contract is None:
            self.RollCheck()

        bar = slice.bars[self.active_contract]
        self._sma.update(bar.end_time, bar.close)

        if bar.close >= self._sma.current.value and not self.portfolio.invested:
            # Go Long
            if self.rollover_liquidated:
                self.market_order(self.active_contract, quantity=self.qty, tag="Rollover Long")
                self.rollover_liquidated = False
            else:
                self.market_order(self.active_contract, quantity=self.qty, tag="Long")
        elif bar.close <= self._sma.current.value and not self.portfolio.invested:
            # Go Short
            if self.rollover_liquidated:
                self.market_order(self.active_contract, quantity=-self.qty, tag="Rollover Short")
                self.rollover_liquidated = False
            else:
                self.market_order(self.active_contract, quantity=-self.qty, tag="Short")
        



    def RollCheck(self):
        # Roll if within 5 days to expiry
        contracts = self.contracts_available

        if not contracts:
            return
        # self.debug(f"contracts: {contracts}")
        closest_expiring_contract = min(contracts, key=lambda x: x.id.date)
        self.log(f"closest_expiring_contract: {closest_expiring_contract} at time: {self.time}")
        next_contract = sorted(contracts, key=lambda x: x.id.date)[1]
        self.log(f"next_contract: {next_contract} at time: {self.time}")
        if self.active_contract is not None and self.time >= self.next_rollover_date and self.portfolio.invested:
            self.liquidate(self.active_contract, tag="Rollover Liquidate")
            self.log(f"ROLLOVER EVENT: from {self.active_contract} to {next_contract}, date: {self.time}, current expiry: {self.active_contract.expiry}")
            self.rollover_liquidated = True
            self.active_contract = next_contract
            self.next_rollover_date = next_contract.expiry - timedelta(days=5)
        elif self.active_contract is None:
            self.active_contract = closest_expiring_contract
            self.next_rollover_date = closest_expiring_contract.expiry - timedelta(days=5)