# region imports
from AlgorithmImports import *
from optimization import SharpePortfolioOptimizer
# endregion
class LazyPricesStrategy(QCAlgorithm):
def initialize(self):
self.set_start_date(2015, 1, 1)
self.set_end_date(2026, 6, 1)
self.set_cash(1_000_000)
self.settings.seed_initial_prices = True
self.settings.min_absolute_portfolio_target_percentage = 0
self._optimizer = SharpePortfolioOptimizer()
# Select the universe monthly to match the rebalance cadence.
self.universe_settings.resolution = Resolution.DAILY
self.universe_settings.schedule.on(self.date_rules.month_end("SPY"))
# Collect the 100 most liquid Equities, then rank them by Brain filing textual similarity.
self._universe = self.add_universe(BrainCompanyFilingLanguageMetricsUniverseAll, self._select_assets)
def on_warmup_finished(self):
# Rebalance on the last trading day of each month.
time_rule = self.time_rules.at(8, 0)
self.schedule.on(self.date_rules.month_end("SPY"), time_rule, self._rebalance)
# Rebalance today too.
if self.live_mode:
self._rebalance()
else:
self.schedule.on(self.date_rules.today, time_rule, self._rebalance)
def _select_assets(self, filings):
similarity_by_symbol = {}
# Scan the past 6 months of 10-K/10-Q filings for similarity scores.
history = self.history(self._universe, timedelta(30 * 6), Resolution.DAILY)
for daily_filings in history:
for filing in daily_filings:
# Prefer risk factors similarity scores, else the full report similarity scores.
similarity = filing.risk_factors_statement_sentiment.similarity.all or filing.report_sentiment.similarity.all
if similarity:
similarity_by_symbol[filing.symbol] = similarity
# Rank by similarity and keep the long-only top 10 most textually stable filers.
return sorted(similarity_by_symbol, key=lambda symbol: similarity_by_symbol[symbol])[-10:]
def _rebalance(self):
if not self._universe.selected:
return
# Run portfolio optimization on the selected long-only 10 asset portfolio.
weight_by_symbol = self._optimizer.get_weights(self, list(self._universe.selected))
if not weight_by_symbol:
return
targets = [PortfolioTarget(symbol, weight) for symbol, weight in weight_by_symbol.items() if self.securities[symbol].price]
self.set_holdings(targets, True)
# region imports
from AlgorithmImports import *
from Portfolio.MaximumSharpeRatioPortfolioOptimizer import MaximumSharpeRatioPortfolioOptimizer # type: ignore
# endregion
class SharpePortfolioOptimizer:
def __init__(self, period: int = 252):
self._period = period
self._optimizer = MaximumSharpeRatioPortfolioOptimizer(0, 1)
def get_weights(self, algorithm: QCAlgorithm, symbols: list) -> dict:
history = algorithm.history(symbols, self._period + 1, Resolution.DAILY)
if history.empty:
return {}
returns = history["close"].unstack(level=0).pct_change().dropna()
if returns.empty or returns.shape[1] < 2:
return {}
# Maximize the portfolio Sharpe ratio using the long-only optimizer.
raw_weights = self._optimizer.optimize(returns)
return {symbol: float(raw_weights[i]) for i, symbol in enumerate(returns.columns)}