每天,投资者从纽约证券交易所(NYSE)、美国证券交易所(AMEX)和纳斯达克(NASDAQ)中选择即将发布财报的股票,目标是活跃期权市场的大型股票。他们根据过去的财报表现进行交易,持有两天,且投资组合中的权重相等。

策略概述

投资者关注纽约证券交易所(NYSE)、美国证券交易所(AMEX)和纳斯达克(NASDAQ)的股票,尤其是具有活跃期权市场的大型股票,每天选择那些宣布第二天公布财报的股票。他们根据股票在上一次财报发布期间的表现来评估股票。该策略涉及买入过去收益表现最差的十分位组合,并卖空那些过去表现最好的组合。头寸维持两天,且投资组合中的权重相等。这种方法旨在利用股票对财报公告的异常反应的预期逆转。

策略合理性

学术论文指出,投资者在历史上以对财报新闻反应不足而闻名,现在可能正在对这类公告过度反应。传统上,关于财报公告后走势(PEAD)的文献主要调查季度投资组合收益。相比之下,本文聚焦于两天内的收益。这种焦点上的差异提出了一个可能性,即PEAD仍然有效,财报新闻的欠反应和过度反应可以作为两种独立的现象共存。这意味着,尽管短期反应可能表现出过度反应,但长期的PEAD效应,即股价逐渐对财报新闻进行调整的特征,仍然可能存在。

论文来源

Overreacting to a History of Underreaction? [点击浏览原文]

<摘要>

先前的研究已经记录了企业财报公告新闻中长期正自相关的悠久历史。这是财报公告后走势现象的主要特征之一,通常被归因于投资者对财报新闻的不足反应。我记录到,对于具有活跃交易所交易期权的公司,这种自相关已经显著变为负值。对于这些容易套利的公司,在其下一个财报公告中,前财报公告异常收益率(前财报意外)排名最高的十分位公司平均表现比排名最低的公司低1.29%(0.73%)。额外的分析与投资者学习有关财报公告后走势并过度补偿的观点一致。由于他们对财报新闻显然表现出不足反应的历史有充分的记录,投资者现在似乎对财报公告新闻反应过度。本文显示,基于相对估值的流行交易策略的尝试可能会显著逆转先前记录的模式。

回测表现

年化收益率40.32%
波动率66.29%
Beta-0.032
夏普比率0.58
索提诺比率0.46
最大回撤-95.4%
胜率48%

Python代码及解释

完整python代码

from quantlib import AssetData, TradingCostModel, TradingControl
from StrategyCore import *
import pandas as pd
from collections import defaultdict
from typing import Dict, Tuple
from pandas.tseries.offsets import BusinessDay
from dateutil.relativedelta import relativedelta
import json
from datetime import datetime, date

class EarningsReversalStrategy(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2009, 1, 1)  # Start Date
        self.SetCash(100000)

        self.leverage = 5
        self.earnings_lookback = 30

        self.last_rebalance_year = -1
        self.last_rebalance_month = -1
                
        self.asset_info = {}

        # Earnings and EPS data storage
        self.earnings_announcement_data = defaultdict(list)
        self.earnings_performance_data = defaultdict(lambda: defaultdict(dict))
        
        self.initial_date = None

        earnings_info = self.Download('data.quantpedia.com/backtesting_data/
economic/earnings_dates_eps.json')
        earnings_info_parsed = json.loads(earnings_info)
        
        for record in earnings_info_parsed:
            announcement_date = datetime.strptime(record['date'], '%Y-%m-%d').date()
            if not self.initial_date:
                self.initial_date = announcement_date

            for stock_info in record['stocks']:
                symbol = stock_info['ticker']
                self.earnings_announcement_data[announcement_date].append(symbol)

                if stock_info['eps'].strip():
                    year, month = announcement_date.year, announcement_date.month
                    self.earnings_performance_data[year][month][symbol] = 
                                                         float(stock_info['eps'])
        
        self.recent_ear_values = []
        self.past_ear_values = []
        
        self.strategy_control = TradingControl(self, 10, 10, 2)

        self.main_asset = self.AddEquity('SPY', Resolution.Daily).Symbol
        
        self.is_selection_time = False
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.select_coarse, self.select_fine)
        self.Schedule.On(self.DateRules.MonthStart(self.main_asset), 
self.TimeRules.AfterMarketOpen(self.main_asset), lambda: self.is_selection_time = True)

    def select_coarse(self, coarse):
        for asset in coarse:
            if asset.Symbol in self.asset_info:
                self.asset_info[asset.Symbol].update_price(self.Time, asset.AdjustedPrice)

        if not self.is_selection_time:
            return Universe.Unchanged
        
        self.is_selection_time = False
        prev_month = (self.Time - relativedelta(months=1)).date()
        self.last_rebalance_year, self.last_rebalance_month = prev_month.year, prev_month.month

        eligible_assets = [x for x in coarse if x.Symbol.Value in 
self.earnings_performance_data[self.last_rebalance_year][self.last_rebalance_month]]
        
        for asset in eligible_assets:
            if asset.Symbol not in self.asset_info:
                self.asset_info[asset.Symbol] = AssetData(self.earnings_lookback)
                historical_prices = self.History(asset.Symbol, self.earnings_lookback, 
Resolution.Daily)
                if not historical_prices.empty:
                    for timestamp, price in historical_prices.loc[asset.Symbol].close.items():
                        self.asset_info[asset.Symbol].update_price(timestamp, price)

        return [asset.Symbol for asset in eligible_assets if self.asset_info[asset.Symbol].is_data_ready()]

    def select_fine(self, fine):
        eligible_symbols = []
        for asset in fine:
            symbol = asset.Symbol
            if symbol.Value in self.earnings_performance_data[self.last_rebalance_year]
[self.last_rebalance_month]:
                earnings_date = max(self.earnings_performance_data[self.last_rebalance_year][self.last_rebalance_month][symbol.Value].keys())
                date_range_start = earnings_date - BusinessDay(2)
                date_range_end = earnings_date + BusinessDay(1)

                market_return = self.asset_info[self.main_asset].calculate_return(date_range_start, date_range_end)
                asset_return = self.asset_info[symbol].calculate_return(date_range_start, date_range_end)

                if market_return is not None and asset_return is not None:
                    ear = asset_return - market_return
                    self.earnings_announcement_data[symbol].append((earnings_date, ear))
                    self.recent_ear_values.append(ear)
                    eligible_symbols.append(symbol)

        if not eligible_symbols:
            return Universe.Unchanged

        return eligible_symbols

    def OnData(self, data):
        target_date = self.Time.date()

        if target_date < self.initial_date:
            return

        self.strategy_control.attempt_to_liquidate()

        if not self.past_ear_values:
            return

        ear_list = self.past_ear_values
        high_ear_threshold = np.percentile(ear_list, 90)
        low_ear_threshold = np.percentile(ear_list, 10)

        if target_date in self.earnings_announcement_data:
            for symbol in self.earnings_announcement_data[target_date]:
                ear_value = self.earnings_announcement_data[symbol][1]
                if ear_value >= high_ear_threshold:
                    self.strategy_control.open_trade(symbol, True)
                elif ear_value <= low_ear_threshold:
                    self.strategy_control.open_trade(symbol, False)

        if self.Time.month % 3 == 0:
            self.past_ear_values = list(self.recent_ear_values)
            self.recent_ear_values.clear()

Leave a Reply

Discover more from Quant Buffet

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

Continue reading