Overall Statistics
Total Orders
10403
Average Win
0.70%
Average Loss
-0.73%
Compounding Annual Return
-20.728%
Drawdown
91.600%
Expectancy
-0.055
Start Equity
10000000
End Equity
902861.02
Net Profit
-90.971%
Sharpe Ratio
-0.767
Sortino Ratio
-0.854
Probabilistic Sharpe Ratio
0.000%
Loss Rate
52%
Win Rate
48%
Profit-Loss Ratio
0.96
Alpha
-0.172
Beta
0.26
Annual Standard Deviation
0.201
Annual Variance
0.04
Information Ratio
-0.994
Tracking Error
0.226
Treynor Ratio
-0.591
Total Fees
$962687.53
Estimated Strategy Capacity
$370000000.00
Lowest Capacity Asset
CL YUBLBNKSVGSH
Portfolio Turnover
267.01%
# region imports
from AlgorithmImports import *
from futures_roll_model import FuturesRollModel
# endregion

class DefaultRollFuturesModel(FuturesRollModel):

  def _find_near_contract(
    self, 
    futures_symbol: Symbol, 
    min_expiry: int,
    contract_offset: int,
    ) -> Symbol|None:

    contract_symbols: List[Symbol] = self._algo.future_chain_provider.get_future_contract_list(
      futures_symbol, self._algo.time + timedelta(days=min_expiry)
    )

    if len(contract_symbols) < contract_offset + 1:
      self._algo.log(f'Not enough contracts found for {futures_symbol}')
      return None

    return sorted(contract_symbols, key=lambda symbol: symbol.id.date)[contract_offset]

  def _execute(self) -> None:
    if self._algo.portfolio.invested:
      self._algo.liquidate()

    near_contract: None|Symbol = self._find_near_contract(
      self._futures_symbol, 
      self._min_expiry, 
      self._contract_offset
    )
    
    if near_contract is not None:
      self._open_position(near_contract)
      self._algo.log(f'Selected contract for {self._futures_symbol}: {near_contract.value} (expiry: {near_contract.id.date})')
    else:
      self._algo.log(f'Could not find contract for {self._futures_symbol}')
  
  def _open_position(self, contract_symbol: Symbol) -> None:
    self._algo.add_future_contract(contract_symbol, Resolution.MINUTE)

    if self._algo.securities[contract_symbol].get_last_data():
        notional_value: float = self._algo.securities[contract_symbol].get_last_data().price * self._algo.securities[contract_symbol].symbol_properties.contract_multiplier
        quantity: int = self._algo.portfolio.total_portfolio_value // notional_value
        self._algo.market_order(contract_symbol, self._trade_direction * quantity)

  def _close_position(self, contract_symbol: Symbol) -> None:
    self._algo.remove_security(contract_symbol)
# region imports
from AlgorithmImports import *
from abc import abstractmethod, ABC
# endregion

class FuturesRollModel(ABC):
  def __init__(
    self, 
    algo, 
    futures_symbol: Symbol, 
    min_expiry: int,
    contract_offset: int,
    trade_direction: int) -> None:

    self._algo = algo
    self._min_expiry = min_expiry
    self._contract_offset: int = contract_offset
    self._trade_direction: int = trade_direction

    self._futures_symbol: Future = futures_symbol
  
  @abstractmethod
  def _find_near_contract(
    self, 
    futures_symbol: Symbol, 
    min_expiry: int,
    contract_offset: int) -> Symbol|None:
    pass

  @abstractmethod
  def _execute(self) -> None:
    pass
# https://quantpedia.com/strategies/intraday-drift-in-crude-oil-price/
# 
# The investment universe for this strategy includes Brent Crude Oil futures contracts, specifically the F1 (front-month) and F3 (third-month) contracts. 
# The approach also considers reinvestment opportunities in the S&P 500 Index (we selected that) and 3-month Treasury Bills.
# (Raw data consisted of tick-by-tick, transaction-level observations of all trades carried out on the Exchange (not just oil), post-processed by authors.)
# Short Summary: The strategy uses historical data analysis to identify intra-day seasonal patterns and price anomalies in the Brent Crude Oil futures market. 
# (The methodology involves entering long or short positions based on deviations from expected seasonal patterns.)
# Strategy Execution (strategy version shorting futures, not spread CL1 vs. CL3):
# Short (sell) CL 3-month out contract at 11:00 a.m.
# Cover (buy) CL 3-month out contract at 4:00 p.m.
# Weighting & Rebalancing: Traded intra-daily, profits re-invested in asset tracking S&P 500 Index (for example, ETFs SPY or VOO, or CFD possibly).
# 
# Implementation changes:
#   - Position cover is done at 5:00 p.m. 
#   - CL3 contract is used as a trading asset.

# region imports
from AlgorithmImports import *
from default_futures_roll_model import DefaultRollFuturesModel
from typing import Dict, Tuple
# endregion

class FinecoFuturesRolling(QCAlgorithm):

    def initialize(self) -> None:
        self.set_start_date(2015, 1, 1)
        self.set_cash(10_000_000)

        cover_hour: int = 17
        execute_hour: int = 11
        contract_offset: int = 2
        min_expiry: int = 2

        futures: Dict[str, Tuple] = {
            # Tuple (market, contract offset, trade direction)
            Futures.Energy.CRUDE_OIL_WTI: (Market.NYMEX, contract_offset, -1),
            Futures.Indices.SP_500_E_MINI: (Market.CME, 0, 1)
        }

        self._futures_models: Dict[str, DefaultRollFuturesModel] = {
            f : DefaultRollFuturesModel(
                self,
                Symbol.create(f, SecurityType.FUTURE, market),
                min_expiry,
                contract_offset,
                trade_direction     
            ) for f, (market, contract_offset, trade_direction) in futures.items()
        }

        seeder = FuncSecuritySeeder(self.get_last_known_prices)
        self.set_security_initializer(BrokerageModelSecurityInitializer(self.brokerage_model, seeder))

        market: Symbol = self.add_equity('SPY', Resolution.MINUTE).symbol

        self.schedule.on(
            self.date_rules.every_day(market),
            self.time_rules.at(execute_hour, 0),
            lambda: self._futures_models[Futures.Energy.CRUDE_OIL_WTI]._execute()
        )

        self.schedule.on(
            self.date_rules.every_day(market),
            self.time_rules.at(cover_hour, 0),
            lambda: self._futures_models[Futures.Indices.SP_500_E_MINI]._execute()
        )

    def on_data(self, slice: Slice) -> None:
        pass