Overall Statistics
Total Orders
7
Average Win
34.02%
Average Loss
-14.52%
Compounding Annual Return
-0.397%
Drawdown
44.500%
Expectancy
0.114
Start Equity
100000
End Equity
99682.8
Net Profit
-0.317%
Sharpe Ratio
0.129
Sortino Ratio
0.138
Probabilistic Sharpe Ratio
20.639%
Loss Rate
67%
Win Rate
33%
Profit-Loss Ratio
2.34
Alpha
0.146
Beta
-0.998
Annual Standard Deviation
0.471
Annual Variance
0.221
Information Ratio
-0.043
Tracking Error
0.558
Treynor Ratio
-0.061
Total Fees
$15.05
Estimated Strategy Capacity
$51000000000.00
Lowest Capacity Asset
ES YYFADOG4CO3L
Portfolio Turnover
8.44%
Drawdown Recovery
43
# region imports
from AlgorithmImports import *
from datetime import timedelta
# end region


class ManualFutureRolloverAlgorithm(QCAlgorithm):
    def Initialize(self):
        self.set_start_date(2025, 1, 1)
        self.set_end_date(2025, 12, 10)
        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(50)
        # 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)