Overall Statistics |
Total Orders 410 Average Win 0.53% Average Loss -0.36% Compounding Annual Return 66.432% Drawdown 4.700% Expectancy 0.405 Start Equity 1000000 End Equity 1265931.36 Net Profit 26.593% Sharpe Ratio 2.233 Sortino Ratio 3.481 Probabilistic Sharpe Ratio 82.876% Loss Rate 44% Win Rate 56% Profit-Loss Ratio 1.49 Alpha 0.317 Beta 0.387 Annual Standard Deviation 0.175 Annual Variance 0.031 Information Ratio 1.109 Tracking Error 0.18 Treynor Ratio 1.009 Total Fees $4248.43 Estimated Strategy Capacity $510000.00 Lowest Capacity Asset ACB WYXFTA8WCGV9 Portfolio Turnover 6.89% |
#region imports from AlgorithmImports import * #endregion # Your New Python File class MomentumAlpha(AlphaModel): def __init__( self, long_period=252, #12-month short_period=21, #1-month long_percent=0.2, #quintile short_percent=0.2 ): self.long_period = long_period self.short_period = short_period self.long_percent = long_percent self.short_percent = short_percent self.mom_by_symbol = {} self.last_month = -1 def update(self, algorithm, data): # emit monthly insight for now if self.last_month == algorithm.Time.month: return [] self.last_month = algorithm.Time.month mom_scores = [] for symbol in self.mom_by_symbol: if self.mom_by_symbol[symbol].is_ready(): #algorithm.log(f"{algorithm.Time}: {symbol} is ready") long_momentum = self.mom_by_symbol[symbol].get_momentum_percent(self.short_period, self.long_period-1) short_momentum = self.mom_by_symbol[symbol].get_momentum_percent(0, self.short_period-1) mom_scores.append([symbol, long_momentum, short_momentum]) # size_in_bytes = sys.getsizeof(self.mom_by_symbol[symbol]) # algorithm.log(f"mom rolling window size: {size_in_bytes}") if not mom_scores: return [] def assign_quintiles(data): return pd.qcut(data, 5, labels=[1, 2, 3, 4, 5]) # algorithm.log(f"{algorithm.Time}: Number of assets available is {len(mom_scores)}") mom_scores_df = pd.DataFrame(mom_scores, columns=['symbol', 'long_momentum', 'short_momentum']) mom_scores_df['long_momentum_quintile'] = assign_quintiles(mom_scores_df['long_momentum']) mom_scores_df['short_momentum_quintile'] = assign_quintiles(mom_scores_df['short_momentum']) long_stocks = mom_scores_df[(mom_scores_df['long_momentum_quintile'] == 5) & (mom_scores_df['short_momentum_quintile'] == 1)] short_stocks = mom_scores_df[(mom_scores_df['long_momentum_quintile'] == 1) & (mom_scores_df['short_momentum_quintile'] == 5)] algorithm.log(f"Long Stocks: {long_stocks['symbol']}") algorithm.log(f"Short Stocks: {short_stocks['symbol']}") insights = [] for symbol in long_stocks['symbol'].unique(): insights.append(Insight.Price(symbol, timedelta(days=20), InsightDirection.Up, None, None)) for symbol in short_stocks['symbol'].unique(): insights.append(Insight.Price(symbol, timedelta(days=20), InsightDirection.Down, None, None)) return insights def on_securities_changed(self, algorithm, changes): for security in changes.RemovedSecurities: # algorithm.log(f"{algorithm.time}: Removed {security.Symbol}") if security.Symbol in self.mom_by_symbol: del self.mom_by_symbol[security.Symbol] for security in changes.AddedSecurities: # algorithm.log(f"{algorithm.time}: Added {security.Symbol}") if security not in self.mom_by_symbol: history_by_symbol = algorithm.History(security.Symbol, self.long_period, Resolution.DAILY) self.mom_by_symbol[security.Symbol] = LongMomentumShortReversalIndicator(security.Symbol, self.long_period, algorithm, history_by_symbol) #register indicator for daily update algorithm.RegisterIndicator(security.Symbol, self.mom_by_symbol[security.Symbol], Resolution.DAILY) del history_by_symbol class LongMomentumShortReversalIndicator(PythonIndicator): def __init__(self, symbol, period, algorithm, history): self.symbol = symbol self.period = period self.value = 0 self.rollingWindow = RollingWindow[float](self.period) #warm up indicator with history if not history.empty: for _, tradebar in history.loc[symbol].iterrows(): self.update(tradebar) # algorithm.log(f"{algorithm.Time}: {symbol}") def update(self, bar): self.rollingWindow.add(bar.close) return self.rollingWindow.is_ready def is_ready(self): return self.rollingWindow.is_ready # percentage momentum between time_0 and time_n calculated as: # (price[time_0] - price[time_n]) / price[time_n] def get_momentum_percent(self, time_0, time_n): return 100 * (self.rollingWindow[time_0] - self.rollingWindow[time_n]) / self.rollingWindow[time_n]
# region imports from AlgorithmImports import * from AlphaModels import MomentumAlpha # endregion class CompetitionTemplate(QCAlgorithm): def Initialize(self): self.set_start_date(2024, 1, 1) self.set_cash(1000000) self.last_month = -1 self.num_coarse = 1000 self.universe_settings.Resolution = Resolution.DAILY self.add_universe(self.CoarseSelectionFunction, self.FineSelectionFunction) # self.add_alpha(NullAlphaModel()) self.add_alpha(MomentumAlpha()) #customized alpha model self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel(self.IsRebalanceDue)) self.set_risk_management(NullRiskManagementModel()) self.set_execution(ImmediateExecutionModel()) #universe rebalance: monthly def IsRebalanceDue(self, time): if time.month == self.last_month: return None self.last_month = time.month return time #coarse selection: 1000 most liquid equity def CoarseSelectionFunction(self, coarse): if not self.IsRebalanceDue(self.Time): return Universe.UNCHANGED sorted_by_dollar_volume = sorted([x for x in coarse if x.HasFundamentalData], key=lambda x: x.DollarVolume, reverse=True) return [x.Symbol for x in sorted_by_dollar_volume if x.Price > 5][:self.num_coarse] #fine selection: sorted by market cap def FineSelectionFunction(self, fine): sorted_by_market_cap = sorted(fine, key=lambda f: f.market_cap, reverse=False) #nano cap return [f.Symbol for f in sorted_by_market_cap][:int(self.num_coarse*0.2)]