Overall Statistics |
Total Trades 74 Average Win 2.05% Average Loss -0.75% Compounding Annual Return 32008.572% Drawdown 16.100% Expectancy 0.748 Net Profit 63.264% Sharpe Ratio 172.759 Probabilistic Sharpe Ratio 99.723% Loss Rate 53% Win Rate 47% Profit-Loss Ratio 2.74 Alpha 179.023 Beta 0.958 Annual Standard Deviation 1.042 Annual Variance 1.087 Information Ratio 173.72 Tracking Error 1.03 Treynor Ratio 188.075 Total Fees $1147.25 |
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. # Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ''' Naked Momentum -- V1.0.3 8.23.20 -- Added Qty (Estimation), not sure if correct + tested Bull Call logic (+) 8.24.20 -- Added Insights, + RiskManagement, etc. Potential Improvements -- Filter for TRADEABLE option symbols IN UNIVERSE ? (Full universe is filled, then QTY works) #Or just an IsTradeable list comp prior to entry loop? Find LEAST CORRELATED symbols within top x Momentum + Mkt Cap https://www.quantconnect.com/forum/discussion/6780/from-research-to-production-uncorrelated-assets/p1 DONE Add a dynamic hedge w realized vol > 12.5% -- Deep OTM Put ? ''' from Execution.ImmediateExecutionModel import ImmediateExecutionModel from Portfolio.EqualWeightingPortfolioConstructionModel import EqualWeightingPortfolioConstructionModel from Risk.MaximumDrawdownPercentPortfolio import MaximumDrawdownPercentPortfolio from Risk.TrailingStopRiskManagementModel import TrailingStopRiskManagementModel from datetime import timedelta from GetUncorrelatedAssets import GetUncorrelatedAssets class CoveredCallAlgorithm(QCAlgorithm): def Initialize(self): self.SetStartDate(2019, 1, 1) self.SetEndDate(2019, 1, 31) self.SetCash(100000) '''Dynamic Universe''' self.AddUniverse(self.SelectCoarse,self.SelectFine) self.UniverseSettings.Resolution = Resolution.Daily self.SetExecution(ImmediateExecutionModel()) self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel()) #self.SetRiskManagement(MaximumDrawdownPercentPortfolio(maximumDrawdownPercent = .05, isTrailing=True)) #self.SetRiskManagement(TrailingStopRiskManagementModel(.05) # ---------- Universe Params ----------- # self.mkt_cap_sort = True #Switched On if self.mkt_cap_sort: self.mom_x = 100 self.top_x = 10 else: self.mom_x = 10 # -- Momentum Params self.momentum_type = 0 #0 = OFF, 1 = year, 2 = month self.momBySym = {} self.momValues = None # -- End Momentum #Uncorrelated Pairs Switch self.uncorr = False #Option Selection + Execution Params self.symbol_list = [] self.min_dte = 0 #0 #DONT think these are actually plugged in ? self.max_dte = 5 #5 self.hold_days = 5 if self.min_dte <= 1 else self.min_dte self.max_positions = 10 self.spread_type = 1 #0 #1 #0 = Long Call, 1 = Bull Call #self.SetWarmUp() def SelectCoarse(self, coarse): sortedByDollarVolume = sorted(coarse, key=lambda x: x.DollarVolume, reverse=True)[:200] filtCoarse = [c for c in sortedByDollarVolume if c.Price > 10] if self.momentum_type == 0: self.symbol_list = [f.Symbol for f in filtCoarse] return self.symbol_list # ---------- Begin Momentum ---------- # selected = [] for c in filtCoarse: pass symbol = c.Symbol if symbol not in self.momBySym: hist = self.History(symbol, 253, Resolution.Daily) self.momBySym[symbol] = Momentum(symbol, self, hist) #pass in symbol, ALGO instance, and hist self.momBySym[symbol].Update(c.AdjustedPrice) if self.momentum_type == 1: sorted_by_momentum = {key:value for key, value in sorted(self.momentumBySymbol.items(),\ key=lambda kv: kv[1].mom_yr, reverse=True)[:self.mom_x]} else: sorted_by_momentum = {key:value for key, value in sorted(self.momentumBySymbol.items(),\ key=lambda kv: kv[1].mom_mo, reverse=True)[:self.mom_x]} self.momValues = sorted_by_momentum selected = list(sorted_by_momentum.keys()) self.symbol_list = selected # ---------- End Momentum ---------- # return self.symbol_list def SelectFine(self, fine): #Mkt Cap Filter if self.mkt_cap_sort: #filteredByMktCap = [x for x in fine if 1e10 < x.MarketCap] #< 1e9] sortedByMktCap = sorted(fine, key = lambda f: f.MarketCap, reverse=True)[:self.top_x] #reverse = Descending self.symbol_list = [f.Symbol for f in sortedByMktCap] else: self.symbol_list = [f.Symbol for f in fine] return self.symbol_list def OnSecuritiesChanged (self, changes): symbols = [x.Symbol for x in changes.AddedSecurities] #Returns <GOOG SYMBOLID> #init = symbols if self.uncorr and len(symbols) > 0: #Should eb only check needed top = len(symbols) #self.top_x #int(self.top_x / 2) if self.mkt_cap_sort else self.top_x history = self.History(symbols, 150, Resolution.Hour) if history.shape[1] > 1: #NEED better way to do this... hist = history.unstack(level = 1).close.transpose().pct_change().dropna() #WHY does this get out of index error ^^ symbols_rank = GetUncorrelatedAssets(hist, top) s2 = [symbol for symbol, corr_rank in symbols_rank] #Why not working right? #for s, s2 in zip(symbols, symbols_new): # self.Debug(f'{s} - {s2}') #Identical? WHY 168 not working? #s3 = [s for s in init if s in s2] s4 = [x.Symbol for x in changes.AddedSecurities if x.Symbol in symbols_rank] symbols = [s for s in symbols if s in s2] for x in changes.AddedSecurities: if x.Symbol not in symbols: continue #Matches w Uncorr or Regular -- DOES NOT WORK if x.Symbol.SecurityType != SecurityType.Equity: continue option = self.AddOption(x.Symbol.Value, Resolution.Minute) option.SetFilter(-1, +1, timedelta(self.min_dte), timedelta(self.max_dte)) '''IF buying CALLS OUTRIGHT -- need more time -- 15 - 30 probably''' for x in changes.RemovedSecurities: if x.Symbol.SecurityType != SecurityType.Equity: continue # Loop through the securities # If it is an option and the underlying matches the removed security, remove it for symbol in self.Securities.Keys: if symbol.SecurityType == SecurityType.Option and symbol.Underlying == x.Symbol: self.RemoveSecurity(symbol) def OnData(self,slice): #if not self.Portfolio["AAPL"].Invested: # self.MarketOrder("AAPL",100) # buy 100 shares of underlying stocks # self.Log(str(self.Time) + " bought SPY " + "@" + str(self.Securities["SPY"].Price) # + " Cash balance: " + str(self.Portfolio.Cash) # + " Equity: " + str(self.Portfolio.HoldStock)) if len(self.symbol_list) == 0: return option_invested = [x.Key for x in self.Portfolio if x.Value.Invested and x.Value.Type==SecurityType.Option] if len(option_invested) < self.max_positions: self.TradeOptions(slice) def TradeOptions(self,slice): #Filter for only ones NOT invested? invested = [x.Key for x in self.Portfolio if x.Value.Invested] #and x.Value.Type == SecurityType.Option] margin_remaining = self.Portfolio.MarginRemaining margin_per_position = margin_remaining * .99 * ( 1 / len(self.symbol_list) ) #Margin available for EACH symbol / option position insights = [] for i in slice.OptionChains: #if i.Key in invested: continue #Addit -- to skip open symbols #if i.Key != self.symbol: continue #if i.Key not in self.FixedUniv: continue #FixedUniverse #THIS is really CONTRACTS chain = i.Value # filter the call options contracts -- CALLS call = [x for x in chain if x.Right == OptionRight.Call] # sorted the contracts according to their expiration dates and choose the ATM options contracts = sorted(sorted(call, key = lambda x: abs(chain.Underlying.Price - x.Strike)), key = lambda x: x.Expiry, reverse=True) #NEED to CHECK remaining margin available! -- SET quantity! #quantity = self.CalculateOrderQuantity(chain.LastPrice, 0.1) #self.LastPrice #Takes SHARES available in 10% of if len(contracts) != 0: #opt_price = contracts[0].LastPrice * 100 opt_price = contracts[0].TheoreticalPrice * 100 #Per contract if opt_price != 0: qty = margin_per_position / opt_price #qty = margin_per_position / contracts[0].Underlying.Price #Approximation -- ATM, low DTE calls will have limited extrinsic #qty = margin_per_position / contracts[0].LastPrice * 100 #take margin per position, divide by cost of option contract self.Debug(f'option_price -- {opt_price} -- qty: {qty} -- basis: {qty * opt_price}') self.long_call = contracts[0].Symbol #Need a check if tradeable? #if self.long_call.IsTradeable: #self.MarketOrder(self.long_call, 1) insights += [Insight.Price(self.long_call, timedelta(days=5),InsightDirection.Up)] self.Debug(f'LE -- {self.Time.date()}: {self.long_call}') #Looks right ? #If turned on, enter BULL CALL leg of trade. if self.spread_type == 1 and len(contracts) > 1: self.short_call = contracts[1].Symbol #self.MarketOrder(self.short_call, -1) insights += [Insight.Price(self.short_call, timedelta(days=5), InsightDirection.Down)] self.EmitInsights(insights) def OnOrderEvent(self, orderEvent): self.Log(str(orderEvent)) self.Log("Cash balance: " + str(self.Portfolio.Cash)) class Momentum(): def __init__(self,symbol, algorithm, history): self.algo = algorithm self.sym = symbol self.window = RollingWindow[float](252) self.dailyBars = RollingWindow[TradeBar](10) self.mom_yr = 0 self.mom_mo = 0 #Consolidator ... self.dailyCons = TradeBarConsolidator(timedelta(days=1)) algorithm.SubscriptionManager.AddConsolidator(symbol, self.dailyCons) self.dailyCons.DataConsolidated += self.onDailyBar #Each daily bar, call handler for bar in history.itertuples(): tb = TradeBar(bar.Index[1], bar.open, bar.high, bar.low, bar.close, bar.volume) self.Update(bar.close) self.dailyBars.Add(tb) def Update(self, price): self.window.Add(price) if self.window.IsReady: self.mom_yr = (self.window[0] - self.window[251]) / self.window[251] #Was [-252] and [-25] self.mom_mo = (self.window[0] - self.window[25]) / self.window[25] #Event handler for NEW DAILY BAR def onDailyBar(self, sender, bar): self.dailyBars.Add(bar) @property def IsReady(self): return self.window.IsReady #and self.dailyBars.IsReady @property def pivlo(self): for i in range(1,8): if self.dailyBars[i].Close > self.dailyBars[i + 1].Close: return False if self.dailyBars[0].Close < self.dailyBars[1].Close: return False return True @property def pivhi(self): for i in range(1,8): if self.dailyBars[i].Close < self.dailyBars[i + 1].Close: return False if self.dailyBars[0].Close > self.dailyBars[1].Close: return False return True #def onIndicUpdate(self, sender, updated): # if self.BBD.IsReady: # self.uppers.Add(self.BBD.UpperBand.Current.Value) # self.lowers.Add(self.BBD.LowerBand.Current.Value) #Manual way to do this... better to use event handler #def WindowUpdate(self, bar): # if self.dailyBars.IsReady: # self.dailyBars.Add(bar)
import numpy as np import pandas as pd def GetUncorrelatedAssets(returns, num_assets): ''' Passed in HIST dataframe -- transformed slightly + unstacked history = qb.History(symbols, 150, Resolution.Hour) # Get hourly returns returns = history.unstack(level = 1).close.transpose().pct_change().dropna() https://www.quantconnect.com/forum/discussion/6780/from-research-to-production-uncorrelated-assets/p1 ''' # Get correlation correlation = returns.corr() # Find assets with lowest mean correlation, scaled by STD selected = [] for index, row in correlation.iteritems(): corr_rank = row.abs().mean()/row.abs().std() selected.append((index, corr_rank)) # Sort and take the top num_assets selected = sorted(selected, key = lambda x: x[1])[:num_assets] return selected ''' #In self.initialize self.FixedUniv = ['AAPL','AMD','TSLA','BABA','ROKU'] #Filter by IV Eventually? #equity = self.AddEquity("AAPL", Resolution.Minute) equities = [self.AddEquity(sym, Resolution.Minute) for sym in self.FixedUniv] option = [self.AddOption(symbol) for symbol in self.FixedUniv] #option = self.AddOption("AAPL", Resolution.Minute) #Original self.symbols = [option.Symbol for option in option] # set strike/expiry filter for this option chain for opt in option: #opt.SetFilter(-3, +3, timedelta(0), timedelta(30)) # use the underlying equity as the benchmark #ONLY for naked calls -- maybe bull calls? opt.SetFilter(-1,+1, timedelta(0), timedelta(5)) #self.SetBenchmark() ------------------------------------------------------------------------------- #Uncorr Assets Filter in FINE #if self.uncorr: # top = int(self.top_x / 2) if self.mkt_cap_sort else self.top_x # history = self.History(self.symbol_list, 150, Resolution.Hour) # history.unstack(level = 1).close.transpose().pct_change().dropna() # symbols_rank = GetUncorrelatedAssets(history, top) # symbols = [symbol for symbol, corr_rank in symbols_rank] # self.symbol_list = [s for s in self.symbol_list if s in symbols] #self.Debug(f'Post Uncorr - {self.symbol_list}') '''