| Overall Statistics |
|
Total Orders 619 Average Win 1.32% Average Loss -0.96% Compounding Annual Return 14.534% Drawdown 33.000% Expectancy 0.250 Start Equity 1000000 End Equity 1969941.16 Net Profit 96.994% Sharpe Ratio 0.382 Sortino Ratio 0.467 Probabilistic Sharpe Ratio 3.893% Loss Rate 47% Win Rate 53% Profit-Loss Ratio 1.38 Alpha 0.031 Beta 0.918 Annual Standard Deviation 0.222 Annual Variance 0.049 Information Ratio 0.149 Tracking Error 0.18 Treynor Ratio 0.092 Total Fees $3135.98 Estimated Strategy Capacity $230000000.00 Lowest Capacity Asset UNPH R735QTJ8XC9X Portfolio Turnover 3.88% Drawdown Recovery 836 |
# region imports
from AlgorithmImports import *
from optimization import SharpePortfolioOptimizer
# endregion
class LazyPricesStrategy(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(self.end_date - timedelta(5*365))
self.set_cash(1_000_000)
self.settings.seed_initial_prices = True
self.settings.min_absolute_portfolio_target_percentage = 0
self._optimizer = SharpePortfolioOptimizer()
self._fundamentals = []
# Select the universe monthly to match the rebalance cadence.
self.universe_settings.resolution = Resolution.DAILY
self._date_rule = self.date_rules.month_start("SPY")
self.universe_settings.schedule.on(self._date_rule)
# 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)
self.set_warm_up(timedelta(45))
def on_warmup_finished(self) -> None:
# Rebalance on the last trading day of each month.
time_rule = self.time_rules.at(8, 0)
self.schedule.on(self._date_rule, 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, fundamentals: List[Fundamental]) -> Universe.UnchangedUniverse:
# Store the 100 most liquid Equities; the Brain filing universe ranks within this set.
self._fundamentals = [f.symbol for f in sorted([f for f in fundamentals if f.has_fundamental_data], key=lambda f: f.dollar_volume)[-100:]]
return Universe.UNCHANGED
def _select_assets(self, filings: List[BrainCompanyFilingLanguageMetricsUniverseAll]) -> List[Symbol]:
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._fundamentals:
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
# Return the top 10 by highest similarity.
return sorted(similarity_by_symbol, key=lambda symbol: similarity_by_symbol[symbol])[-25:]
def _rebalance(self) -> None:
if self.is_warming_up or 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
# Invest into a long only portfolio with weights optimized for sharpe.
targets = [PortfolioTarget(symbol, weight) for symbol, weight in weight_by_symbol.items()]
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)}