双重上市股票套利策略
登录后收藏学术论文
Mispricing of Dual-Class Shares: Profit Opportunities, Arbitrage, and Trading
Schultz
- ?Shive
本文首次研究了双重股权股票错误定价的微观结构如何形成及解决。研究表明,利用双重股权股票价格差异的简单交易策略,在扣除交易成本及多重稳健性检验后,仍能实现异常收益。通过TAQ交易数据发现,投资者的交易模式会根据价格差异进行调整。与传统看法相反,多空套利在消除差异中占比较小,而单边交易校正了大部分价格差异。此外,我们发现更具流动性的股权类别往往是价格偏离的主要原因。研究结果对风险套利和资产定价领域具有广泛的理论意义。
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.158.6695&rep=rep1&type=pdf
策略概要
目标股票:
多股权类别股票(多种股票类别具有相同现金流权利)。
股价高于5美元的股票。
交易规则:
每2分钟检查价格差异。
当价格差异超过0.50美元时:
买入价格较低的股票(在2分钟后报价基础上下单)。
持有头寸至价格收敛(两类股票价格接近)。
投资组合:
采用等权重分配,确保选定股票间的资金分布均匀。
通过捕捉双重上市股票类别间的短期价格差异,该策略利用市场定价效率偏差实现套利收益。
策略合理性
投票权溢价:
投票股通常因赋予公司控制权的私人收益而更有价值。
流动性差异:
不同股票类别间的流动性差异可能导致价格偏离。
市场低效:
这些因素本身不足以完全解释价格差异。市场低效使价格暂时偏离合理水平,从而为投票股与非投票股之间的套利创造机会。
利用这些低效之处,套利者能够在价格趋于合理时获利。
回测表现
年化收益10.1%
波动率12.76%
贝塔-0.064
夏普比率0.48
胜率48%
完整 Python 代码
from AlgorithmImports import *
from typing import Dict, List
from data_tools import SymbolData, OpenTrade, CustomFeeModel
# endregion
class CalculatingYellowHippopotamus(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.leverage:int = 5
self.threshold:float = 0.01
self.period:int = 2
self.data:Dict[Symbol, SymbolData] = {}
self.open_trades:Dict[str, OpenTrade] = {}
stocks_with_both_share_classes:List[str] = [
'BRK','AKO','BF','CRB','FCE','JW','MOG','RDS','LGF',
]
self.stock_pairs:Dict[str, str] = { x + '.A' : x + '.B' for x in stocks_with_both_share_classes }
tickers:List[str] = list(self.stock_pairs.keys()) + list(self.stock_pairs.values())
for ticker in tickers:
security = self.AddEquity(ticker, Resolution.Daily)
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(self.leverage)
self.data[ticker] = SymbolData(security.Symbol, self.period)
self.UniverseSettings.Resolution = Resolution.Minute
def OnData(self, data: Slice):
# update prices on minute basis
for _, symbol_data in self.data.items():
symbol:Symbol = symbol_data.get_symbol()
if symbol in data and data[symbol]:
price:float = data[symbol].Value
# start updating prices, only when base price is set
if not symbol_data.base_price_ready():
symbol_data.set_base_price(price)
continue
symbol_data.update_prices(price)
long_leg:List[Symbol] = []
short_leg:List[Symbol] = []
closed_trades_flag:bool = False
for ticker1, ticker2 in self.stock_pairs.items():
ticker1_return:float|None = None
ticker2_return:float|None = None
if self.data[ticker1].prices_ready():
ticker1_return = self.data[ticker1].get_return()
self.data[ticker1].reset_prices()
if self.data[ticker2].prices_ready():
ticker2_return = self.data[ticker2].get_return()
self.data[ticker2].reset_prices()
# make sure both stocks from pair has returns
if not (ticker1_return and ticker2_return):
continue
identificator:str = ticker1 + ticker2
# if stocks are invested, check if they should be sell
if identificator in self.open_trades:
open_trade:OpenTrade = self.open_trades[identificator]
prev_diff:float = open_trade.get_prev_diff()
if (prev_diff >= self.threshold and ticker1_return <= ticker2_return) or \
(prev_diff <= -self.threshold and ticker1_return >= ticker2_return):
# liquidate, becasue stock, which had previously larger return, has now smaller or equal return
traded_symbols:List[Symobl] = open_trade.get_traded_symbols()
for symbol in traded_symbols:
self.Liquidate(symbol)
# remove OpenTrade object, because it's stocks were liquidated
del self.open_trades[identificator]
closed_trades_flag = True
continue
diff:float = ticker1_return - ticker2_return
# trade stocks, when differecne between their percentual return
# in absolute value is greater than threshold
if abs(diff) >= self.threshold:
symbol1:Symbol = self.data[ticker1].get_symbol()
symbol2:Symobl = self.data[ticker2].get_symbol()
if ticker1_return > ticker2_return:
long_leg.append(symbol2)
short_leg.append(symbol1)
self.open_trades[identificator] = OpenTrade(diff, symbol2, symbol1)
else:
long_leg.append(symbol1)
short_leg.append(symbol2)
self.open_trades[identificator] = OpenTrade(diff, symbol1, symbol2)
if len(long_leg) != 0 or closed_trades_flag:
# trade execution
# trade execution
long_leg = long_leg + [open_trade.get_long_symbol() for _, open_trade in self.open_trades.items()]
short_leg = short_leg + [open_trade.get_short_symbol() for _, open_trade in self.open_trades.items()]
length:int = len(long_leg)
for symbol in long_leg:
self.SetHoldings(symbol, 1 / length)
for symbol in short_leg:
self.SetHoldings(symbol, -1 / length)