Overall Statistics |
Total Orders 133 Average Win 1.48% Average Loss -1.11% Compounding Annual Return 91.891% Drawdown 17.800% Expectancy 0.454 Start Equity 1000000 End Equity 1328222.18 Net Profit 32.822% Sharpe Ratio 2.036 Sortino Ratio 2.721 Probabilistic Sharpe Ratio 73.977% Loss Rate 38% Win Rate 62% Profit-Loss Ratio 1.33 Alpha 0.362 Beta 1.365 Annual Standard Deviation 0.285 Annual Variance 0.081 Information Ratio 1.634 Tracking Error 0.257 Treynor Ratio 0.425 Total Fees $1508.35 Estimated Strategy Capacity $25000000.00 Lowest Capacity Asset CYTK SY8OYP5ZLDUT Portfolio Turnover 7.04% |
#region imports from AlgorithmImports import * #endregion # Your New Python File class MomentumAlpha(AlphaModel): def __init__( self, long_period=252, short_period=21, long_percent=0.2, 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 [] # 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_rank'] = mom_scores_df['long_momentum'].rank(ascending=False) #high long momentum receives higher(smaller) rank mom_scores_df['long_bucket'] = pd.qcut(mom_scores_df['long_rank'], 10, labels=False) + 1 highest_rank_bucket = mom_scores_df[mom_scores_df['long_bucket'] == 1] sorted_by_short_momentum = highest_rank_bucket.sort_values(by='short_momentum', ascending=True) insights = [] num_long = int(self.long_percent * len(sorted_by_short_momentum)) for index, row in sorted_by_short_momentum.iloc[:num_long].iterrows(): symbol = row['symbol'] insights.append(Insight.Price(symbol, timedelta(days=10), InsightDirection.Up)) del mom_scores_df, highest_rank_bucket, sorted_by_short_momentum 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) # size_in_bytes = sys.getsizeof(history_by_symbol) # algorithm.log(f"mom rolling window size: {size_in_bytes / (1024 * 1024)}") 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 = 500 self.universe_settings.Resolution = Resolution.DAILY self.add_universe(self.CoarseSelectionFunction, self.FineSelectionFunction) 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 selected = sorted([x for x in coarse if x.HasFundamentalData], key=lambda x: x.DollarVolume, reverse=True) return [x.Symbol for x in selected[: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=True) return [f.Symbol for f in sorted_by_market_cap]