Overall Statistics
Total Orders
252452
Average Win
0.03%
Average Loss
-0.03%
Compounding Annual Return
5.064%
Drawdown
50.500%
Expectancy
0.034
Start Equity
100000
End Equity
350076.00
Net Profit
250.076%
Sharpe Ratio
0.334
Sortino Ratio
0.357
Probabilistic Sharpe Ratio
31.340%
Loss Rate
51%
Win Rate
49%
Profit-Loss Ratio
1.09
Alpha
0.013
Beta
0.002
Annual Standard Deviation
0.038
Annual Variance
0.001
Information Ratio
-0.171
Tracking Error
0.164
Treynor Ratio
7.211
Total Fees
$0.00
Estimated Strategy Capacity
$230000000.00
Lowest Capacity Asset
PX R735QTJ8XC9X
Portfolio Turnover
272.50%
# https://quantpedia.com/strategies/end-of-day-reversal-in-the-cross-section-of-stocks/
# 
# The investment universe for this strategy consists of stocks listed on the New York Stock Exchange (NYSE), NASDAQ, and American Stock Exchange (AMEX). The 
# selection criteria exclude stocks with a market capitalization below the 10th percentile of the NYSE or those priced below $5. To be included in the sample, 
# stocks must have at least 126 days of historical data in the Trade and Quote (TAQ) database.
# (Stock market data are obtained from the Center for Research in Security Prices (CRSP), and accounting data are from Compustat.)
# Portfolios Formation: The conditioning variables (all at day t − 1) are size (market capitalization), volume (trading volume), illiquidity (Amihud, 2002), 
# realized volatility computed using 5-minute returns, overnight volatility (standard deviation of overnight returns over the past 90 trading days), and the 
# mispricing score of Stambaugh and Yuan (2017). And ROD3_i,t is the return from market close on
# day t − 1 till 3:00 p.m. on day t.
# Form portfolios are first formed on a conditioning characteristic; we use size (only Big).
# Then, on ROD3 return, the return between the market close on day t − 1 till 3:00 p.m. on day t.
# Strategy Execution: The intraday loser (L), intraday winner (H). ”L-H” is the self-financing low-minus-high portfolio.
# The buy rule is to enter long positions in stocks with low ROD3 returns (L) at 3:30 p.m. and subsequently
# short (sell) positions in stocks with high ROD3 (H).
# returns.
# Hold these portfolios during the last half hour (LH) of the trading day t. Then liquidate.
# Rebalancing & Weighting: value-weighted portfolios are used and trades are executed every day (intraday).
# 
# Implementation changes:
#   - Universe consists of 50 largest stocks from NYSE, NASDAQ and AMEX.
#   - Portfolio is equally weighted.
#   - Zero fee are applied.

# region imports
from AlgorithmImports import *
# endregion

class EndOfDayReversalInTheCrossSectionOfStocks(QCAlgorithm):

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

        self._value_weight_flag: bool = True

        self.exchange_codes: List[str] = ['NYS', 'NAS', 'ASE']	

        leverage: int = 10
        self._quantile: int = 5
        self._min_share_price: int = 5

        self._data: Dict[Symbol, float] = {}
        self._weight: Dict[Symbol, float] = {} 

        market: Symbol = self.AddEquity('SPY', Resolution.MINUTE).Symbol

        self._fundamental_count: int = 50
        self._fundamental_sorting_key = lambda x: x.market_cap

        self._selection_flag: bool = False
        self._trade_flag: bool = False
        self.universe_settings.leverage = leverage
        self.universe_settings.resolution = Resolution.MINUTE
        self.add_universe(self.fundamental_selection_function)
        self.settings.daily_precise_end_time = False
        self.settings.minimum_order_margin_portfolio_percentage = 0.
        self.set_security_initializer(lambda security: security.set_fee_model(ConstantFeeModel(0)))

        self.schedule.on(self.date_rules.every_day(market),
                        self.time_rules.at(15, 0),
                        self.selection)
        self.schedule.on(self.date_rules.every_day(market),
                        self.time_rules.at(15, 30),
                        self.trade)
        self.schedule.on(self.date_rules.every_day(market),
                        self.time_rules.before_market_close(market, 1),
                        lambda: self.liquidate())

    def fundamental_selection_function(self, fundamental: List[Fundamental]) -> List[Symbol]:
        # Update the data every day.
        for stock in fundamental:
            symbol: Symbol = stock.symbol
            
            if symbol in self._data:
                self._data[symbol] = stock.adjusted_price

        selected: List[Fundamental] = [
            x for x in fundamental 
            if x.has_fundamental_data
            and x.price > self._min_share_price
            and x.SecurityReference.ExchangeId in self.exchange_codes
        ]
        
        if len(selected) > self._fundamental_count:
            selected = [x for x in sorted(selected, key=self._fundamental_sorting_key, reverse=True)[:self._fundamental_count]]

        # Price warmup.
        for stock in selected:
            symbol: Symbol = stock.symbol

            if symbol not in self._data:
                self._data[symbol] = stock.adjusted_price

        if len(selected) == 0:
            self.log('No stocks in selection.')
            return Universe.UNCHANGED

        self.last_selection = list(map(lambda x:x.symbol, selected))

        return self.last_selection

    def on_data(self, slice: Slice) -> None:
        # order execution
        if self._trade_flag:
            self._trade_flag = False

            portfolio: List[PortfolioTarget] = [
                PortfolioTarget(symbol, w) for symbol, w in self._weight.items() 
                if slice.contains_key(symbol) 
                and slice[symbol]
            ]
            self.set_holdings(portfolio, True)
            self._weight.clear()

        if not self._selection_flag:
            return
        self._selection_flag = False

        ROD: Dict[Symbol, float] = {}
        long: List[Symbol] = []
        short: List[Symbol] = []

        for symbol in self.last_selection:
            if slice.contains_key(symbol) and slice[symbol]:
                if self._data[symbol] != 0:
                    ROD[symbol] = slice[symbol].close / self._data[symbol] - 1

        # Sort and divide.
        if len(ROD) < self._quantile:
            self.log('Not enough data for further calculation.')
            return

        sorted_ROD: List[Symbol] = sorted(ROD, key=ROD.get)
        quantile: int = int(len(sorted_ROD) / self._quantile)
        long: List[Symbol] = sorted_ROD[:quantile]
        short: List[Symbol] = sorted_ROD[-quantile:]

        for i, portfolio in enumerate([long, short]):
            divisor: float = sum([self.securities[x].fundamentals.market_cap for x in portfolio]) if self._value_weight_flag else len(portfolio)
            for symbol in portfolio:
                dividend: float = self.securities[symbol].fundamentals.market_cap if self._value_weight_flag else 1.
                self._weight[symbol] = ((-1)**i) * (dividend / divisor)

    def selection(self) -> None:
        self._selection_flag = True
    
    def trade(self) -> None:
        self._trade_flag = True

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