Overall Statistics |
Total Orders 614 Average Win 0.54% Average Loss -0.56% Compounding Annual Return 5.770% Drawdown 22.200% Expectancy -0.014 Start Equity 100000 End Equity 105796.62 Net Profit 5.797% Sharpe Ratio 0.044 Sortino Ratio 0.054 Probabilistic Sharpe Ratio 21.462% Loss Rate 50% Win Rate 50% Profit-Loss Ratio 0.97 Alpha 0 Beta 0 Annual Standard Deviation 0.171 Annual Variance 0.029 Information Ratio 0.355 Tracking Error 0.171 Treynor Ratio 0 Total Fees $733.62 Estimated Strategy Capacity $4300000.00 Lowest Capacity Asset CYTK SY8OYP5ZLDUT Portfolio Turnover 6.67% |
#region imports from AlgorithmImports import * #endregion class LongShortAlphaModel(AlphaModel): def __init__(self, algorithm): self.algorithm = algorithm self.rebalance_time = datetime.min def update(self, algorithm, data): # Do nothing until next rebalance if algorithm.time < self.rebalance_time: return [] insights = [] # Enter long positions for symbol in self.algorithm.long_symbols: insights.append(Insight.price(symbol, timedelta(91), InsightDirection.UP)) # Enter short positions for symbol in self.algorithm.short_symbols: insights.append(Insight.price(symbol, timedelta(91), InsightDirection.DOWN)) # Set next rebalance time self.rebalance_time = Expiry.EndOfMonth(algorithm.time) return insights
#region imports from AlgorithmImports import * from selection import ReturnAndEarningsUniverseSelectionModel from alpha import LongShortAlphaModel #endregion class PriceEarningsMomentumAlgorithm(QCAlgorithm): ''' A stock momentum strategy based on quarterly returns and earning growth. Paper: http://papers.ssrn.com/sol3/papers.cfm?abstract_id=299107 Online copy: https://www.trendrating.com/wp-content/uploads/dlm_uploads/2019/03/momentum.pdf ''' def initialize(self): self.set_start_date(2023, 3, 1) self.set_end_date(2024, 3, 1) self.set_cash(100000) # Set Strategy Cash self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN) self.set_security_initializer(BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices))) num_of_coarse = 100 # Number of coarse selected universe self.long_symbols = [] self.short_symbols = [] self.universe_settings.data_normalization_mode = DataNormalizationMode.RAW self.universe_settings.resolution = Resolution.MINUTE # Resolution setting of universe selection self.universe_settings.schedule.on(self.date_rules.month_start()) self.universe_model = ReturnAndEarningsUniverseSelectionModel(self, num_of_coarse) self.set_universe_selection(self.universe_model) # Coarse and Fine Universe Selection self.add_alpha(LongShortAlphaModel(self)) self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel(Expiry.END_OF_WEEK)) self.add_risk_management(MaximumDrawdownPercentPerSecurity(0.2))
#region imports from AlgorithmImports import * #endregion class ReturnAndEarningsUniverseSelectionModel(FundamentalUniverseSelectionModel): """ This universe selection model refreshes monthly to contain US securities in the asset management industry. """ def __init__(self, algorithm, numOfCoarse): self.algorithm = algorithm self.num_of_coarse = numOfCoarse self.eps_by_symbol = {} # Contains RollingWindow objects of EPS for every stock super().__init__(self.select) def select(self, fundamental): """ Coarse universe selection is called each day at midnight. Input: - algorithm Algorithm instance running the backtest - coarse List of CoarseFundamental objects Returns the symbols that have fundamental data. """ # Sort the equities (prices > 5) by Dollar Volume descendingly # # Pick the top 100 liquid equities as the coarse-selected universe selectedByDollarVolume = sorted([x for x in fundamental if x.price > 5 and x.has_fundamental_data], key = lambda x: x.dollar_volume, reverse = True)[:self.num_of_coarse] symbols = [x.symbol for x in selectedByDollarVolume] # Get the quarterly returns for each symbol history = self.algorithm.history(symbols, timedelta(91), Resolution.DAILY, data_normalization_mode=DataNormalizationMode.SCALED_RAW) history = history.drop_duplicates().close.unstack(level = 0) rankByQuarterReturn = self.get_quarterly_return(history) # Get the earning growth for each symbol rankByEarningGrowth = self.get_earning_growth(selectedByDollarVolume) # Get the sum of rank for each symbol and pick the top ones to long and the bottom ones to short rankSumBySymbol = {key: rankByQuarterReturn.get(key, 0) + rankByEarningGrowth.get(key, 0) for key in set(rankByQuarterReturn) | set(rankByEarningGrowth)} # Get 10 symbols to long and short respectively sortedDict = sorted(rankSumBySymbol.items(), key = lambda x: x[1], reverse = True) self.algorithm.long_symbols = [Symbol(SecurityIdentifier.parse(x[0]), str(x[0]).split(' ')[0]) for x in sortedDict[:5]] self.algorithm.short_symbols = [Symbol(SecurityIdentifier.parse(x[0]), str(x[0]).split(' ')[0]) for x in sortedDict[-5:]] selected = self.algorithm.long_symbols + self.algorithm.short_symbols return selected def get_quarterly_return(self, history): ''' Get the rank of securities based on their quarterly return from historical close prices Return: dictionary ''' # Get quarterly returns for all symbols # (The first row divided by the last row) returns = history.iloc[0] / history.iloc[-1] # Transform them to dictionary structure returns = returns.to_dict() # Get the rank of the returns (key: symbol; value: rank) # (The symbol with the 1st quarterly return ranks the 1st, etc.) ranked = sorted(returns, key = returns.get, reverse = True) return {symbol: rank for rank, symbol in enumerate(ranked, 1)} def get_earning_growth(self, fine): ''' Get the rank of securities based on their EPS growth Return: dictionary ''' # Earning Growth by symbol egBySymbol = {} for stock in fine: # Select the securities with EPS (> 0) if stock.earning_reports.basic_eps.three_months == 0: continue # Add the symbol in the dict if not exist if not stock.symbol in self.eps_by_symbol: self.eps_by_symbol[stock.symbol] = RollingWindow[float](2) # Update the rolling window for each stock self.eps_by_symbol[stock.symbol].add(stock.earning_reports.basic_eps.three_months) # If the rolling window is ready if self.eps_by_symbol[stock.symbol].is_ready: rw = self.eps_by_symbol[stock.symbol] # Caculate the Earning Growth egBySymbol[stock.symbol] = (rw[0] - rw[1]) / rw[1] # Get the rank of the Earning Growth ranked = sorted(egBySymbol, key = egBySymbol.get, reverse = True) return {symbol: rank for rank, symbol in enumerate(ranked, 1)}