Overall Statistics
Total Trades
18
Average Win
3.39%
Average Loss
-1.17%
Compounding Annual Return
19.040%
Drawdown
15.100%
Expectancy
-0.025
Net Profit
9.025%
Sharpe Ratio
0.676
Probabilistic Sharpe Ratio
37.860%
Loss Rate
75%
Win Rate
25%
Profit-Loss Ratio
2.90
Alpha
-0.063
Beta
2.013
Annual Standard Deviation
0.239
Annual Variance
0.057
Information Ratio
0.281
Tracking Error
0.177
Treynor Ratio
0.08
Total Fees
$62.75
Estimated Strategy Capacity
$48000.00
Lowest Capacity Asset
QQQ VRQOXO2FK7L2|QQQ RIWIV7K5Z9LX
Portfolio Turnover
1.11%
# region imports
from AlgorithmImports import *
# endregion

import re
class TestCallSell(QCAlgorithm):

    def Initialize(self):
        # set start/end date for backtest
        self.SetStartDate(2013, 12, 1)
        self.SetEndDate(2014, 5, 31)
        # set starting balance for backtest
        self.SetCash(100000)
        # add the underlying asset
        self.equity = self.AddEquity("QQQ", Resolution.Daily)
        self.equity.SetDataNormalizationMode(DataNormalizationMode.Raw)
        self.symbol = self.equity.Symbol
        self.callContract = str()
        self.contractsAdded = set()

        # parameters ------------------------------------------------------------
        self.callDaysB4Exp = 2 # number of days before expiry to exit calls
        self.callDTE = 25 # target days till expiration
        self.callOTM = 1.00 # target percentage OTM of call
        self.percentage = 0.9 # percentage of portfolio for underlying asset
        self.options_alloc = 100 # 1 option for X num of shares (balanced would be 100)
        # ------------------------------------------------------------------------

    def OnData(self, data):
        if(self.IsWarmingUp):
            return
        
        # buy underlying asset
        if not self.Portfolio[self.symbol].Invested:
            self.Debug(f"Buying {self.symbol} {self.Time}")
            self.SetHoldings(self.symbol, self.percentage)
        
        # buy calls
        self.BuyCall(data)

        # close call before it expires
        if self.callContract:
            db4 = self.callContract.ID.Date - self.Time
            if self.Time > datetime(2014,3,1) and self.Time < datetime(2014,3,11):
                self.Debug(f"Call exp {self.callContract.ID.Date} db4e {db4}")
            if (self.callContract.ID.Date - self.Time) <= timedelta(self.callDaysB4Exp):
                self.Liquidate(self.callContract)
                self.Debug(f"Closed call: too close to expiration {db4} {self.Time.date()}")
                self.callContract = str()

    def BuyCall(self, data):
        # get option data
        if self.callContract == str():
            self.callContract = self.CallOptionsFilter(data)
            return
        
        # if not invested and option data added successfully, buy option
        elif not self.Portfolio[self.callContract].Invested and data.ContainsKey(self.callContract):
            nc = round(self.Portfolio[self.symbol].Quantity / self.options_alloc)
            q = self.Portfolio[self.symbol].Quantity
            mr = self.Portfolio.MarginRemaining
            mu = self.Portfolio.TotalMarginUsed
            dt = self.callContract.ID.Date.date()
            self.Log(f"BC Buying calls {self.Time.date()} nc {nc} q {q} mu {mu} X {dt}")
            self.Buy(self.callContract, nc)

    def CallOptionsFilter(self, data):
        ''' OptionChainProvider gets a list of option contracts for an underlying symbol at requested date.
            Then you can manually filter the contract list returned by GetOptionContractList.
            The manual filtering will be limited to the information included in the Symbol
            (strike, expiration, type, style) and/or prices from a History call '''

        contracts = self.OptionChainProvider.GetOptionContractList(self.symbol, data.Time)
        self.underlyingPrice = self.Securities[self.symbol].Price
        # filter the out-of-money call options from the contract list which expire close to self.callDTE num of days from now
        otm_calls = [i for i in contracts if i.ID.OptionRight == OptionRight.Call and
                                            i.ID.StrikePrice >= self.callOTM * self.underlyingPrice and
                                            self.callDTE - 8 < (i.ID.Date - data.Time).days < self.callDTE + 8]
        if len(otm_calls) > 0:
            # sort options by closest to self.callDTE days from now and desired strike, and pick first
            contract = sorted(sorted(otm_calls, key = lambda x: abs((x.ID.Date - self.Time).days - self.callDTE)),
             key = lambda x: self.underlyingPrice - x.ID.StrikePrice,reverse=True)[0]
#            self.Debug(f"call selected {contract.ID.Date.date()} str {contract.ID.StrikePrice}")
            if contract not in self.contractsAdded:
#                self.Debug(f"Buy call {contract.ID.StrikePrice} {self.symbol} {'%.2f'%self.underlyingPrice}")
                self.contractsAdded.add(contract)
                # use AddOptionContract() to subscribe the data for specified contract
                self.AddOptionContract(contract, Resolution.Daily)
            return contract
        else:
            return str()

    def OnOrderEvent(self, orderEvent):
        if self.Time > datetime(2014,1,1) and self.Time < datetime(2014,5,31):
            self.Debug(f" Order {str(orderEvent)}")

        if 0 and orderEvent.Status == OrderStatus.Filled:
            self.Debug(f"Cash {self.Portfolio.Cash} Holdings {self.Portfolio.TotalHoldingsValue} Val {self.Portfolio.TotalPortfolioValue}")
            self.Debug(str(orderEvent))

    def OnAssignmentOrderEvent(self, assignmentEvent: OrderEvent) -> None:
        self.Debug(f"Assignment event: {str(assignmentEvent)}")
# just set a flag to reset holdings in OnData
        self.NeedReset = True

    def OnEndOfAlgorithm(self):
        self.Debug(f"Final Report")
#        self.Liquidate()