Overall Statistics
Total Orders
270
Average Win
3.19%
Average Loss
-1.20%
Compounding Annual Return
51.197%
Drawdown
30.400%
Expectancy
0.986
Start Equity
1000000
End Equity
4108676.68
Net Profit
310.868%
Sharpe Ratio
1.222
Sortino Ratio
1.51
Probabilistic Sharpe Ratio
68.540%
Loss Rate
46%
Win Rate
54%
Profit-Loss Ratio
2.66
Alpha
0.195
Beta
1.136
Annual Standard Deviation
0.265
Annual Variance
0.07
Information Ratio
0.939
Tracking Error
0.224
Treynor Ratio
0.285
Total Fees
$3636.55
Estimated Strategy Capacity
$150000000.00
Lowest Capacity Asset
APLS WPES8QY5PX0L
Portfolio Turnover
4.07%
Drawdown Recovery
266
# region imports
from AlgorithmImports import *
from optimization import SharpePortfolioOptimizer
# endregion


class LazyPricesStrategy(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2023, 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)
        # self.schedule.on(self.date_rules.month_start("SPY"), self.time_rules.at(8, 0), self._rebalance)

    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)}