| 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("====================================")