| 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"))