| Overall Statistics |
|
Total Orders 145 Average Win 1.32% Average Loss -1.15% Compounding Annual Return 78.167% Drawdown 18.800% Expectancy 0.364 Start Equity 1000000 End Equity 1285983.74 Net Profit 28.598% Sharpe Ratio 1.738 Sortino Ratio 2.507 Probabilistic Sharpe Ratio 68.012% Loss Rate 37% Win Rate 63% Profit-Loss Ratio 1.15 Alpha 0.273 Beta 1.413 Annual Standard Deviation 0.287 Annual Variance 0.083 Information Ratio 1.314 Tracking Error 0.258 Treynor Ratio 0.353 Total Fees $1554.39 Estimated Strategy Capacity $24000000.00 Lowest Capacity Asset CYTK SY8OYP5ZLDUT Portfolio Turnover 7.39% |
#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])
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]
algorithm.liquidate(security.Symbol)
for security in changes.AddedSecurities:
# algorithm.log(f"{algorithm.time}: Added {security.Symbol}")
if security not in self.mom_by_symbol:
self.mom_by_symbol[security.Symbol] = LongMomentumShortReversalIndicator(security.Symbol, period=self.long_period)
#warm up indicator with history
history_by_symbol = algorithm.History(security.Symbol, self.long_period, Resolution.DAILY)
for _, row in history_by_symbol.loc[security.Symbol].iterrows():
bar = TradeBar(row.name, security.Symbol, row['open'], row['high'], row['low'], row['close'], row['volume'])
self.mom_by_symbol[security.Symbol].update(bar)
#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):
self.symbol = symbol
self.period = period
self.value = 0
self.rollingWindow = RollingWindow[float](self.period)
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())
self.set_warm_up(7)
#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]