The strategy trades large Australian stocks, going long zero-share issuance stocks and short high-share issuance stocks, sorting annually into portfolios by net issuance and rebalancing equally weighted positions yearly.

I. STRATEGY IN A NUTSHELL

Universe: Australian exchange-listed stocks, restricted to the largest 30% by market capitalization.

Portfolio Formation:

Each December, firms are sorted into eight portfolios based on net share issuance:

Net Issuance Measure = log(adjusted shares at June (t–1)) – log(adjusted shares at June (t–2)).

Negative issuance stocks: split into two groups: NegLow (most negative) and NegHigh.

Zero issuance stocks: grouped into the Zeros portfolio.

Positive issuance stocks: ranked into quintiles (PosLow → PosHigh).

Strategy Rule:

Equal-weighted positions, rebalanced annually.

Go long the Zeros portfolio.

Go short the PosHigh portfolio.

II. ECONOMIC RATIONALE

Dilution Effect:
New share issuance dilutes existing shareholders’ ownership, often putting downward pressure on stock prices.

Investor Behavioural Bias:
Markets do not always fully and immediately price in dilution effects. The delayed adjustment reflects investor underreaction and other behavioural biases.

Exploitable Anomaly:
By systematically avoiding high-issuance stocks (shorting PosHigh) and favoring no-issuance stocks (long Zeros), the strategy exploits this persistent mispricing.

III. SOURCE PAPER

Share Issuance Effects in the Cross-Section of Stock Returns [Click to Open PDF]

Lancaster, Bornholt, Reserve Bank of Australia, Griffith University

<Abstract>

Previous research describes the net share issuance anomaly in U.S. stocks as pervasive, both in size-based sorts and in cross-section regressions. As a further test of its pervasiveness, this paper undertakes an in-depth study of share issuance effects in the Australian equity market. The anomaly is observed in all size stocks except micro stocks. For example, equal weighted portfolios of non-issuing big stocks outperform portfolios of high issuing big stocks by an average of 0.84% per month over 1990–2009. This outperformance survives risk adjustment and appears to subsume the asset growth effect in Australian stock returns.

IV. BACKTEST PERFORMANCE

Annualised Return10.56%
Volatility12.25%
Beta-0.133
Sharpe Ratio0.54
Sortino Ratio-0.045
Maximum DrawdownN/A
Win Rate66%

V. FULL PYTHON CODE

from AlgorithmImports import *
from math import isnan
class ShareIssuanceEffect(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        
        self.long:List[Symbol] = []
        self.short:List[Symbol] = []
        self.shares_number:Dict[Symbol, RollingWindow] = {}
        self.leverage:int = 5
        self.min_share_price:float = 5.
        
        market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        
        self.fundamental_count:int = 500
        self.fundamental_sorting_key = lambda x: x.DollarVolume
        self.record_shares_flag = False
        self.record_shares_flag_month:int = 6
        self.selection_flag = False
        self.selection_flag_month:int = 11
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.Schedule.On(self.DateRules.MonthEnd(market), self.TimeRules.BeforeMarketClose(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]:
        if not self.selection_flag and not self.record_shares_flag:
            return Universe.Unchanged
        selected:List[Fundamental] = [
            x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.Price >= self.min_share_price and \
            not isnan(x.FinancialStatements.BalanceSheet.OrdinarySharesNumber.ThreeMonths) and x.FinancialStatements.BalanceSheet.OrdinarySharesNumber.ThreeMonths > 0
        ]
        
        if len(selected) > self.fundamental_count:
            selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
        if self.record_shares_flag:
            for stock in selected:
                symbol:Symbol = stock.Symbol
            
                if symbol not in self.shares_number:
                    self.shares_number[symbol] = RollingWindow[float](2)
                
                shares_number:float = stock.FinancialStatements.BalanceSheet.OrdinarySharesNumber.ThreeMonths
                self.shares_number[symbol].Add(shares_number)
            
            # NOTE: Get rid of old shares number records so we work with latest values.
            del_symbols:List[Symbol] = []
            for symbol in self.shares_number:
                if symbol not in [x.Symbol for x in selected]:
                    del_symbols.append(symbol)
            for symbol in del_symbols:
                del self.shares_number[symbol]
            
            self.record_shares_flag = False
            
        elif self.selection_flag:
            net_issuance:Dict[Symbol, float] = {}
            
            for stock in selected:
                symbol:Symbol = stock.Symbol
                
                if symbol in self.shares_number and self.shares_number[symbol].IsReady:
                    shares_values:List[float] = list(self.shares_number[symbol])
                    net_issuance[symbol] = shares_values[0] / shares_values[-1] - 1
                
            if len(net_issuance) != 0:
                zero_net_issuance:List[float] = [x[0] for x in net_issuance.items() if x[1] == 0]
                
                pos_net_issuance:List = [x for x in net_issuance.items() if x[1] > 0]
                sorted_pos_by_net_issuance:List = sorted(pos_net_issuance, key = lambda x: x[1], reverse = True)
                quantile:int = int(len(sorted_pos_by_net_issuance)/5)
                pos_high:List[Symbol] = [x[0] for x in sorted_pos_by_net_issuance[:quantile]]
                
                neg_net_issuance:List = [x for x in net_issuance.items() if x[1] < 0]
                sorted_neg_by_net_issuance:List = sorted(neg_net_issuance, key = lambda x: x[1], reverse = False)
                half:int = int(len(sorted_neg_by_net_issuance) / 2)
                neg_high:List[Symbol] = [x[0] for x in sorted_neg_by_net_issuance[:half]]
                
                #self.long = zero_net_issuance
                self.long = neg_high 
                self.short = pos_high
            
        return self.long + self.short
    
    def OnData(self, data: Slice) -> None:
        if not self.selection_flag:
            return
        self.selection_flag = False
        # Trade execution and rebalance
        targets:List[PortfolioTarget] = []
        for i, portfolio in enumerate([self.long, self.short]):
            for symbol in portfolio:
                if symbol in data and data[symbol]:
                    targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
        
        self.SetHoldings(targets, True)
        self.long.clear()
        self.short.clear()
    
    def Selection(self) -> None:
        if self.Time.month == self.record_shares_flag_month:
            self.record_shares_flag = True
        elif self.Time.month == self.selection_flag_month:
            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"))

VI. Backtest Performance

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading