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