| Overall Statistics |
|
Total Orders 4741 Average Win 0.20% Average Loss -0.15% Compounding Annual Return 18.501% Drawdown 23.600% Expectancy 0.207 Start Equity 100000 End Equity 205900.95 Net Profit 105.901% Sharpe Ratio 0.62 Sortino Ratio 0.693 Probabilistic Sharpe Ratio 21.350% Loss Rate 48% Win Rate 52% Profit-Loss Ratio 1.33 Alpha 0.039 Beta 0.965 Annual Standard Deviation 0.203 Annual Variance 0.041 Information Ratio 0.359 Tracking Error 0.1 Treynor Ratio 0.13 Total Fees $4774.45 Estimated Strategy Capacity $20000000.00 Lowest Capacity Asset GDDY VZCF7NCASX2D Portfolio Turnover 11.21% |
from AlgorithmImports import *
from QuantConnect.DataSource import *
import statistics
# TODO:
# - combine ranking values from various timeframes
# - combine with Brain Language Metrics and Sentiment Indicator
class BrainMLRankingDataAlgorithm(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2020, 1, 1)
self.set_end_date(2024, 4, 1)
self.set_cash(100000)
self.position_keep_days = int(self.GetParameter("position_keep_days")) if self.GetParameter("position_keep_days") else 3
self.sentiment_days = int(self.GetParameter("sentiment_days")) if self.GetParameter("sentiment_days") else 7
self.no_positions = int(self.GetParameter("no_positions")) if self.GetParameter("no_positions") else 10
self.universe_size = int(self.GetParameter("universe_size")) if self.GetParameter("universe_size") else 100
self.do_short = int(self.GetParameter("do_short")) if self.GetParameter("do_short") else 1
self.ranking_weight = float(self.GetParameter("ranking_weight")) if self.GetParameter("ranking_weight") else 0.33
self.sentiment_weight = float(self.GetParameter("sentiment_weight")) if self.GetParameter("sentiment_weight") else 0.33
self.company_filing_weight = float(self.GetParameter("company_filing_weight")) if self.GetParameter("company_filing_weight") else 0.33
self.Debug(f"position_keep_days: {self.position_keep_days}")
self.Debug(f"sentiment_days: {self.sentiment_days}")
self.Debug(f"no_positions: {self.no_positions}")
self.Debug(f"universe_size: {self.universe_size}")
self.Debug(f"do_short: {self.do_short}")
self.Debug(f"ranking_weight: {self.ranking_weight}")
self.Debug(f"sentiment_weight: {self.sentiment_weight}")
self.Debug(f"company_filing_weight: {self.company_filing_weight}")
if self.do_short == 0:
self.no_positions *= 2
self.brain_stock_ranking_class = self.get_brain_stock_ranking_class()
self.brain_sentiment_class = self.get_brain_sentiment_class()
self.brain_filing_class = BrainCompanyFilingLanguageMetrics10K
self.UniverseSettings.Resolution = Resolution.Daily
self.SetUniverseSelection(FineFundamentalUniverseSelectionModel(self.SelectCoarse, self.SelectFine))
self.symbol_str_to_symbol = {}
self.symbol_by_ranking_symbol = {}
self.symbol_by_sentiment_symbol = {}
self.symbol_by_filing_symbol = {}
self.history_14_days = {}
self.atr_14_days = {}
self.open_dates = {}
benchmark_chart = Chart("Benchmark SPY")
benchmark_chart.AddSeries(Series("SPY", SeriesType.Line))
benchmark_chart.AddSeries(Series("Portfolio", SeriesType.Line))
self.add_chart(benchmark_chart)
self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol # for benchmarking
self.starting_spy_price = None
self.starting_portfolio_value = None
rank_chart = Chart("Brain Stock Ranking")
rank_chart.AddSeries(Series("Max Rank", SeriesType.Line))
rank_chart.AddSeries(Series("Min Rank", SeriesType.Line))
self.add_chart(rank_chart)
sentiment_chart = Chart("Brain Sentiment Indicator")
sentiment_chart.AddSeries(Series("Max Sentiment", SeriesType.Line))
sentiment_chart.AddSeries(Series("Min Sentiment", SeriesType.Line))
self.add_chart(sentiment_chart)
filing_chart = Chart("Brain Company Filing Language Metrics")
filing_chart.AddSeries(Series("Max Sentiment", SeriesType.Line))
filing_chart.AddSeries(Series("Min Sentiment", SeriesType.Line))
filing_chart.AddSeries(Series("Max Uncertainty", SeriesType.Line))
filing_chart.AddSeries(Series("Min Uncertainty", SeriesType.Line))
filing_chart.AddSeries(Series("Max Litigious", SeriesType.Line))
filing_chart.AddSeries(Series("Min Litigious", SeriesType.Line))
self.add_chart(filing_chart)
self.set_warmup(14)
def get_brain_stock_ranking_class(self):
if self.position_keep_days <= 2:
return BrainStockRanking2Day
elif self.position_keep_days <= 3:
return BrainStockRanking3Day
elif self.position_keep_days <= 5:
return BrainStockRanking5Day
elif self.position_keep_days <= 10:
return BrainStockRanking10Day
elif self.position_keep_days <= 21:
return BrainStockRanking21Day
else:
raise Exception("Invalid position_keep_days")
def get_brain_sentiment_class(self):
if self.sentiment_days <= 7:
return BrainSentimentIndicator30Day
elif self.position_keep_days <= 30:
return BrainSentimentIndicator30Day
else:
raise Exception("Invalid sentiment_days")
def SelectCoarse(self, coarse):
filtered_coarse = [x for x in coarse if x.price > 5]
filtered_coarse = [x for x in filtered_coarse if x.volume > 1_000_000]
sortedByDollarVolume = sorted(filtered_coarse, key=lambda x: x.dollar_volume, reverse=True)
symbols = [ x.Symbol for x in sortedByDollarVolume[:self.universe_size] ]
return symbols
def SelectFine(self, fine):
# we already have our self.universe_size symbols from coarse selection, no further selection needed
return [ x.Symbol for x in fine ]
def OnSecuritiesChanged(self, changes):
for added in changes.AddedSecurities:
symbol = added.Symbol
self.symbol_str_to_symbol[symbol.value] = symbol
ranking_dataset_symbol = self.add_data(self.brain_stock_ranking_class, symbol).symbol
sentiment_dataset_symbol = self.add_data(self.brain_sentiment_class, symbol).symbol
filing_dataset_symbol = self.add_data(self.brain_filing_class, symbol).symbol
self.symbol_by_ranking_symbol[ranking_dataset_symbol] = symbol
self.symbol_by_sentiment_symbol[sentiment_dataset_symbol] = symbol
self.symbol_by_filing_symbol[filing_dataset_symbol] = symbol
self.history_14_days[symbol.value] = RollingWindow[TradeBar](14)
self.atr_14_days[symbol.value] = AverageTrueRange(14)
# warm up the ATR indicator and the history
history = self.history[TradeBar](symbol, 14, Resolution.DAILY)
for bar in history:
self.history_14_days[symbol.value].Add(bar)
self.atr_14_days[symbol.value].Update(bar)
#self.Debug(f"added {symbol} with dataset symbol {dataset_symbol}")
for removed in changes.RemovedSecurities:
symbol = removed.Symbol
if symbol.value in self.symbol_str_to_symbol:
del self.symbol_str_to_symbol[symbol.value]
if symbol in self.symbol_by_ranking_symbol:
del self.symbol_by_ranking_symbol[symbol]
if symbol in self.symbol_by_sentiment_symbol:
del self.symbol_by_sentiment_symbol[symbol]
if symbol in self.symbol_by_filing_symbol:
del self.symbol_by_filing_symbol[symbol]
if symbol.value in self.history_14_days:
del self.history_14_days[symbol.value]
if symbol.value in self.atr_14_days:
del self.atr_14_days[symbol.value]
def on_data(self, slice: Slice) -> None:
for symbol, trade_bar in slice.Bars.items():
#self.Debug(f"adding {symbol} to history {symbol in self.history_14_days} {symbol in self.atr_14_days}")
if symbol in self.history_14_days:
self.history_14_days[symbol.value].Add(trade_bar)
if symbol in self.atr_14_days:
self.atr_14_days[symbol.value].Update(trade_bar)
if self.is_warming_up:
return
if self.spy in slice.Bars:
spy_price = slice[self.spy].Close
if self.starting_spy_price is None:
self.starting_spy_price = spy_price
self.starting_portfolio_value = self.Portfolio.TotalPortfolioValue
normalized_spy = spy_price / self.starting_spy_price
normalized_portfolio = self.Portfolio.TotalPortfolioValue / self.starting_portfolio_value
self.Plot("Benchmark SPY", "SPY", normalized_spy)
self.Plot("Benchmark SPY", "Portfolio", normalized_portfolio)
# Collect rankings for all symbols
ranking_points = slice.Get(self.brain_stock_ranking_class)
sentiment_points = slice.Get(self.brain_sentiment_class)
filing_points = slice.Get(self.brain_filing_class)
if ranking_points is None or len(ranking_points) == 0:
self.Debug("No ranking points")
return
# Create new dictionaries
ranking_points_dict = {point.symbol.Value: point for point in ranking_points.Values if point.symbol in self.symbol_by_ranking_symbol}
rank_values = [point.rank for point in ranking_points_dict.values()]
min_rank, max_rank = min(rank_values), max(rank_values)
self.Plot("Brain Stock Ranking", "Max Rank", max_rank)
self.Plot("Brain Stock Ranking", "Min Rank", min_rank)
if sentiment_points is None or len(sentiment_points) == 0:
sentiment_points_dict = {}
sentiment_values = []
min_sentiment, max_sentiment = 0, 0
else:
sentiment_points_dict = {point.symbol.Value: point for point in sentiment_points.Values if point.symbol in self.symbol_by_sentiment_symbol}
sentiment_values = [point.sentiment for point in sentiment_points_dict.values()]
min_sentiment, max_sentiment = min(sentiment_values), max(sentiment_values)
self.Plot("Brain Sentiment Indicator", "Max Sentiment", max_sentiment)
self.Plot("Brain Sentiment Indicator", "Min Sentiment", min_sentiment)
if filing_points is None or len(filing_points) == 0:
filing_points_dict = {}
filing_values = []
min_filing, max_filing = 0, 0
else:
filing_points_dict = {point.symbol.Value: point for point in filing_points.Values if point.symbol in self.symbol_by_filing_symbol}
filing_sentiment_values = [point.report_sentiment.sentiment for point in filing_points_dict.values()]
filing_uncertainity_values = [point.report_sentiment.uncertainty for point in filing_points_dict.values()]
filing_litigious_values = [point.report_sentiment.litigious for point in filing_points_dict.values()]
min_filing_sentiment, max_filing_sentiment = min(filing_sentiment_values), max(filing_sentiment_values)
min_filing_uncertainty, max_filing_uncertainty = min(filing_uncertainity_values), max(filing_uncertainity_values)
min_filing_litigious, max_filing_litigious = min(filing_litigious_values), max(filing_litigious_values)
self.Plot("Brain Company Filing Language Metrics", "Max Sentiment", max_filing_sentiment)
self.Plot("Brain Company Filing Language Metrics", "Min Sentiment", min_filing_sentiment)
self.Plot("Brain Company Filing Language Metrics", "Max Uncertainty", max_filing_uncertainty)
self.Plot("Brain Company Filing Language Metrics", "Min Uncertainty", min_filing_uncertainty)
self.Plot("Brain Company Filing Language Metrics", "Max Litigious", max_filing_litigious)
self.Plot("Brain Company Filing Language Metrics", "Min Litigious", min_filing_litigious)
filing_values = filing_sentiment_values + filing_uncertainity_values + filing_litigious_values
min_filing, max_filing = min(filing_values), max(filing_values)
symbol_rank_pairs = []
for symbol in ranking_points_dict.keys():
rank = ranking_points_dict[symbol]
sentiment = sentiment_points_dict[symbol] if symbol in sentiment_points_dict else None
filing = filing_points_dict[symbol] if symbol in filing_points_dict else None
ranking_weight = self.ranking_weight
normalized_rank = (rank.rank - min_rank) / (max_rank - min_rank) if max_rank != min_rank else 0
if sentiment is None or max_sentiment == min_sentiment:
normalized_sentiment = 0
ranking_weight += self.sentiment_weight
else:
normalized_sentiment = (sentiment.sentiment - min_sentiment) / (max_sentiment - min_sentiment)
if filing is None or max_filing == min_filing:
normalized_filing = 0
ranking_weight += self.company_filing_weight
else:
normalized_filing = (filing.report_sentiment.sentiment - min_filing) / (max_filing - min_filing)
combined_rank = ranking_weight * normalized_rank + self.sentiment_weight * normalized_sentiment + self.company_filing_weight * normalized_filing
symbol_rank_pairs.append((symbol, combined_rank))
if len(symbol_rank_pairs) == 0:
self.Debug("No symbols with rank after step 1")
return []
symbol_rank_pairs = [p for p in symbol_rank_pairs if p[0] in self.history_14_days and self.history_14_days[p[0]].Count >= 14]
if len(symbol_rank_pairs) == 0:
self.Debug("No symbols with rank after step 2")
return []
symbol_rank_pairs = [p for p in symbol_rank_pairs if self.history_14_days[p[0]][0].close >= 5]
if len(symbol_rank_pairs) == 0:
self.Debug("No symbols with rank after step 3")
return []
symbol_rank_pairs = [p for p in symbol_rank_pairs if statistics.mean(bar.volume for bar in self.history_14_days[p[0]]) >= 1_000_000]
if len(symbol_rank_pairs) == 0:
self.Debug("No symbols with rank after step 4")
return []
symbol_rank_pairs = [p for p in symbol_rank_pairs if self.atr_14_days[p[0]][0].value >= 0.5]
if len(symbol_rank_pairs) == 0:
self.Debug("No symbols with rank after step 5")
return []
sorted_symbols = sorted(symbol_rank_pairs, key=lambda x: x[1], reverse=True)
top_symbols = sorted_symbols[:int(self.no_positions/2)]
bottom_symbols = sorted_symbols[-int(self.no_positions/2):]
still_holding = 0
for holding in self.Portfolio.Values:
if holding.Invested and holding.Symbol.value not in [symbol for symbol, rank in top_symbols] and holding.Symbol.value not in [symbol for symbol, rank in bottom_symbols]:
open_date = self.open_dates.get(holding.Symbol.value)
if open_date is None:
# if we don't have an open date, this position should be here
self.set_holdings(holding.Symbol, 0)
elif (self.Time - open_date).days >= self.position_keep_days:
self.set_holdings(holding.Symbol, 0)
del self.open_dates[holding.Symbol.value]
#self.Debug(f"liquidate {holding.Symbol.value}")
else:
still_holding += 1
#if open_date is None:
# self.Debug(f"open_date not none {holding.Symbol.value}")
total_invested = sum([abs(holding.HoldingsValue) for holding in self.Portfolio.Values if holding.Invested])
portfolio_percentage_used = total_invested / self.Portfolio.TotalPortfolioValue
if portfolio_percentage_used > 1.0:
return
new_positions = self.no_positions - still_holding
top_symbols = top_symbols[:int(new_positions/2)]
bottom_symbols = bottom_symbols[-int(new_positions/2):]
#self.Debug(f"bottom_symbols: {len(bottom_symbols)}")
# Place orders
sum_ranks = sum(abs(rank) for symbol, rank in top_symbols)
if self.do_short:
sum_ranks += sum(abs(rank) for symbol, rank in bottom_symbols)
for symbol, rank in top_symbols:
weight = (abs(rank) / sum_ranks) * (1 - portfolio_percentage_used)
self.set_holdings(self.symbol_str_to_symbol[symbol], weight)
self.open_dates[symbol] = self.Time
#self.Debug(f"long {symbol}")
if self.do_short > 0:
for symbol, rank in bottom_symbols:
weight = (-(abs(rank) / sum_ranks)) * (1 - portfolio_percentage_used)
self.set_holdings(self.symbol_str_to_symbol[symbol], weight)
self.open_dates[symbol] = self.Time
#self.Debug(f"short {symbol}")