Overall Statistics
Total Trades
16
Average Win
0.37%
Average Loss
-0.35%
Compounding Annual Return
18.829%
Drawdown
2.500%
Expectancy
0.547
Net Profit
1.516%
Sharpe Ratio
1.536
Probabilistic Sharpe Ratio
57.179%
Loss Rate
25%
Win Rate
75%
Profit-Loss Ratio
1.06
Alpha
-0.279
Beta
1.331
Annual Standard Deviation
0.094
Annual Variance
0.009
Information Ratio
-2.856
Tracking Error
0.061
Treynor Ratio
0.109
Total Fees
$23.00
"""

refs
# https://www.quantconnect.com/tutorials/strategy-library/volatility-risk-premium-effect
# https://www.quantconnect.com/forum/discussion/2894/the-options-trading-strategy-based-on-macd-indicator/p1
# https://www.quantconnect.com/tutorials/tutorial-series/applied-options
# https://www.quantconnect.com/forum/discussion/5709/optionchain-is-empty/p1
"""

from datetime import timedelta
import numpy as np
import pandas as pd
from scipy import stats
np.random.seed(2020) # comment to make it a real roulette

class OptionRouletteAlgorithm(QCAlgorithm):

    def Initialize(self):
        
        self.SetStartDate(2017, 1, 15)
        self.SetEndDate(2017,2, 15)
        #self.SetStartDate(2015, 1, 1)
        #self.SetEndDate(datetime.now().date() - timedelta(1))
        
        self.SetCash(100000)
        equity = self.AddEquity("SPY", Resolution.Minute)
        option = self.AddOption("SPY", Resolution.Minute)
        self.symbol = equity.Symbol
        option.SetFilter(self.UniverseFunc)
        self.SetBenchmark(equity.Symbol)
        self.slice = None

        # Define the Schedules
        self.Schedule.On(
            self.DateRules.WeekStart(self.symbol),
            self.TimeRules.AfterMarketOpen(self.symbol, 5),
            Action(self.MyLiquidate)
        )
        
        # Define the Schedules
        self.Schedule.On(
            self.DateRules.WeekStart(self.symbol),
            self.TimeRules.AfterMarketOpen(self.symbol, 10),
            Action(self.MyTrade)
        )
        
    def OnData(self,slice):
        self.slice = slice
        if slice.OptionChains.Count > 0:
            pass
            
    def OnAssignmentOrderEvent(self, assignmentEvent):
        self.Log(str(assignmentEvent))
        self.MyLiquidate()
        
    def OnOrderEvent(self, orderEvent):
        self.Log(str(orderEvent)) 

    def UniverseFunc(self, universe):
        price = self.Securities[self.symbol].Price
        return universe.IncludeWeeklys()\
                    .Strikes(-50,50)\
                    .Expiration(timedelta(30), timedelta(50))
        # TODO: read above api.
                    
    def MyLiquidate(self):
        for x in self.Portfolio:
            if x.Value.Invested:
                self.Liquidate(x.Key)
        # redundant?
        if self.Portfolio[self.symbol].Invested:
            self.Liquidate(self.symbol)
                
        self.Log("MyLiquidate")
    
    
    def MyTrade(self):
        slice = self.slice
        
        if slice is None:
            return
        
        self.Log("MyTrade {} {}".format(self.Portfolio.Invested,slice.OptionChains.Count))
        if slice.OptionChains.Count == 0:
            return
        for i in slice.OptionChains:
            chains = i.Value
            
            if not self.Portfolio.Invested:
                self.Log("trading!")
                # divide option chains into call and put options 
                calls = list(filter(lambda x: x.Right == OptionRight.Call, chains))
                puts = list(filter(lambda x: x.Right == OptionRight.Put, chains))
                
                # if lists are empty return
                if not calls or not puts: return
                
                underlying_price = self.Securities[self.symbol].Price
                expiries = [i.Expiry for i in puts]
                
                # determine expiration date nearly one month
                expiry = min(expiries, key=lambda x: abs((x.date()-self.Time.date()).days-40))
                strikes = [i.Strike for i in puts]
                
                # determine at-the-money strike
                strike = min(strikes, key=lambda x: abs(x-underlying_price))
                
                # compute probability
                hist = self.History([self.symbol], 252*5, Resolution.Daily)
                prct_changes = hist.loc[self.symbol]['close'].pct_change(40)
                # 68% = 1sd, 90% = 2sd.
                m2sd,m1sd,p1sd,p2sd = np.nanpercentile(prct_changes,[5,32,68,95])
                
                # roulette logic
                optionStyle = np.random.choice(['short_strangle','short_iron_condor','long_strangle','synthetic_long'],1)[0]
                num = np.random.choice([2,5,10],1)[0]
                
                # long volatility strategies ********************************
                
                # why would you ever?
                if optionStyle == 'synthetic_long':
                    self.atm_put = [i for i in puts if i.Expiry == expiry and i.Strike == strike]
                    self.atm_call = [i for i in calls if i.Expiry == expiry and i.Strike == strike]
                    
                    if self.atm_put and self.atm_call:
                        mylist = [self.atm_put[0],self.atm_call[0]]
                        self.Log('{}'.format([stats.percentileofscore(prct_changes,(x.Strike-underlying_price)/underlying_price) for x in mylist]))
                        
                        self.Sell(self.atm_put[0].Symbol, num)
                        self.Buy(self.atm_call[0].Symbol, num)
                        
                if optionStyle == 'long_strangle':
                    
                    self.atm_put = [i for i in puts if i.Expiry == expiry and i.Strike == strike]
                    self.atm_call = [i for i in calls if i.Expiry == expiry and i.Strike == strike]
                    
                    if self.atm_put and self.atm_call:
                        mylist = [self.atm_put[0],self.atm_call[0]]
                        self.Log('{}'.format([stats.percentileofscore(prct_changes,(x.Strike-underlying_price)/underlying_price) for x in mylist]))
                        
                        self.Buy(self.atm_put[0].Symbol, num)
                        self.Buy(self.atm_call[0].Symbol, num)
                
                # short volatility strategies ********************************
                
                if optionStyle == 'short_iron_condor':
                    
                    otm_call_strike = min(strikes, key = lambda x:abs(x-underlying_price+p2sd*underlying_price))
                    atm_call_strike = min(strikes, key = lambda x:abs(x-underlying_price+p1sd*underlying_price)) # more like near atm
                    atm_put_strike = min(strikes, key = lambda x:abs(x-underlying_price+m1sd*underlying_price))
                    otm_put_strike = min(strikes, key = lambda x:abs(x-underlying_price+m2sd*underlying_price))

                    self.otm_call = [i for i in calls if i.Expiry == expiry and i.Strike == otm_call_strike]
                    self.atm_call = [i for i in calls if i.Expiry == expiry and i.Strike == atm_call_strike]
                    self.atm_put = [i for i in puts if i.Expiry == expiry and i.Strike == atm_put_strike]
                    self.otm_put = [i for i in puts if i.Expiry == expiry and i.Strike == otm_put_strike]
                    
                    if self.atm_call and self.atm_put and self.otm_put and self.otm_call:
    
                        mylist = [self.otm_put[0],self.atm_call[0],self.atm_put[0],self.otm_call[0]]
                        self.Log('{}'.format([stats.percentileofscore(prct_changes,(x.Strike-underlying_price)/underlying_price) for x in mylist]))
                        # TODO: log net profit and potential max loss.
                        
                        # buy otm
                        self.Buy(self.otm_call[0].Symbol, num)
                        self.Buy(self.otm_put[0].Symbol, num)
                        # sell near atm
                        self.Sell(self.atm_call[0].Symbol, num)
                        self.Sell(self.atm_put[0].Symbol, num)
                
                
                if optionStyle == 'short_strangle':
                    
                    otm_call_strike = min(strikes, key = lambda x:abs(x-underlying_price+p2sd*underlying_price))
                    otm_put_strike = min(strikes, key = lambda x:abs(x-underlying_price+m2sd*underlying_price))
                    
                    self.otm_put = [i for i in puts if i.Expiry == expiry and i.Strike == otm_put_strike]
                    self.otm_call = [i for i in calls if i.Expiry == expiry and i.Strike == otm_call_strike]
                    if self.otm_put and self.otm_call:
                        mylist = [self.otm_put[0],self.otm_call[0]]
                        self.Log('{}'.format([stats.percentileofscore(prct_changes,(x.Strike-underlying_price)/underlying_price) for x in mylist]))
                        
                        self.Sell(self.otm_put[0].Symbol, num)
                        self.Sell(self.otm_call[0].Symbol, num)