投资池包括纽约证券交易所(NYSE)、美国证券交易所(AMEX)和纳斯达克(NASDAQ)的股票。根据资产负债表应计项公式对股票进行排序,投资于应计项最低的股票,并做空应计项最高的股票。投资组合在每年5月财报发布后重新平衡。

策略概述

该策略的投资范围包括来自纽约证券交易所、美国证券交易所和纳斯达克的股票。应计项(Accruals)是非现金收益的组成部分,按照以下公式计算:
BS_ACC = (∆CA – ∆Cash) – (∆CL – ∆STD – ∆ITP) – Dep

其中,∆CA代表流动资产的年变化,∆Cash为现金等价物的变化,∆CL为流动负债的变化,∆STD为短期债务的变化,∆ITP为应付所得税的变化,Dep为折旧费用。根据应计项对股票进行十等分排名,投资于应计项最低的股票,做空应计项最高的股票。投资组合每年5月(收益报告发布后)进行重新平衡。

策略合理性

应计异常是基于“收益执着假说”(Earnings Fixation Hypothesis),认为投资者过度关注公司公布的收益,而忽视了将现金流和应计项部分分开评估的必要性。这种忽视导致高应计项公司的乐观情绪过度,从而估值过高,表现较差;而低应计项公司因悲观情绪而被低估,反而带来较高回报。Detzel、Schabel和Strauss的研究“应计异常的两种截然不同类型”强调了投资相关应计项和非投资应计项之间的差异。他们发现,投资相关应计项更能预测回报,受到市场情绪影响,并且与风险溢价相关;而非投资应计项则更多地与定价错误有关。这一发现澄清了之前研究中的混淆,表明应计异常中存在两种不同的现象:基于风险的投资应计溢价和非投资应计项的错误定价。

论文来源

The Persistence of the Accruals Anomaly [点击浏览原文]

<摘要>

应计异常——会计应计项与后续股票回报之间的负相关关系——在学术和实践文献中已被记录近十年。由于该异常表明市场效率存在问题,预计精明的投资者会发现并通过套利消除这一异常。然而,我们发现应计异常仍然存在,且其程度并未随着时间的推移而减弱。虽然我们发现机构投资者对应计信息反应迅速,但这种反应相对较弱,主要体现在一小部分活跃投资者身上。主要原因是应计异常极端公司的特征对大多数机构投资者来说并不具吸引力。由于交易成本高和信息获取困难,个人投资者大多无法从应计信息中获利。因此,应计异常可能会继续存在。

回测表现

年化收益率7.5%
波动率10.26%
Beta0.125
夏普比率-0.103
索提诺比率-0.092
最大回撤66.6%
胜率49%

完整python代码

from AlgoLib import *
import numpy as np
from typing import List, Dict

class AccrualsBasedStrategy(XXX):
    
    def Initialize(self):
        self.SetStartDate(2006, 1, 1)
        self.SetCash(100000)
        
        self.UniverseSettings.Leverage = 5
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.ScreenStocks)
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.0

        # Configuration
        self.stock_exchanges: List[str] = ['NYS', 'NAS', 'ASE']
        self.stock_limit: int = 3000
        self.rebalance_month: int = 5
        self.accruals: Dict[Symbol, float] = {}
        self.buy_list: List[Symbol] = []
        self.sell_list: List[Symbol] = []
        self.is_selection_time: bool = False
        
        self.spy_symbol: Symbol = self.AddEquity("SPY", Resolution.Daily).Symbol
        self.Schedule.On(self.DateRules.MonthStart(self.spy_symbol), 
                         self.TimeRules.AfterMarketOpen(self.spy_symbol), 
                         self.DecideRebalance)

    def ScreenStocks(self, fundamentals: List[Fundamental]) -> List[Symbol]:
        if not self.is_selection_time:
            return Universe.Unchanged

        eligible_stocks = [f for f in fundamentals if f.HasFundamentalData and
                           f.SecurityReference.ExchangeId in self.stock_exchanges and
                           all(not np.isnan(x) for x in [
                               f.FinancialStatements.BalanceSheet.CurrentAssets.TwelveMonths,
                               f.FinancialStatements.BalanceSheet.CashAndCashEquivalents.TwelveMonths,
                               f.FinancialStatements.BalanceSheet.CurrentLiabilities.TwelveMonths,
                               f.FinancialStatements.BalanceSheet.CurrentDebt.TwelveMonths,
                               f.FinancialStatements.BalanceSheet.IncomeTaxPayable.TwelveMonths,
                               f.FinancialStatements.IncomeStatement.DepreciationAndAmortization.TwelveMonths])]

        if len(eligible_stocks) > self.stock_limit:
            eligible_stocks = sorted(eligible_stocks, key=lambda x: x.MarketCap, reverse=True)[:self.stock_limit]

        for stock in eligible_stocks:
            symbol = stock.Symbol
            accruals_data = self.ComputeAccruals(stock)
            self.accruals[symbol] = accruals_data

        sorted_accruals = sorted(self.accruals.items(), key=lambda item: item[1])
        decile_size = len(sorted_accruals) // 10
        self.buy_list = [symbol for symbol, _ in sorted_accruals[:decile_size]]
        self.sell_list = [symbol for symbol, _ in sorted_accruals[-decile_size:]]

        return self.buy_list + self.sell_list

    def ComputeAccruals(self, stock: Fundamental) -> float:
        delta_assets = stock.FinancialStatements.BalanceSheet.CurrentAssets.TwelveMonths - stock.FinancialStatements.BalanceSheet.CashAndCashEquivalents.TwelveMonths
        delta_liabilities = stock.FinancialStatements.BalanceSheet.CurrentLiabilities.TwelveMonths - stock.FinancialStatements.BalanceSheet.CurrentDebt.TwelveMonths - stock.FinancialStatements.BalanceSheet.IncomeTaxPayable.TwelveMonths
        depreciation = stock.FinancialStatements.IncomeStatement.DepreciationAndAmortization.TwelveMonths
        total_assets = stock.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths
        accruals = (delta_assets - delta_liabilities - depreciation) / total_assets
        return accruals

    def DecideRebalance(self):
        self.is_selection_time = self.Time.month == self.rebalance_month

    def OnSecuritiesChanged(self, changes: SecurityChanges):
        for removed in changes.RemovedSecurities:
            if removed.Symbol in self.accruals:
                del self.accruals[removed.Symbol]

    def OnData(self, data: Slice):
        if not self.is_selection_time:
            return
        
        self.is_selection_time = False
        self.ExecuteTrades(data)

    def ExecuteTrades(self, data: Slice):
        targets = [PortfolioTarget(symbol, 1 / len(self.buy_list)) for symbol in self.buy_list if data.ContainsKey(symbol)] + \
                  [PortfolioTarget(symbol, -1 / len(self.sell_list)) for symbol in self.sell_list if data.ContainsKey(symbol)]
        self.SetHoldings(targets)
        self.buy_list.clear()
        self.sell_list.clear()

# Example custom fee model
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
        fee = parameters.Security.Price

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading