Overall Statistics |
Total Trades 48 Average Win 2.61% Average Loss -4.22% Compounding Annual Return 22.841% Drawdown 46.600% Expectancy 0.101 Net Profit 7.679% Sharpe Ratio 0.867 Probabilistic Sharpe Ratio 41.931% Loss Rate 32% Win Rate 68% Profit-Loss Ratio 0.62 Alpha -0.371 Beta 6.394 Annual Standard Deviation 0.805 Annual Variance 0.648 Information Ratio 0.69 Tracking Error 0.769 Treynor Ratio 0.109 Total Fees $458.59 Estimated Strategy Capacity $310000.00 Lowest Capacity Asset SPY WOV3B703VNIE|SPY R735QTJ8XC9X |
f = False if f: from AlgorithmImports import * from collections import deque from typing import List import configs as cfg from indicators import GoldenCross, ATRBuySell from datetime import timedelta import pickle class EnergeticBlueDonkey(QCAlgorithm): def Initialize(self): self.SetStartDate(2017, 6, 1) self.SetEndDate(2017, 10, 10) self.SetCash(100000) self.symbol = self.AddEquity('SPY', Resolution.Minute).Symbol self.gc = GoldenCross(cfg.fast_sma_period, cfg.slow_sma_period) self.atrbs = ATRBuySell() self.Consolidate(self.symbol, Resolution.Daily, self.OnConsolidation) option = self.AddOption(self.symbol) option.SetFilter(-20, +20, timedelta(15), timedelta(45)) option.PriceModel = OptionPriceModels.CrankNicolsonFD() # necessary for greeks self.options : List[Symbol] = [] # makes it so we emulate Daily Resolution on Minute Resolution # this is necessary since we are dealing with options, which only work on Minute or finer data self.curr_day = -1 self.SetWarmUp(max(cfg.slow_sma_period + 3, cfg.atr_period), Resolution.Daily) # stuff for data recording self.orders = [] self.bb = BollingerBands(cfg.bb_period, cfg.bb_multiple) self.kelt = KeltnerChannels(cfg.kelt_period, cfg.kelt_multiple) indicators = ['time', 'price', 'sma_short', 'sma_long', 'atr', 'obv', 'bb', 'bb_lower', 'bb_upper', 'kelt', 'kelt_lower', 'kelt_upper'] self.indicator_values = {indicator: list() for indicator in indicators} self.bars = [] def OnEndOfAlgorithm(self): self.ObjectStore.SaveBytes('orders', pickle.dumps(self.orders)) self.ObjectStore.SaveBytes('indicators', pickle.dumps(self.indicator_values)) self.ObjectStore.SaveBytes('bars', pickle.dumps(self.bars)) def OnConsolidation(self, bar: TradeBar): self.atrbs.Update(bar) self.bb.Update(bar.EndTime, bar.Close) self.kelt.Update(bar) self.RecordValues(bar) def RecordValues(self, bar: TradeBar): if self.IsWarmingUp: return self.bars.append({ 'time': bar.EndTime, 'open': bar.Open, 'high': bar.High, 'low': bar.Low, 'close': bar.Close }) self.indicator_values['time'].append(bar.EndTime) self.indicator_values['price'].append(bar.Close) self.indicator_values['obv'].append(bar.Volume) self.indicator_values['sma_short'].append(self.gc.fast_sma.Current.Value) self.indicator_values['sma_long'].append(self.gc.slow_sma.Current.Value) self.indicator_values['atr'].append(self.atrbs.atr.Current.Value) self.indicator_values['bb'].append(self.bb.Current.Value) self.indicator_values['bb_lower'].append(self.bb.LowerBand.Current.Value) self.indicator_values['bb_upper'].append(self.bb.UpperBand.Current.Value) self.indicator_values['kelt'].append(self.kelt.Current.Value) self.indicator_values['kelt_lower'].append(self.kelt.LowerBand.Current.Value) self.indicator_values['kelt_upper'].append(self.kelt.UpperBand.Current.Value) def RecordTrade(self, direction): self.orders.append({'time':self.Time, 'direction': direction}) def Print(self, msg:str): ''' Just Debug, but with a check that debugging is enabled ''' if cfg.debug: self.Debug(msg) def OnData(self, data:Slice): if self.curr_day == self.Time.day: return self.curr_day = self.Time.day self.ProcessOptions(data) if not data.Bars.ContainsKey(self.symbol): return self.gc.Update(data[self.symbol]) if self.IsWarmingUp or not self.gc.IsReady or not self.atrbs.IsReady: return self.Rebalance() self.Plots() def Rebalance(self): ''' take profit, stop loss GoldenCross entry and DeathCross exit equity/option weightage rebalance ''' self.Print('Rebalancing...') if self.atrbs.Value == 1: self.atrbs.Reset() self.Print('TakeProfit - Liquidating') self.Liquidate() self.RecordTrade(0) elif self.atrbs.Value == -1: self.atrbs.Reset() self.Print('StopLoss - Liquidating') self.Liquidate() self.RecordTrade(2) elif self.gc.Value == 0: self.Print('DeathCross - Liquidating') self.Liquidate() self.RecordTrade(3) elif self.gc.Value == 2 and not self.Portfolio.Invested: self.Print('GoldenCross - Going Long') self.atrbs.SetLevels() self.SetHoldings(self.symbol, .5) for option in self.options: self.SetHoldings(option, .5/len(self.options)) self.RecordTrade(4) elif self.Portfolio.Invested: equity_value = self.Portfolio[self.symbol].Price * self.Portfolio[self.symbol].Quantity equity_portfolio_pct = equity_value / self.Portfolio.TotalPortfolioValue if .45 < equity_portfolio_pct < .65: return self.Print('Equity/Option Imbalance, resetting to 50/50') invested_options = [ kvp.Key for kvp in self.Portfolio if kvp.Value.Invested and kvp.Key.SecurityType == SecurityType.Option ] self.SetHoldings(self.symbol, .5) for option in invested_options: self.SetHoldings(option, .5/len(invested_options)) def ProcessOptions(self, data:Slice): self.Print('Processing options...') self.options = [] for x in data.OptionChains: chain: List[OptionContract] = [x for x in x.Value] contracts = [optionContract for optionContract in chain if cfg.option_filter(optionContract)] self.Print(len(contracts)) underlying_price = self.Securities[self.symbol].Price nearTheMoney = sorted(contracts, key=lambda contract: abs(contract.Strike - underlying_price))[:cfg.options_count] self.options.extend([contract.Symbol for contract in nearTheMoney]) self.Print(f'Found {len(self.options)} options') def Plots(self): if not cfg.debug: return self.Plot('GoldenCross', 'Value', self.gc.Value)
f = False if f: from AlgorithmImports import * import configs as cfg from collections import deque class GoldenCross: def __init__(self, fast_period:int, slow_period:int): ''' GoldenCross indicator .Value = 0 -> not golden cross or death cross .Value = 1 -> golden cross formed, entry not .Value = 2 -> entry formed after golden cross ''' self.Value = 0 self.fast_sma = SimpleMovingAverage(fast_period) self.slow_sma = SimpleMovingAverage(slow_period) # fast sma - slow sma self.sma_diffs = deque(maxlen=3) def dq_rdy(self, vals:deque): ''' returns True iff the deque is has maxlen elements ''' return len(vals) == vals.maxlen def Update(self, input:TradeBar): ''' updates the Golden Cross indicator with a new bar of data returns self.IsReady ''' self.Time = input.EndTime close = input.Close self.fast_sma.Update(self.Time, close) self.slow_sma.Update(self.Time, close) if not self.slow_sma.IsReady: # since the slow_sma takes more values, if its ready # the fast_sma must be ready return False self.sma_diffs.append( self.fast_sma.Current.Value - self.slow_sma.Current.Value ) if not self.dq_rdy(self.sma_diffs): return False is_crossed = ( self.sma_diffs[2] > 0 and self.sma_diffs[1] < 0 and self.sma_diffs[0] < 0 ) # if the fast just recently rises above the slow is_death_crossed = ( self.sma_diffs[2] < 0 and self.sma_diffs[1] > 0 and self.sma_diffs[0] > 0 ) # if the fast just recently dips above the slow if is_death_crossed: self.Value = 0 if self.Value <= 0 and is_crossed: self.Value = 1 elif self.Value == 1 and cfg.entry_condition(close, self.fast_sma.Current.Value, self.slow_sma.Current.Value) : self.Value = 2 return True def Warmup(self): pass @property def IsReady(self): ''' returns True iff the indicator is ready to use ''' return self.dq_rdy(self.sma_diffs) class ATRBuySell: def __init__(self): ''' ATR Take Profit and Stop Loss .Value = -1 -> stop loss .Value = 0 -> neutral .Value = 1 -> take profit ''' self.Value = 0 self.atr = AverageTrueRange(cfg.atr_period) self.Close = 0 self.StopLoss = None self.TakeProfit = None def Update(self, input:TradeBar): ''' updates the ATRBuySell indicator with a new bar of data returns self.IsReady ''' self.Time = input.EndTime self.Close = input.Close self.atr.Update(input) if not self.atr.IsReady: return False if self.TakeProfit and input.Close > self.TakeProfit: self.Value = 1 elif self.StopLoss and input.Close < self.StopLoss: self.Value = -1 return True def Reset(self): self.TakeProfit = None self.StopLoss = None self.Value = 0 def SetLevels(self): ''' Sets the TakeProfit and StopLoss levels, which are used to determine .Value ''' self.Value = 0 atr = self.atr.Current.Value self.TakeProfit = self.Close + cfg.atr_take_profit_factor * atr self.StopLoss = self.Close - cfg.atr_stop_loss_factor * atr def Warmup(self): pass @property def IsReady(self): ''' returns True iff the indicator is ready to use ''' return self.atr.IsReady
f = False if f: from AlgorithmImports import * # disable below if you want to reduce logging/plotting debug = False #BEGIN GoldenCross configurations fast_sma_period = 5 slow_sma_period = 20 assert(fast_sma_period < slow_sma_period) # entry condition after cross has formed def entry_condition(curr_price:float, fast_sma:float, slow_sma:float)->bool: ''' return True iff entry condition is met ''' sma_avg = (fast_sma + slow_sma) / 2 # 4% within average of two SMAs return abs(1-(curr_price / sma_avg)) < .04 #END GoldenCross configurations #BEGIN ATR configs atr_period = 14 atr_take_profit_factor = 2 atr_stop_loss_factor = 1 #END ATR configs #BEGIN Options configurations options_count = 3 # how many of the top delta options target_delta = .5 def option_filter(optionContract: OptionContract): ''' return True iff contract is a call and Greek conditions are met ''' if optionContract.Symbol.ID.OptionRight != OptionRight.Call: return False # NEVER TRADES # elif optionContract.Greeks.Theta < -.05: elif optionContract.Greeks.Theta < -10: return False # NEVER TRADES # elif ( # .25 < delta < 1 # optionContract.Greeks.Delta < target_delta * .5 # or optionContract.Greeks.Delta > target_delta * 2 # ): elif ( # .25 < delta < 1 optionContract.Greeks.Delta < target_delta * .5 or optionContract.Greeks.Delta > target_delta * 2 ): return False return True #END Options configurations #BEGIN plotting configs bb_period = 14 bb_multiple = 2 kelt_period = 14 kelt_multiple = 2