该策略针对在纽交所(NYSE)、美国证券交易所(AMEX)和纳斯达克(NASDAQ)上市的多股权类股票,当价格差异超过0.50美元时买入价格较低的股票,价格收敛时平仓,并采用等权重投资组合管理。融数据库。上市不足一年的股票被排除在外。

I. 策略概述

目标股票:

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

股价高于5美元的股票。

交易规则:

每2分钟检查价格差异。

当价格差异超过0.50美元时:

买入价格较低的股票(在2分钟后报价基础上下单)。

持有头寸至价格收敛(两类股票价格接近)。

投资组合:

采用等权重分配,确保选定股票间的资金分布均匀。

通过捕捉双重上市股票类别间的短期价格差异,该策略利用市场定价效率偏差实现套利收益。

II. 策略合理性

投票权溢价:
投票股通常因赋予公司控制权的私人收益而更有价值。

流动性差异:
不同股票类别间的流动性差异可能导致价格偏离。

市场低效:
这些因素本身不足以完全解释价格差异。市场低效使价格暂时偏离合理水平,从而为投票股与非投票股之间的套利创造机会。

利用这些低效之处,套利者能够在价格趋于合理时获利。

III. 论文来源

Mispricing of Dual-Class Shares: Profit Opportunities, Arbitrage, and Trading [点击浏览原文]

<摘要>

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

IV. 回测表现

年化收益率10.1%
波动率12.76%
Beta-0.064
夏普比率0.48
索提诺比率N/A
最大回撤N/A
胜率48%

V. 完整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)



发表评论

了解 Quant Buffet 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读