
“该策略根据过往表现和彩票代理(MAX)对美国股票进行排序,然后在亏损组合中做多最低彩票十分位数,做空最高彩票十分位数,每月重新平衡。”
资产类别: 股票 | 地区: 美国 | 周期: 每月 | 市场: 股票 | 关键词: 彩票
I. 策略概要
该投资范围包括CRSP数据库中的美国股票(在纽约证券交易所、美国证券交易所或纳斯达克交易),不包括股价低于1美元的股票、封闭式基金和房地产投资信托基金。该策略涉及根据过去3至1个月的股票表现(PFM)进行条件双变量投资组合排序,创建三个投资组合:赢家、输家和中间。在输家投资组合中,股票根据彩票代理(MAX或LTRY)进一步分为五分位数。MAX代理是上个月的最大日回报。该策略涉及做多最低彩票十分位数,做空最高彩票十分位数。投资组合采用价值加权,每月重新平衡。
II. 策略合理性
彩票投资者受行为偏差影响,偏爱表现不佳的彩票型股票,认为它们有更高的反弹潜力。这导致此类股票定价过高,可以在交易策略中加以利用。这些投资者认为,表现良好的股票获得显著回报的机会较低,而表现不佳的股票则有更好的上涨机会。没有彩票型特征的股票可能会继续表现不佳,这进一步证明了对表现不佳的彩票型股票的偏好。这种偏差导致股票定价错误,这可以作为交易策略中的机会。
III. 来源论文
Gambling Preferences for Loser Stocks [点击查看论文]
- 袁培轩,罗格斯商学院,罗格斯大学
<摘要>
我发现投资者对赌博的偏好主要集中在过去三个月表现不佳的股票上,因为表现不佳的彩票型股票比表现良好的彩票型股票更有可能产生巨额回报(61.53% vs. 40.17%)。此外,彩票投资者倾向于认为表现不佳的彩票型股票可能在短期内强劲反弹,而表现良好的彩票型股票由于价格高昂,产生高额正回报的可能性较小。因此,表现不佳的彩票型股票具有非常有效的彩票型外观,从而吸引了彩票投资者。另一方面,没有彩票型特征的亏损股票可能会继续表现不佳。对具有(不具有)彩票型特征的股票过于乐观(悲观)的信念导致亏损股票中出现显著的彩票溢价。

IV. 回测表现
| 年化回报 | 28.78% |
| 波动率 | 28.38% |
| β值 | -0.814 |
| 夏普比率 | 1.23 |
| 索提诺比率 | -0.183 |
| 最大回撤 | N/A |
| 胜率 | 55% |
V. 完整的 Python 代码
import numpy as np
from AlgorithmImports import *
from pandas.core.frame import dataframe
class LotteryStocksandPastPerformance(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.period:int = 3 * 21
self.performance_quantile:int = 3
self.lottery_quantile:int = 10
self.leverage:int = 5
self.fundamental_count:int = 1000
self.fundamental_sorting_key = lambda x: x.DollarVolume
self.weight:Dict[Symbol, float] = {}
# Daily price data.
self.data:Dict[Symbol, RollingWindow] = {}
self.selection_flag:bool = False
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.Schedule.On(self.DateRules.MonthEnd(market), self.TimeRules.AfterMarketOpen(market), self.Selection)
self.settings.daily_precise_end_time = False
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(self.leverage)
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
# Update the rolling window every day.
for stock in fundamental:
symbol:Symbol = stock.Symbol
# Store monthly price.
if symbol in self.data:
self.data[symbol].Add(stock.AdjustedPrice)
if not self.selection_flag:
return Universe.Unchanged
selected:List[Fundamental] = [x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.MarketCap != 0]
if len(selected) > self.fundamental_count:
selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
performance:Dict[Fundamental, float] = {}
# Warmup price rolling windows.
for stock in selected:
symbol:Symbol = stock.Symbol
if symbol not in self.data:
self.data[symbol] = RollingWindow[float](self.period)
history:dataframe = self.History(symbol, self.period, Resolution.Daily)
if history.empty:
self.Log(f"Not enough data for {symbol} yet.")
continue
closes:pd.Series = history.loc[symbol].close
for time, close in closes.items():
self.data[symbol].Add(close)
if self.data[symbol].IsReady:
performance[stock] = self.data[symbol][0] / self.data[symbol][self.period-1] - 1
if len(performance) >= self.performance_quantile * self.lottery_quantile:
# Performance sorting.
sorted_by_performance:List[Fundamental] = sorted(performance, key = performance.get, reverse = True)
quantile:int = int(len(sorted_by_performance) / self.performance_quantile)
losers:List[Fundamental] = sorted_by_performance[-quantile:]
# MAX calc.
lottery:Dict[Fundamental, float] = {}
for stock in losers:
daily_closes:np.ndarray = np.array([x for x in self.data[stock.Symbol]][:21])
daily_returns:np.ndarray = (daily_closes[:-1] - daily_closes[1:]) / daily_closes[1:]
lottery[stock] = max(daily_returns)
# Lottery sorting.
sorted_by_lottery = sorted(lottery, key = lottery.get, reverse = True)
quantile:int = int(len(lottery) / self.lottery_quantile)
long:List[Fundamental] = sorted_by_lottery[-quantile:]
short:List[Fundamental] = sorted_by_lottery[:quantile]
# Market cap weighting.
for i, portfolio in enumerate([long, short]):
mc_sum:float = sum(map(lambda x: x.MarketCap, portfolio))
for stock in portfolio:
self.weight[stock.Symbol] = ((-1) ** i) * stock.MarketCap / mc_sum
return list(self.weight.keys())
def OnData(self, data: Slice) -> None:
if not self.selection_flag:
return
self.selection_flag = False
# Trade execution.
portfolio:List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in self.weight.items() if symbol in data and data[symbol]]
self.SetHoldings(portfolio, True)
self.weight.clear()
def Selection(self) -> None:
self.selection_flag = True
# Custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))