Overall Statistics
Total Orders
1704
Average Win
0.29%
Average Loss
-0.27%
Compounding Annual Return
18.222%
Drawdown
22.000%
Expectancy
0.386
Start Equity
100000
End Equity
231008.00
Net Profit
131.008%
Sharpe Ratio
0.643
Sortino Ratio
0.762
Probabilistic Sharpe Ratio
32.600%
Loss Rate
33%
Win Rate
67%
Profit-Loss Ratio
1.08
Alpha
0.028
Beta
0.999
Annual Standard Deviation
0.153
Annual Variance
0.023
Information Ratio
0.477
Tracking Error
0.059
Treynor Ratio
0.098
Total Fees
$1731.80
Estimated Strategy Capacity
$120000000.00
Lowest Capacity Asset
CHKPF R735QTJ8XC9X
Portfolio Turnover
2.51%
Drawdown Recovery
526
from AlgorithmImports import *
import numpy as np

class StandardizedUnexpectedEarnings(QCAlgorithm):
    '''Step 1. Calculate the change in quarterly EPS from its value four quarters ago
       Step 2. Calculate the st dev of this change over the prior eight quarters
       Step 3. Get standardized unexpected earnings (SUE) from dividing results of step 1 by step 2
       Step 4. Each month, sort universe by SUE and long the top quantile
       Reference:
       [1] Foster, Olsen and Shevlin, 1984, Earnings Releases, Anomalies, and the Behavior of Security Returns,
           The Accounting Review.
       [2] Hou, Xue and Zhang, 2018, Replicating Anomalies, Review of Financial Studies.
    '''

    def initialize(self):
        self.set_start_date(self.end_date - timedelta(5*365))
        self.set_cash(100000)
        self.settings.seed_initial_prices = True        
        # Define some parameters.
        self._months_eps_change = 12
        self._months_count = 36
        self._num_selected = 1000
        self._top_percent = 0.05        
        self._eps_by_symbol = {}
        # Add a universe of US Equities.
        self._date_rule = self.date_rules.month_start("SPY") 
        self.universe_settings.schedule.on(self._date_rule)
        self.universe_settings.resolution = Resolution.DAILY
        self._universe = self.add_universe(self._select)        
        self.set_warm_up(timedelta(self._months_count*31+30))
    
    def _select(self, fundamentals):
        # Filter to stocks with valid price and EPS data, select top by dollar volume.
        selected = sorted(
            [f for f in fundamentals if f.price > 5 and f.earning_reports.basic_eps.three_months],
            key=lambda f: f.dollar_volume
        )[-self._num_selected:]        
        # Update EPS rolling windows and calculate SUE for stocks with sufficient history.
        sue_by_symbol = {}
        for f in selected:
            # Update the trailing EPS data.
            if f.symbol not in self._eps_by_symbol:
                self._eps_by_symbol[f.symbol] = RollingWindow[float](self._months_count)
            eps_window = self._eps_by_symbol[f.symbol]
            eps_window.add(f.earning_reports.basic_eps.three_months)
            # Calculate SUE if there is enough data.
            if not eps_window.is_ready:
                continue
            # Current quarter EPS change from four quarters ago.
            eps_change = eps_window[0] - eps_window[self._months_eps_change]            
            # Calculate historical EPS changes for standard deviation.
            new_eps = list(eps_window)[:self._months_count - self._months_eps_change:3]
            old_eps = list(eps_window)[self._months_eps_change::3]
            changes = [float(new - old) for new, old in zip(new_eps, old_eps)]
            std = np.std(changes)
            if std > 0:
                sue_by_symbol[f.symbol] = eps_change / std
        # Select top quantile by SUE.
        if not sue_by_symbol:
            return []
        num_long = max(1, int(self._top_percent * len(sue_by_symbol)))
        return sorted(sue_by_symbol, key=lambda symbol: sue_by_symbol[symbol])[-num_long:]
        
    def on_warmup_finished(self):
        # Add a Scheduled event to rebalance the portfolio monthly.
        time_rule = self.time_rules.at(8, 0)
        self.schedule.on(self._date_rule, time_rule, self._rebalance)
        # Rebalance the portfolio today too.
        if self.live_mode:
            self._rebalance()
        else:
            self.schedule.on(self.date_rules.today, time_rule, self._rebalance)

    def _rebalance(self):
        # Form an equal-weighted portfolio..
        weight = 1.0 / len(self._universe.selected)
        self.set_holdings([PortfolioTarget(symbol, weight) for symbol in self._universe.selected], True)