| Overall Statistics |
|
Total Orders 91 Average Win 0.09% Average Loss -0.02% Compounding Annual Return 216.910% Drawdown 2.900% Expectancy 3.930 Start Equity 2000000 End Equity 2110382.76 Net Profit 5.519% Sharpe Ratio 3.039 Sortino Ratio 4.445 Probabilistic Sharpe Ratio 63.844% Loss Rate 24% Win Rate 76% Profit-Loss Ratio 5.53 Alpha 0.941 Beta -0.887 Annual Standard Deviation 0.171 Annual Variance 0.029 Information Ratio 0.131 Tracking Error 0.318 Treynor Ratio -0.585 Total Fees $195.91 Estimated Strategy Capacity $14000.00 Lowest Capacity Asset QQQ XUERCY466YXY|QQQ RIWIV7K5Z9LX Portfolio Turnover 26.76% |
from AlgorithmImports import *
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
class DeltaHedgingStrategy(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2021, 12, 1)
self.SetEndDate(2021, 12, 17)
self.SetCash(2000000)
self.qqq = self.AddEquity("QQQ", Resolution.Hour)
self.qqq_option = self.AddOption("QQQ", Resolution.Hour)
self.qqq_option.SetFilter(self.OptionFilterFunc)
self.InitializeStrategyVariables()
self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(9, 30), self.UpdateHistoricalVolatility)
self.CalculateInitialHistoricalVolatility()
self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(16, 0), self.LogDailySummary)
def InitializeStrategyVariables(self):
## option position tracking
self.option_symbol = None
self.option_strike = 0
self.is_position_open = False
self.position_type = None
self.option_position_quantity = 0
## hedging parameters
self.last_delta_hedge_time = datetime.min
self.hedge_frequency = timedelta(hours=1)
self.option_expiry = datetime(2021, 12, 17)
self.start_trading_time = datetime(2021, 12, 1, 10, 0, 0)
##risk management parameters
self.max_capital_usage = 0.05
self.max_margin_usage = 0.70
self.margin_buffer = 0.30
## volatility tracking
self.historical_volatility = 0
# PnL tracking
self.daily_pnl_start = 0
self.today_date = None
def OptionFilterFunc(self, universe):
return (universe
.Expiration(timedelta(0), self.option_expiry - self.Time)
.Strikes(-10, 10)
.CallsOnly())
def CalculateInitialHistoricalVolatility(self):
hist_start = self.Time - timedelta(days=40)
hist_end = self.Time - timedelta(days=1)
history = self.History(self.qqq.Symbol, hist_start, hist_end, Resolution.Daily)
if not history.empty and len(history) >= 25:
closes = history['close'].values[-25:]
log_returns = np.diff(np.log(closes))
self.historical_volatility = np.std(log_returns) * np.sqrt(252)
def UpdateHistoricalVolatility(self):
end_date = self.Time.date() - timedelta(days=1)
start_date = end_date - timedelta(days=40)
history = self.History(self.qqq.Symbol, start_date, end_date, Resolution.Daily)
if len(history) >= 25:
closes = history['close'].values[-25:]
log_returns = np.diff(np.log(closes))
self.historical_volatility = np.std(log_returns) * np.sqrt(252)
self.daily_pnl_start = self.Portfolio.TotalPortfolioValue
self.today_date = self.Time.date()
def SelectATMOption(self, chain):
underlying_price = chain.Underlying.Price
call_options = [contract for contract in chain if
contract.Right == OptionRight.Call and
contract.Strike >= underlying_price and
contract.Expiry.date() == self.option_expiry.date()]
if not call_options:
return None
call_options.sort(key=lambda x: x.Strike)
return call_options[0]
def CalculateSafePositionSize(self, option_price):
underlying_price = self.Securities[self.qqq.Symbol].Price
estimated_margin_per_contract = (underlying_price * 0.2 + option_price) * 100
available_capital = self.Portfolio.Cash * self.max_capital_usage
max_contracts_by_capital = int(available_capital / option_price / 100)
available_margin = self.Portfolio.Cash * self.max_margin_usage
max_contracts_by_margin = int(available_margin / estimated_margin_per_contract)
return max(1, min(max_contracts_by_capital, max_contracts_by_margin) // 2)
def OpenOptionPosition(self, atm_call):
option_price = (atm_call.BidPrice + atm_call.AskPrice) / 2
self.option_position_quantity = self.CalculateSafePositionSize(option_price)
self.option_symbol = atm_call.Symbol
self.option_strike = atm_call.Strike
implied_vol = atm_call.ImpliedVolatility
if implied_vol > self.historical_volatility:
self.position_type = "short"
self.Sell(self.option_symbol, self.option_position_quantity)
else:
self.position_type = "long"
self.Buy(self.option_symbol, self.option_position_quantity)
self.is_position_open = True
def CalculateSafeDeltaHedgeSize(self, delta, target_shares):
available_cash = self.Portfolio.Cash
underlying_price = self.Securities[self.qqq.Symbol].Price
hedge_cost = abs(target_shares) * underlying_price
if hedge_cost > available_cash * self.margin_buffer:
safe_shares = int((available_cash * self.margin_buffer) / underlying_price)
return safe_shares if target_shares > 0 else -safe_shares
return target_shares
def PerformDeltaHedge(self, option_contract):
try:
delta = option_contract.Greeks.Delta
if self.position_type == "long":
target_shares = -delta * self.option_position_quantity * 100
else:
target_shares = -delta * self.option_position_quantity * 100
current_shares = self.Portfolio[self.qqq.Symbol].Quantity
shares_to_trade = int(target_shares - current_shares)
safe_shares_to_trade = self.CalculateSafeDeltaHedgeSize(delta, shares_to_trade)
if abs(safe_shares_to_trade) > 0:
if safe_shares_to_trade > 0:
self.Buy(self.qqq.Symbol, abs(safe_shares_to_trade))
else:
self.Sell(self.qqq.Symbol, abs(safe_shares_to_trade))
self.last_delta_hedge_time = self.Time
except Exception:
pass
def CheckMarginStatus(self):
margin_used = self.Portfolio.TotalMarginUsed
margin_remaining = self.Portfolio.MarginRemaining
margin_total = margin_used + margin_remaining if margin_used + margin_remaining > 0 else self.Portfolio.Cash
if margin_remaining < self.margin_buffer * margin_total:
reduction_factor = 3
if self.position_type == "long":
qty_to_reduce = self.option_position_quantity // reduction_factor
if qty_to_reduce > 0:
self.Sell(self.option_symbol, qty_to_reduce)
self.option_position_quantity -= qty_to_reduce
else:
qty_to_reduce = self.option_position_quantity // reduction_factor
if qty_to_reduce > 0:
self.Buy(self.option_symbol, qty_to_reduce)
self.option_position_quantity -= qty_to_reduce
def LogDailySummary(self):
if self.today_date is None:
return
daily_pnl = self.Portfolio.TotalPortfolioValue - self.daily_pnl_start
total_pnl = self.Portfolio.TotalProfit
self.Debug(f"Date: {self.today_date} | Daily PnL: ${daily_pnl:,.2f} | Total PnL: ${total_pnl:,.2f}")
self.daily_pnl_start = self.Portfolio.TotalPortfolioValue
def OnData(self, slice):
if self.Time < self.start_trading_time:
return
if slice.OptionChains.Count == 0:
return
chain = list(slice.OptionChains.Values)[0]
if not chain:
return
if not self.is_position_open:
atm_call = self.SelectATMOption(chain)
if atm_call:
self.OpenOptionPosition(atm_call)
return
if (self.Time - self.last_delta_hedge_time) >= self.hedge_frequency:
option_contract = next((contract for contract in chain
if contract.Symbol == self.option_symbol), None)
if option_contract is None:
return
self.CheckMarginStatus()
self.PerformDeltaHedge(option_contract)
def OnEndOfAlgorithm(self):
self.Debug(f"Final PnL: ${self.Portfolio.TotalProfit:,.2f}")