Quant Buffet放轻松,别过度思虑

双重上市股票套利策略

登录后收藏

学术论文

Mispricing of Dual-Class Shares: Profit Opportunities, Arbitrage, and Trading

作者Schultz

机构
  • ?Shive
论文摘要

本文首次研究了双重股权股票错误定价的微观结构如何形成及解决。研究表明,利用双重股权股票价格差异的简单交易策略,在扣除交易成本及多重稳健性检验后,仍能实现异常收益。通过TAQ交易数据发现,投资者的交易模式会根据价格差异进行调整。与传统看法相反,多空套利在消除差异中占比较小,而单边交易校正了大部分价格差异。此外,我们发现更具流动性的股权类别往往是价格偏离的主要原因。研究结果对风险套利和资产定价领域具有广泛的理论意义。

策略概要

目标股票:

多股权类别股票(多种股票类别具有相同现金流权利)。

股价高于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)