| Overall Statistics |
|
Total Orders 471 Average Win 2.65% Average Loss -1.52% Compounding Annual Return 28.294% Drawdown 38.300% Expectancy 0.506 Start Equity 1000000 End Equity 4952036.69 Net Profit 395.204% Sharpe Ratio 0.711 Sortino Ratio 0.799 Probabilistic Sharpe Ratio 21.494% Loss Rate 45% Win Rate 55% Profit-Loss Ratio 1.74 Alpha 0.109 Beta 1.069 Annual Standard Deviation 0.289 Annual Variance 0.083 Information Ratio 0.51 Tracking Error 0.226 Treynor Ratio 0.192 Total Fees $7318.09 Estimated Strategy Capacity $150000000.00 Lowest Capacity Asset APLS WPES8QY5PX0L Portfolio Turnover 3.74% Drawdown Recovery 1001 |
# region imports
from AlgorithmImports import *
from optimization import SharpePortfolioOptimizer
# endregion
class LazyPricesStrategy(QCAlgorithm):
def initialize(self):
self.set_start_date(2020, 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()
self._fundamental = []
# Select the universe monthly to match the rebalance cadence.
self.universe_settings.resolution = Resolution.DAILY
self.universe_settings.schedule.on(self.date_rules.month_start("SPY"))
# Collect the 100 most liquid Equities, then rank them by Brain filing textual similarity.
self.add_universe(self._fundamental_filter)
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_start("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 _fundamental_filter(self, fundamental):
# Store the 100 most liquid Equities; the Brain filing universe ranks within this set.
self._fundamental = [f.symbol for f in sorted([f for f in fundamental if f.has_fundamental_data], key=lambda f: f.dollar_volume)[-100:]]
return Universe.UNCHANGED
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:
if filing.symbol not in self._fundamental:
continue
# 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)}