Original paper
Abstract
This paper empirically investigates how abnormal trading volume reveals new information to market participants. Trading volume is generally regarded as a good proxy for information flow and theory argues that it enhances the information set of investors. However, as yet, no research has related the presence of abnormal trading volume to firm characteristics, such as ownership and governance structure, which also has a theoretical link to information quality. I find strong excess returns around extreme trading levels, which is only moderately attributable to information disclosure. Moreover, these returns are not caused by liquidity fluctuations since prices do not reverse over the following period. In contrast, and in violation of the semi-strong form of market efficiency, there is evidence of price momentum, suggesting that traders can implement successful portfolio strategies based on the observation of current volumes. Consistent with the hypotheses presented in this study, the information content of abnormal trading volume is related to ownership characteristics, such as the level of control and the family-firm status.
Keywords:Â Market efficiency, Trading volume
Trading rules
- Utilize all stocks listed on the Milan Stock Exchange for research (strategy may also apply to other national equity markets)
- Calculate the 66-day average trading volume and standard deviation for each stock daily
- Define “abnormal” volume as daily trading volume exceeding 2.33 standard deviations from the mean
- Select firms with no abnormal volume within the previous 30 trading days
- Buy stocks at market close on the event day if they experienced a minimum of 1% gain
- Hold positions for one day
- Employ equal-weighted stocks in the portfolio
- Rebalance the portfolio daily
Python code
Backtrader
import backtrader as bt
class AbnormalVolumeStrategy(bt.Strategy):
params = (
('lookback_period', 66),
('abnormal_volume_std', 2.33),
('previous_days', 30),
('min_gain', 0.01),
('hold_period', 1),
)
def __init__(self):
self.sma_vol = {}
self.std_vol = {}
self.abnormal_volume = {}
self.buy_signals = {}
for data in self.datas:
self.sma_vol[data] = bt.indicators.SimpleMovingAverage(
data.volume,
period=self.params.lookback_period
)
self.std_vol[data] = bt.indicators.StdDev(
data.volume,
period=self.params.lookback_period
)
self.abnormal_volume[data] = bt.indicators.CrossUp(
data.volume,
self.sma_vol[data] + (self.params.abnormal_volume_std * self.std_vol[data])
)
self.buy_signals[data] = bt.indicators.CrossUp(
data.close,
data.close * (1 - self.params.min_gain)
)
def next(self):
for data in self.datas:
if self.position and self.order_target_percent(data, 0):
self.sell(data)
if not self.position and self.abnormal_volume[data]:
last_abnormal_volume = self.abnormal_volume[data].get(size=self.params.previous_days)
if not any(last_abnormal_volume[:-1]) and self.buy_signals[data]:
self.buy(data)
self.order_target_percent(data, 1 / len(self.datas))
cerebro = bt.Cerebro()
# Add data feeds from Milan Stock Exchange for each stock
# cerebro.adddata(...)
cerebro.addstrategy(AbnormalVolumeStrategy)
cerebro.broker.setcash(100000.0)
# Run the strategy
results = cerebro.run()