Advertisement
Guest User

etf-mom-strategy

a guest
Sep 26th, 2022
731
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 13.48 KB | Source Code | 0 0
  1. #region imports
  2. from AlgorithmImports import *
  3. #endregion
  4. ##########################################################################################
  5. # ETF Index Momentum Rebalancer
  6. # -----------------------------
  7. #
  8. # Hold 'N' of the fastest moving stocks from the given ETF. Equally weightied
  9. # Rebalance every Day/Week/Month and cut losers with drawdown higher than X%.
  10. #
  11. # External Parameters, and defaults
  12. # -----------------------------------
  13. # maxHoldings           = 5     # Max number of positions to hold
  14. # lookbackInDays        = 160   # Look at performance over last x days    
  15. # rebalancePeriodIndex  = 0     # 0:Monthly | 1:Weekly | 2:Daily
  16. # exitLosersPeriodIndex = 1     # irrelvant if the same as rebalance period index
  17. # exitLoserMaxDD        = 10    # exit if ddown >= x%. Seems to do more harm than good.
  18. # maxPctEquity          = 80    # % of equity to trade
  19. # @shock_and_awful
  20. #
  21. ##########################################################################################
  22.  
  23. class ETFUniverse(QCAlgorithm):
  24.  
  25.     ## Main entry point for the algo    
  26.     ## -----------------------------
  27.     def Initialize(self):
  28.         self.InitBacktestParams()
  29.         self.InitExternalParams()
  30.         self.InitAssets()
  31.         self.InitAlgoParams()
  32.         self.ScheduleRoutines()
  33.  
  34.        
  35.     ## Set backtest params: dates, cash, etc. Called from Initialize().
  36.     ## ----------------------------------------------------------------
  37.     def InitBacktestParams(self):
  38.         self.SetStartDate(2010, 1, 1)   # Start Date
  39.         # self.SetEndDate(2021, 1, 1)   # End Date. Omit to run till present day
  40.         self.SetCash(10000)             # Set Strategy Cash
  41.         self.EnableAutomaticIndicatorWarmUp = True
  42.  
  43.     ## Initialize external parameters. Called from Initialize().
  44.     ## ---------------------------------------------------------
  45.     def InitExternalParams(self):
  46.         self.maxPctEquity          = float(self.GetParameter("maxPctEquity"))/100
  47.         self.maxHoldings           = int(self.GetParameter("maxHoldings"))
  48.         self.rebalancePeriodIndex  = int(self.GetParameter("rebalancePeriodIndex"))
  49.         self.lookbackInDays        = int(self.GetParameter("lookbackInDays"))
  50.         self.useETFWeights         = bool(self.GetParameter("useETFWeights") == 1)
  51.         self.exitLosersPeriodIndex = int(self.GetParameter("exitLosersPeriodIndex"))
  52.         self.exitLoserMaxDD        = -float(self.GetParameter("exitLoserMaxDD"))/100
  53.  
  54.     ## Init assets: Symbol, broker model, universes, etc. Called from Initialize().
  55.     ## ----------------------------------------------------------------------------
  56.     def InitAssets(self):
  57.        
  58.         # Try diffferent ETF tickers like 'QQQ','SPY','XLF','EEM', etc
  59.         self.ticker = 'QQQ'
  60.  
  61.         self.etfSymbol = self.AddEquity(self.ticker, Resolution.Hour).Symbol
  62.  
  63.         # Specify that we are using an ETF universe, and specify the data resolution
  64.         # (ETF Universe versus some other universe, eg: based on fundamental critiera)
  65.         self.AddUniverse(self.Universe.ETF(self.etfSymbol, self.UniverseSettings, self.ETFConstituentsFilter))
  66.         self.UniverseSettings.Resolution = Resolution.Hour
  67.  
  68.         # self.SetSecurityInitializer(lambda x: x.SetMarketPrice(self.GetLastKnownPrice(x)))
  69.         self.SetSecurityInitializer(self.CustomSecurityInitializer)
  70.  
  71.         # TODO: Explore not trading if the ETF is below a critical SMA (eg 200)
  72.         # Uncomment this code, and some additional code further below, to do so
  73.         # -----------------------------------------------------------------------
  74.         # self.etfSMA    = self.SMA(self.etfSymbol, 100, Resolution.Daily)
  75.  
  76.     ## Custom Security initializer, for reality modeling so you can
  77.     ## better mimic real world conditions (eg: slippage, fees, etc)
  78.     ## You can use your own reality modelling. read more in the docs
  79.     ## quantconnect.com/docs/v2/writing-algorithms/reality-modeling/slippage/key-concepts
  80.     ## ---------------------------------------------------------------------------------
  81.     def CustomSecurityInitializer(self, security):
  82.         security.SetMarketPrice(self.GetLastKnownPrice(security))  
  83.         security.SetFeeModel(InteractiveBrokersFeeModel())      # Model IB's trading fees.
  84.         security.SetSlippageModel(VolumeShareSlippageModel())   # Model slippage based on volume impact
  85.         security.SetFillModel(LatestPriceFillModel())           # Model fills based on latest price
  86.  
  87.     ## Set algo params: Symbol, broker model, ticker, etc. Called from Initialize().
  88.     ## -----------------------------------------------------------------------------
  89.     def InitAlgoParams(self):
  90.  
  91.         # Flags to track and trigger rebalancing state
  92.         self.timeToRebalance     = True
  93.         self.universeRepopulated = False
  94.  
  95.         # State vars
  96.         self.symbolWeightDict    = {}
  97.         self.screenedSymbolData  = {}
  98.         self.ScreenedSymbols     = []
  99.  
  100.         # Interval Periods. Try Monthly, Weekly, Daily
  101.         intervalPeriods          = [IntervalEnum.MONTHLY, IntervalEnum.WEEKLY, IntervalEnum.DAILY]
  102.         self.rebalancePeriod     = intervalPeriods[self.rebalancePeriodIndex]
  103.         self.exitLosersPeriod    = intervalPeriods[self.exitLosersPeriodIndex]
  104.  
  105.  
  106.     ## Schedule routine that we need to run on intervials
  107.     ## Eg: rebalance every month, Exit losers every week, etc
  108.     ## ------------------------------------------------------
  109.     def ScheduleRoutines(self):
  110.  
  111.         # Schedule rebalancing flag
  112.         if( self.rebalancePeriod == IntervalEnum.MONTHLY ):
  113.             self.Schedule.On( self.DateRules.MonthStart(self.etfSymbol),
  114.                               self.TimeRules.AfterMarketOpen(self.etfSymbol, 31),
  115.                               self.SetRebalanceFlag )
  116.  
  117.         elif( self.rebalancePeriod == IntervalEnum.WEEKLY ):
  118.             self.Schedule.On( self.DateRules.WeekStart(self.etfSymbol),
  119.                               self.TimeRules.AfterMarketOpen(self.etfSymbol, 31),
  120.                               self.SetRebalanceFlag )
  121.  
  122.         # Schedule routines to exit losers
  123.         if( self.exitLosersPeriod == IntervalEnum.WEEKLY ):
  124.             self.Schedule.On( self.DateRules.WeekStart(self.etfSymbol),
  125.                               self.TimeRules.AfterMarketOpen(self.etfSymbol, 31),
  126.                               self.ExitLosers )
  127.  
  128.         elif( self.exitLosersPeriod == IntervalEnum.DAILY ):
  129.             self.Schedule.On( self.DateRules.EveryDay(self.etfSymbol),
  130.                               self.TimeRules.AfterMarketOpen(self.etfSymbol, 31),
  131.                               self.ExitLosers)
  132.  
  133.         # Buy screened symbols
  134.         self.Schedule.On( self.DateRules.EveryDay(self.etfSymbol),
  135.                               self.TimeRules.AfterMarketOpen(self.etfSymbol, 60),
  136.                               self.BuyScreenedSymbols)
  137.                    
  138.  
  139.     ## This event handler receives the constituents of the ETF, and their weights.
  140.     ## In here, if it is time to rebalance the portfolio, we will rank the stocks
  141.     ## by momentum, and 'select' the top N positive movers. We dont use the weights atm.
  142.     ## ---------------------------------------------------------------------------------
  143.     def ETFConstituentsFilter(self, constituents):
  144.         if( self.timeToRebalance ):
  145.            
  146.             # Create a dictionary , dict[symbol] = weight    
  147.             self.symbolWeightDict = {c.Symbol: c.Weight for c in constituents}
  148.            
  149.             # reset flags
  150.             self.universeRepopulated = True
  151.             self.timeToRebalance = False
  152.  
  153.            
  154.             # Loop through the symbols, create a symbol data object (contains indicator calcs)
  155.             for symbol in self.symbolWeightDict:
  156.                 if symbol not in self.screenedSymbolData:
  157.                    
  158.                     self.screenedSymbolData[symbol] = SymbolData(self, symbol, self.symbolWeightDict[symbol], \
  159.                                                           self.lookbackInDays)
  160.  
  161.                 # fetch recent history, then seed the symbol data object,
  162.                 # so indicators values can be calculated    
  163.                 symbolData = self.screenedSymbolData[symbol]
  164.                 history    = self.History[TradeBar](symbol, self.lookbackInDays+1, Resolution.Daily)
  165.                 symbolData.SeedTradeBarHistory(history)
  166.  
  167.             # - - Here is where you can add custom logic for signals.    --
  168.             # - - right now the only entry criteria is positive momentum --
  169.             # - - to change this, update the 'ScreeningCriteriaMet' in SymbolData
  170.             # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  171.             self.screenedSymbolData   = {key: symData for key, symData in self.screenedSymbolData.items() if symData.ScreeningCriteriaMet}
  172.  
  173.             # Sort the symbols based on indicator values in the symbol data object.
  174.             # right now we are using momentum. Might consider others in the future.
  175.             momSorted = sorted(self.screenedSymbolData.items(), key=lambda x: x[1].MomentumValue, reverse=True)[:self.maxHoldings]
  176.  
  177.             # Add Symbols to the 'selected' list
  178.             self.ScreenedSymbols = [x[0] for x in momSorted]
  179.            
  180.             return self.ScreenedSymbols
  181.  
  182.         else:
  183.             return []    
  184.            
  185.     ## Called when the rebalance time interval is up
  186.     ## -----------------------------------------------
  187.     def SetRebalanceFlag(self):
  188.         self.timeToRebalance = True
  189.  
  190.  
  191.     ## Open positions for screened symbols.
  192.     ## ------------------------------------------
  193.     def BuyScreenedSymbols(self):
  194.        
  195.         # TODO: Explore not trading if the ETF is below a critical SMA (eg 200)
  196.         # ----------------------------------------------------------------------
  197.         # if( self.Securities[self.etfSymbol].Price < self.etfSMA.Current.Value ):
  198.         #     if(self.Portfolio.Invested):
  199.         #         self.Liquidate(tag="ETF trading below critical SMA") # liquidate everything
  200.         #     return
  201.  
  202.         if (self.universeRepopulated):
  203.             self.Liquidate()    # liquidate everything
  204.             self.symbolWeightDict = {}   # reset weights
  205.            
  206.  
  207.  
  208.             # TODO: Explore using the relative weight of the assets for position sizing
  209.             #       ie: Respect the % of the assets in the ETF. Sof if you are trading SPY constituents
  210.             #       and AAPL had high momentum, it would have releatively large position size.
  211.             # ------------------------------------------------------------------------------------------            
  212.            
  213.             # weightsSum = sum(self.screenedSymbolData[symbol].etfWeight for symbol in self.ScreenedSymbols)
  214.            
  215.             for symbol in self.ScreenedSymbols:
  216.  
  217.                 # TODO: Uncomment Explore using relative weight. You'd use
  218.                 # symbolWeight = self.screenedSymbolData[symbol].etfWeight / weightsSum # respect weighting
  219.                
  220.                 symbolWeight = 1 / len(self.ScreenedSymbols) # Equally weighted
  221.                
  222.                 # Adjust to ensure we trade less than max pct of equity
  223.                 adjustedWeight = symbolWeight * self.maxPctEquity
  224.                 self.SetHoldings(symbol, adjustedWeight, tag=f"Momentum Pct: {round(self.screenedSymbolData[symbol].MomentumValue,2)}%")
  225.    
  226.             self.universeRepopulated = False    
  227.  
  228.     def ExitLosers(self):
  229.         # Loop through holdings, and exit any that are losing below the threshold
  230.         for x in self.Portfolio:
  231.             if x.Value.Invested:
  232.                 if( x.Value.UnrealizedProfitPercent <= self.exitLoserMaxDD):
  233.                     orderMsg = f"Unacceptable drawdown ({round(x.Value.UnrealizedProfitPercent*100,2)})% < {self.exitLoserMaxDD*100}"
  234.                     self.Liquidate(x.Key, tag=orderMsg)
  235.    
  236.     # TODO: Periodically check if a security is no longer in the ETF
  237.     # ---------------------------------------------------------------
  238.     # def RemoveDelistedSymbols(self, changes):
  239.     #     for investedSymbol in [x.Key for x in self.Portfolio if x.Value.Invested]:
  240.     #         if( investedSymbol not in self.symbolWeightDict.keys() ):
  241.     #             self.Liquidate(symbol, 'No longer in universe')
  242.  
  243.  
  244. ##################################################
  245. # Symbol Data Class --
  246. # Data we need to persist for each Symvol
  247. ##################################################
  248. class SymbolData():
  249.    
  250.     def __init__(self, algo, symbol, etfWeight, lookbackInDays):
  251.         self.algo         = algo
  252.         self.symbol       = symbol  
  253.         self.etfWeight    = etfWeight
  254.         self.momPct       = MomentumPercent(lookbackInDays)
  255.  
  256.     def SeedTradeBarHistory(self,history):
  257.         for tradeBar in history:
  258.             self.momPct.Update(tradeBar.Time, tradeBar.Close)
  259.  
  260.     @property
  261.     def MomentumValue(self):
  262.         if(self.momPct.IsReady):
  263.             return self.momPct.Current.Value
  264.         else:
  265.             return float('-inf')
  266.  
  267.     # - - Here is where you can add custom logic for signals.    --
  268.     # - - right now the only entry criteria is positive momentum --
  269.     # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  
  270.     @property
  271.     def ScreeningCriteriaMet(self):
  272.         return (self.MomentumValue > 0 )
  273.  
  274. ###############################
  275. # Interval Enum  
  276. ###############################
  277. class IntervalEnum(Enum):
  278.     MONTHLY  = "MONTHLY"
  279.     WEEKLY   = "WEEKLY"
  280.     DAILY    = "DAILY"
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement