Overall Statistics
Total Orders
3386
Average Win
1.72%
Average Loss
-0.38%
Compounding Annual Return
0%
Drawdown
100.100%
Expectancy
1.721
Start Equity
100000
End Equity
-83.93
Net Profit
-100.084%
Sharpe Ratio
-0.757
Sortino Ratio
-0.406
Probabilistic Sharpe Ratio
0%
Loss Rate
51%
Win Rate
49%
Profit-Loss Ratio
4.51
Alpha
-0.376
Beta
0.072
Annual Standard Deviation
0.499
Annual Variance
0.249
Information Ratio
-0.674
Tracking Error
0.521
Treynor Ratio
-5.217
Total Fees
$45.54
Estimated Strategy Capacity
$1000.00
Lowest Capacity Asset
AWX RBSIMWGA33VP
Portfolio Turnover
-0.10%
# https://quantpedia.com/strategies/predicting-anomalies/
# 
# The investment universe for this strategy consists of publicly traded U.S. stocks within, assumingly, NYSE, NASDAQ, and AMEX.
# (Daily stock returns can be sourced from the Center for Research in Security Prices (CRSP). You can use Compustat to collect firms’ quarterly and annual financial 
# information to calculate anomaly variables. Thirdly, use the Compustat Snapshot database to identify point-in-time information releases.)
# (A list of considered anomaly portfolios is in Table IV.)
# Sorting: The strategy variant, that we picked uses the Asset Growth measure as signal (see appendix C for calculation). For example, the Asset Growth measure after 
# the release of third quarter financial statements is calculated by comparing total assets through Q3t of this year to total assets as of the end of last year, Q4t−1.
# Stocks are ranked based on their predicted anomaly signals using simple predictive models, and those in the top and bottom deciles are selected for the portfolio.
# This variant employs the predictive Martingale model to forecast anomaly signals. The methodology predicts anomaly signals six months before the expected information 
# release date.
# A martingale model (eq. (2)) assumes the next period’s anomaly portfolio ranking will be the same as the current ranking: The conditional expected value of the anomaly 
# signal ranking as of Qq is best approximated with the Q_q-1 ranking.
# Trading Rules: Buy rules are triggered when a stock is anticipated to exhibit a positive anomaly signal. In contrast, sell rules are applied if a stock no longer meets 
# the criteria.
# Portfolio Construction: The portfolio is constructed by buying (and selling) stocks in the signal’s highest (lowest) decile ranking.
# Use the quarterly and year-to-date information for calculation and rebalance annually. Assume equal weighting.
# 
# QC implementation changes:
#   - The investment universe consists of 3000 largest stocks from NYSE, AMEX and NASDAQ.
#   - Rebalance is made annually in october.

# region imports
from AlgorithmImports import *
# endregion

class PredictingAnomalies(QCAlgorithm):

    def initialize(self) -> None:
        self.set_start_date(2000, 1, 1)
        self.set_cash(100_000)

        self._tickers_to_ignore: List[str] = ['TARO', 'ITWO', 'BVSND', 'NURM', 'DRYS', 'ASKJ', 'GME', 'SBEI', 'KNOT']
        self._exchange_codes: List[str] = ['NYS', 'NAS', 'ASE']	

        leverage: int = 10
        self._quantile: int = 10
        self._period: int = 2
        self._selection_month: int = 10
        self._data_update_months: List[int] = [1, 7]

        self._long: List[Symbol] = []
        self._short: List[Symbol] = []
        self._total_assets: Dict[Symbol, RollingWindow] = {}

        market: Symbol = self.add_equity('SPY', Resolution.DAILY).symbol

        self._fundamental_count: int = 3_000
        self._fundamental_sorting_key = lambda x: x.market_cap

        self._data_update_flag: bool = False
        self._selection_flag: bool = False

        self.universe_settings.leverage = leverage
        self.universe_settings.resolution = Resolution.DAILY
        self.add_universe(self.fundamental_selection_function)
        self.settings.daily_precise_end_time = False
        self.settings.minimum_order_margin_portfolio_percentage = 0.

        self.schedule.on(
            self.date_rules.month_start(market),
            self.time_rules.before_market_close(market),
            self.selection
        )

    def on_securities_changed(self, changes: SecurityChanges) -> None:
        for security in changes.added_securities:
            security.set_fee_model(CustomFeeModel())

    def fundamental_selection_function(self, fundamental: List[Fundamental]) -> List[Symbol]:
        if self._data_update_flag:
            self._data_update_flag = False

            # update fundamental data
            for stock in fundamental:
                symbol: Symbol = stock.symbol
                
                if symbol in self._total_assets:
                    if stock.financial_statements.balance_sheet.total_assets.three_months != 0:
                        self._total_assets[symbol].add(stock.financial_statements.balance_sheet.total_assets.three_months)

        if not self._selection_flag:
            return Universe.UNCHANGED

        selected: List[Fundamental] = [
            x for x in fundamental
            if x.has_fundamental_data
            and x.financial_statements.balance_sheet.total_assets.has_value
            and x.security_reference.exchange_id in self._exchange_codes
            and x.symbol.value not in self._tickers_to_ignore
        ]
        
        if len(selected) > self._fundamental_count:
            selected = [x for x in sorted(selected, key=self._fundamental_sorting_key, reverse=True)[:self._fundamental_count]]

        asset_growth_symbol: Dict[Symbol, float] = {}

        for stock in selected:
            symbol: Symbol = stock.symbol
            if symbol not in self._total_assets:
                self._total_assets[symbol] = RollingWindow[float](self._period)
            if not self._total_assets[symbol].is_ready:
                continue

            asset_growth: float = (self._total_assets[symbol][0] - self._total_assets[symbol][1]) / self._total_assets[symbol][1]
            if asset_growth != 0:
                asset_growth_symbol[symbol] = asset_growth

        # sort and divide
        if len(asset_growth_symbol) > self._quantile:
            sorted_asset_growth: List[Symbol] = sorted(asset_growth_symbol, key=asset_growth_symbol.get, reverse=True)
            quantile: int = int(len(sorted_asset_growth) / self._quantile)
            self._long = sorted_asset_growth[:quantile]
            self._short = sorted_asset_growth[-quantile:]

        return self._long + self._short

    def on_data(self, slice: Slice) -> None:
        if not self._selection_flag:
            return
        self._selection_flag = False

        targets: List[PortfolioTarget] = []
        for i, portfolio in enumerate([self._long, self._short]):
            for symbol in portfolio:
                if slice.contains_key(symbol) and slice[symbol]:
                    targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
        
        self.set_holdings(targets, True)
        self._long.clear()
        self._short.clear()

    def selection(self) -> None:
        if self.time.month in self._data_update_months:
            self._data_update_flag = True

        if self.time.month == self._selection_month:
            self._selection_flag = True

# Custom fee model
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
        fee: float = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))