
The strategy trades 15 developed countries based on senior population percentages, leveraging demographic trends for growth opportunities, with annual rebalancing and customizable country selection for flexibility and alignment with investor goals.
ASSET CLASS: ETFs | REGION: Global | FREQUENCY:
Yearly | MARKET: equities | KEYWORD: Demographic, Stock, Market Returns
I. STRATEGY IN A NUTSHELL
Invest in 15 developed countries based on senior population share: go long in countries with below-average elderly populations, short where it exceeds one standard deviation above the mean. Portfolios rebalance annually.
II. ECONOMIC RATIONALE
Market trends are influenced by generational asset supply and demand. Middle-aged investors buy more, driving prices up, while older populations sell to fund retirement, exerting downward pressure. Demographic shifts create predictable effects on equity returns.
III. SOURCE PAPER
Do Demographic Changes Affect Risk Premiums? Evidence from International Data [Click to Open PDF]
Ang, BlackRock, Inc; Maddaloni, European Central Bank (ECB)
<Abstract>
We examine the link between equity risk premiums and demographic changes using a very long sample over the twentieth century for the US, Japan, UK, Germany and France, and a shorter sample covering the last third of the twentieth century for fifteen countries. We find that demographic variables significantly predict excess returns internationally. However, the demographic predictability found in the US by past studies for the average age of the population does not extend to other countries. Pooling international data, we find that, on average, faster growth in the fraction of retired persons significantly decreases risk premiums. This demographic predictability of risk premiums is strongest in countries with well-developed social security systems and lesser-developed financial markets.


IV. BACKTEST PERFORMANCE
| Annualised Return | 8.93% |
| Volatility | N/A |
| Beta | 0.022 |
| Sharpe Ratio | N/A |
| Sortino Ratio | N/A |
| Maximum Drawdown | N/A |
| Win Rate | 64% |
V. FULL PYTHON CODE
from AlgorithmImports import *
import numpy as np
#endregion
class DemographicChanges(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.symbols = {'AU':'EWA', 'AT':'EWO', 'BE':'EWK', 'CA':'EWC', 'FR':'EWQ', 'DE':'EWG', 'IT':'EWI', 'JP':'EWJ', 'NL':'EWN', 'ES':'EWP', 'SE':'EWD', 'CH':'EWL', 'GB':'EWU', 'US':'SPY'}
for symbol in self.symbols:
data = self.AddEquity(self.symbols[symbol], Resolution.Daily)
data.SetLeverage(10)
self.demographic_data:Symbol = self.AddData(DemographicData, 'DemographicData').Symbol
self.recent_month:int = -1
def OnData(self, data:Slice) -> None:
if self.recent_month == self.Time.month:
return
self.recent_month = self.Time.month
# rebalance once a year
if self.Time.month != 1: return
age_data = {}
if self.demographic_data in data and data[self.demographic_data]:
for symbol in self.symbols:
age_data[symbol] = data[self.demographic_data].GetProperty(symbol)
if len(age_data) == 0:
self.Liquidate()
return
age_data_values = [x[1] for x in age_data.items()]
age_data_mean = np.mean(age_data_values)
age_data_std = np.std(age_data_values)
long = []
short = []
for symbol in self.symbols:
if age_data[symbol] < age_data_mean - age_data_std:
long.append(self.symbols[symbol])
elif age_data[symbol] > age_data_mean + age_data_std:
short.append(self.symbols[symbol])
# liquidate
invested = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in long + short:
self.Liquidate(symbol)
# trade exxecution
long_count = len(long)
short_count = len(short)
for symbol in long:
if symbol in data and data[symbol]:
self.SetHoldings(symbol, 1 / long_count)
for symbol in short:
if symbol in data and data[symbol]:
self.SetHoldings(symbol, -1 / short_count)
# Demographic data - population 65+ yo
# NOTE: IMPORTANT: Data order must be ascending (date-wise)
from dateutil.relativedelta import relativedelta
class DemographicData(PythonData):
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource("data.quantpedia.com/backtesting_data/economic/population_65_over.csv", SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config, line, date, isLiveMode):
data = DemographicData()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split = line.split(';')
# Data example:
# year;AU;AT;BE;CA;FR;DE;IT;JP;NL;ES;SE;CH;GB;US
# 2000;12.4;15.4;16.7;12.5;16.2;16.2;18.1;17.4;13.5;16.5;17.3;15.2;15.8;12.4
#
# YEARLY DATA
data.Time = datetime.strptime(split[0], "%Y") + relativedelta(months=12) # NOTE: Preventing of look ahaead bias. Add 12 months so this year we see last year's data.
data['AU'] = float(split[1])
data['AT'] = float(split[2])
data['BE'] = float(split[3])
data['CA'] = float(split[4])
data['FR'] = float(split[5])
data['DE'] = float(split[6])
data['IT'] = float(split[7])
data['JP'] = float(split[8])
data['NL'] = float(split[9])
data['ES'] = float(split[10])
data['SE'] = float(split[11])
data['CH'] = float(split[12])
data['GB'] = float(split[13])
data['US'] = float(split[14])
data.Value = float(split[1])
return data