Overall Statistics
Total Orders
1419
Average Win
0.72%
Average Loss
-0.62%
Compounding Annual Return
3.997%
Drawdown
15.800%
Expectancy
0.108
Start Equity
1000000
End Equity
1564737.43
Net Profit
56.474%
Sharpe Ratio
0.058
Sortino Ratio
0.069
Probabilistic Sharpe Ratio
0.764%
Loss Rate
49%
Win Rate
51%
Profit-Loss Ratio
1.17
Alpha
-0.001
Beta
0.066
Annual Standard Deviation
0.074
Annual Variance
0.005
Information Ratio
-0.491
Tracking Error
0.155
Treynor Ratio
0.064
Total Fees
$7632.23
Estimated Strategy Capacity
$360000000.00
Lowest Capacity Asset
APLS WPES8QY5PX0L
Portfolio Turnover
1.50%
Drawdown Recovery
853
# 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._universe = self.add_universe(lambda fundamental:
            [f.symbol for f in sorted([f for f in fundamental if f.has_fundamental_data], 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)
        short_symbols = ranked[:self._assets_per_side]
        long_symbols = ranked[-self._assets_per_side:]
        weight = 0.5 / self._assets_per_side
        targets = [PortfolioTarget(s, weight) for s in long_symbols] + [PortfolioTarget(s, -weight) for s in short_symbols]
        self.set_holdings(targets, True)