| 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]