Overall Statistics
Total Orders
785
Average Win
0.97%
Average Loss
-0.89%
Compounding Annual Return
8.054%
Drawdown
34.900%
Expectancy
0.238
Start Equity
1000000
End Equity
2422967.16
Net Profit
142.297%
Sharpe Ratio
0.304
Sortino Ratio
0.352
Probabilistic Sharpe Ratio
2.251%
Loss Rate
41%
Win Rate
59%
Profit-Loss Ratio
1.08
Alpha
-0.011
Beta
0.591
Annual Standard Deviation
0.12
Annual Variance
0.014
Information Ratio
-0.427
Tracking Error
0.103
Treynor Ratio
0.062
Total Fees
$6594.59
Estimated Strategy Capacity
$240000000.00
Lowest Capacity Asset
IDPH R735QTJ8XC9X
Portfolio Turnover
0.86%
Drawdown Recovery
518
# region imports
from AlgorithmImports import *
# 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._assets_per_side = 10
        self.universe_settings.resolution = Resolution.DAILY
        self._style_boxes = [
            StyleBox.MID_VALUE, StyleBox.MID_CORE, StyleBox.MID_GROWTH,
            StyleBox.SMALL_VALUE, StyleBox.SMALL_CORE, StyleBox.SMALL_GROWTH
        ]
        self._universe = self.add_universe(self._fundamental_filter)

    def _fundamental_filter(self, fundamental):
        # Keep the most liquid mid/small-cap names that have fundamental data.
        eligible = [
            f for f in fundamental
            if f.has_fundamental_data and f.asset_classification.style_box in self._style_boxes
        ]
        return [f.symbol for f in sorted(eligible, key=lambda f: f.dollar_volume)[-100:]]

    def on_warmup_finished(self):
        # Rebalance on the first trading day of each quarter at 8 AM.
        time_rule = self.time_rules.at(8, 0)
        self.schedule.on(self.date_rules.quarter_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 on_securities_changed(self, changes):
        for security in changes.added_securities:
            security.brain_10K = self.add_data(BrainCompanyFilingLanguageMetrics10K, security.symbol, Resolution.DAILY).symbol
            security.brain_10Q = self.add_data(BrainCompanyFilingLanguageMetricsAll, security.symbol, Resolution.DAILY).symbol

    def _rebalance(self):
        if self.is_warming_up or not self._universe.selected:
            return
        similarity_by_symbol = {}
        for symbol in self._universe.selected:
            security = self.securities[symbol]
            # Fetch 10K and 10Q filings over 500 days and merge them sorted by date to find the most recent similarity score.
            history_10k = list(self.history[BrainCompanyFilingLanguageMetrics10K](security.brain_10K, timedelta(days=500), Resolution.DAILY))
            history_10q = list(self.history[BrainCompanyFilingLanguageMetricsAll](security.brain_10Q, timedelta(days=500), Resolution.DAILY))
            history = sorted(history_10k + history_10q, key=lambda p: p.end_time)[::-1]
            score = None
            # Search for the most recent valid similarity score, preferring risk factors statement over full report sentiment.
            for point in history:
                if point.risk_factors_statement_sentiment.similarity.all is not None:
                    score = float(point.risk_factors_statement_sentiment.similarity.all)
                elif point.report_sentiment.similarity.all is not None:
                    score = float(point.report_sentiment.similarity.all)
                if score is not None:
                    break
            if score is not None:
                similarity_by_symbol[symbol] = score
        if len(similarity_by_symbol) < 2 * self._assets_per_side:
            return
        # Rank by similarity ascending: low scores (textual divergence) signal shorts, high scores (convergence) signal longs.
        ranked = sorted(similarity_by_symbol, key=similarity_by_symbol.get)
        weight = 0.5 / self._assets_per_side
        targets = [PortfolioTarget(s, weight) for s in ranked[-self._assets_per_side:]]
        self.set_holdings(targets, True)