Overall Statistics
Total Orders
2
Average Win
0%
Average Loss
-0.42%
Compounding Annual Return
-1.258%
Drawdown
1.700%
Expectancy
-1
Start Equity
1000000
End Equity
995817.53
Net Profit
-0.418%
Sharpe Ratio
-2.975
Sortino Ratio
-3.734
Probabilistic Sharpe Ratio
15.931%
Loss Rate
100%
Win Rate
0%
Profit-Loss Ratio
0
Alpha
-0.062
Beta
0.033
Annual Standard Deviation
0.019
Annual Variance
0
Information Ratio
-1.723
Tracking Error
0.127
Treynor Ratio
-1.707
Total Fees
$2.47
Estimated Strategy Capacity
$1500000000.00
Lowest Capacity Asset
CL Y7S8YM5PTSJL
Portfolio Turnover
0.13%
from AlgorithmImports import *
from datetime import timedelta
import numpy as np

class DeltaNeutralOilStrategy(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2023, 1, 1)
        self.SetEndDate(2023, 5, 1)
        self.SetCash(1_000_000)
        
        self.futureSymbol = None
        self.optionSymbol = None
        self.priceHistory = {}
        self.calculatedDelta = None
        self.requiredOptionContracts = None
        
        self.future = self.AddFuture(Futures.Energies.CrudeOilWTI)
        self.future.SetFilter(timedelta(85), timedelta(95))
        
        self.AddFutureOption(self.future.Symbol, self.FilterOptions)
        
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(10, 0), self.CalculateDelta)
    
    def FilterOptions(self, universe):
        return universe.Strikes(-5, 5).Expiration(timedelta(85), timedelta(95))
    
    def OnData(self, slice):
        if slice.OptionChains:
            self.Log(f"Option Chains Available: {[symbol for symbol in slice.OptionChains.keys()]}")
        
        if self.futureSymbol is None and slice.FutureChains:
            for chain in slice.FutureChains.Values:
                contracts = [c for c in chain.Contracts.Values if c.Expiry > self.Time + timedelta(90)]
                if contracts:
                    contract = sorted(contracts, key=lambda x: x.Expiry)[0]
                    self.futureSymbol = contract.Symbol
                    
                    # long 1 futures contract
                    self.MarketOrder(self.futureSymbol, 1)
                    self.Log(f"Entered long position on future: {self.futureSymbol}, Expiry: {contract.Expiry}")
                    break
        
        if self.futureSymbol is not None and self.optionSymbol is None and slice.OptionChains:
            future_price = self.Securities[self.futureSymbol].Price
            
            for symbol, chain in slice.OptionChains.items():
                if "CL" in str(symbol):  #crude oil symbols = CL
                    atm_options = sorted(chain, key=lambda x: abs(x.Strike - future_price))
                    
                    if atm_options:
                        atm_option = atm_options[0]
                        self.optionSymbol = atm_option.Symbol
                        
                        self.priceHistory = {
                            'option': {
                                'price': atm_option.LastPrice if atm_option.LastPrice > 0 else (atm_option.BidPrice + atm_option.AskPrice)/2,
                                'time': self.Time
                            },
                            'future': {
                                'price': future_price,
                                'time': self.Time
                            }
                        }
                        
                        self.Log(f"chosen ATM option: {self.optionSymbol}, Strike: {atm_option.Strike}, Type: {atm_option.Right}")
                        break
        
        #update price history for delta finding
        if self.futureSymbol is not None and self.optionSymbol is not None:
            if self.futureSymbol in slice.Bars:
                self.priceHistory['future'] = {
                    'price': slice.Bars[self.futureSymbol].Close,
                    'time': self.Time
                }
            
            if self.optionSymbol in slice.Bars:
                self.priceHistory['option'] = {
                    'price': slice.Bars[self.optionSymbol].Close,
                    'time': self.Time
                }
                
            #try option chain
            if slice.OptionChains:
                for _, chain in slice.OptionChains.items():
                    for contract in chain:
                        if contract.Symbol == self.optionSymbol:
                            self.priceHistory['option'] = {
                                'price': contract.LastPrice if contract.LastPrice > 0 else (contract.BidPrice + contract.AskPrice)/2,
                                'time': self.Time
                            }
                            break
    
    def CalculateDelta(self):
        if not self.optionSymbol or not self.futureSymbol:
            self.Log("Option or future symbol not set. Skipping delta calculation!!")
            return
            
        option_history = self.History([self.optionSymbol], 5, Resolution.Daily)
        future_history = self.History([self.futureSymbol], 5, Resolution.Daily)
        
        if option_history.empty or future_history.empty:
            self.Log("Not enough price history for delta calculation yet")
            return
            
        option_prices = option_history['close'].values
        future_prices = future_history['close'].values
        
        if len(option_prices) < 2 or len(future_prices) < 2:
            self.Log("insufficient data points for delta calculation")
            return
            
        #find price changes
        option_changes = np.diff(option_prices)
        future_changes = np.diff(future_prices)
        
        # find delta
        deltas = []
        for i in range(min(len(option_changes), len(future_changes))):
            if abs(future_changes[i]) > 0.001: 
                delta = option_changes[i] / future_changes[i]
                deltas.append(delta)
        
        if deltas:
            ##reduce outlier impact w median
            estimated_delta = np.median(deltas)
            self.calculatedDelta = estimated_delta
            
            ###for delta neutral: 1 (future delta) + num_options * option_delta = 0, so num_options = -1 / option_delta
            if abs(estimated_delta) > 0.001:  # Avoid division by very small values
                self.requiredOptionContracts = abs(round(-1 / estimated_delta))
                
                self.Log(f"Calculated Option Delta: {estimated_delta:.4f}")
                self.Log(f"For Delta-neutral portfolio: {self.requiredOptionContracts} option contracts needed")
    
    def OnEndOfAlgorithm(self):
        self.Log("==== Delta-Neutral Strategy Analysis ====")
        
        if self.futureSymbol:
            self.Log(f"Futures Contract: {self.futureSymbol}")
            self.Log(f"Position: LONG 1 contract (Delta = +1)")
        
        if self.optionSymbol and self.calculatedDelta:
            self.Log(f"Option Contract: {self.optionSymbol}")
            self.Log(f"Calculated Option Delta: {self.calculatedDelta:.4f}")
            
            if self.requiredOptionContracts:
                self.Log(f"For a delta-neutral portfolio:")
                self.Log(f"- Position type: {'LONG' if self.calculatedDelta < 0 else 'SHORT'} {self.requiredOptionContracts} option contracts")
                self.Log(f"- Portfolio Delta: +1 (future) + {self.requiredOptionContracts} × {self.calculatedDelta:.4f} (options) ≈ 0")
            
        self.Log("====================================")