Index tracking
An index tracking strategy aims to follow the returns of a certain index. The assets in such an index are generally chosen according to a general theme (e.g. the american stock market, utility stocks, rare metals) or as specified by a set of rules (e.g. only assets that have returned >5% over the last five years). The index allocations (%-share of a specific asset in the index) are usually determined by the size of the asset's market cap relative to the sum of all asset market caps, but other methods can also used.
The strategy can be summarized with the following pseudo-code:
Construct an index according to a theme or ruleset
While the strategy is running:
1. Update the index
1. Add or remove assets from the index
2. Rebalance asset allocations
2. Buy all index assets according to their allocations
Despite the superficial simplicity, a lot of complications arise when performing each of these steps.
Index construction
The index construction method has to be aware of "illegitimate" assets: scams or low-liquidity assets with inflated valuations that can become value-less at any time. Certain rules can be added to keep these kinds of assets out of the index:
- Blacklisting: An easy method to keep known scams out of the index, or to simply avoid specific assets.
- Market cap treshold: To avoid assets with penny-stock behaviour.
- Volume treshold: To avoid illiquid assets.
Once a list of assets is constructed their allocations need to be calculated according to a certain weighting method. This can affect the returns of the index, so it should be chosen with care. The two following methods are the most common:
- Capitalization weighting: An asset's allocation is equal to its market cap divided by the sum of the market caps of all assets in the index.
- Uniform weighting: Each asset has an equal allocation.
Rebalancing
Over time the portfolio of the index tracker will 'drift away' from the index. It is therefore necessary to determine a rebalancing strategy that minimizes portfolio drift. There are two variables that can be changed to achieve an optimal rebalancing strategy:
- Monitoring frequency: How often the index is monitored
- Deviation treshold: How much an asset is allowed to deviate from its appointed allocation
Potential values for these variables could be 1 week and 5%, respectively.
An important issue here is the tracking error, a small difference of returns between the market and the index tracker caused by the trading costs associated with rebalancing. The assets being sold or bought when rebalancing can influence the (asset's) market, moving the price away from the initially sought price. This, plus the fact that trading fees are incurred, then results in a tracking error. Depending on the amount of assets being managed by the index tracker this error should be relatively insignificant. In the case of large tracking errors one can use specialized order execution algorithms that seek to minimize market impact.
Vanguard's research division has written an excellent guide concerning rebalancing which goes more into depth, it can be found here.
Python implementation
The following python code is an implementation (work in progress) of the index tracking strategy described above. It is built upon the Strategy
class, part of an automated trading framework I'm working on. A Strategy
instance requires a generate_signals
function that takes ticker data and returns trading signals that can then be executed by an order router.
import time
from system.event import Signal
from strategies.strategy import Strategy
class IndexTracker(Strategy):
def __init__(self, holdings, settings):
Strategy.__init__(settings)
self.holdings = holdings
self.allocation = {}
self.last_updated = 0 # Unix timestamp
# General settings
self.securities = settings['securities']
self.exchanges = settings['exchanges']
self.mode = settings['mode']
# Index construction settings
self.blacklist = [asset.lower() for asset in settings['blacklist']]
self.max_assets = settings['max_assets']
self.min_market_cap = settings['min_market_cap']
self.min_volume = settings['min_volume']
# Index rebalancing settings
self.frequency = settings['frequency']
self.treshold = settings['treshold']
The IndexTracker
takes two arguments on initialization: a dictionary describing any asset holdings and another dictionary containing settings.
settings = {'mode': 'AUTOMATED',
'exchanges': []
'blacklist': [],
'max_assets': 3,
'min_market_cap': 1000000,
'min_volume': 1000000
'frequency': 120,
'treshold': 5}
holdings = {}
The asset allocation (calculated using market share) is handled as follows:
def allocate(self):
# Get list of eligible assets, sorted by market cap, remove any assets
# that are blacklisted or don't fulfill the requirements otherwise
assets = sorted(iter(self.ticker.values()),
key=lambda asset: -float(asset['market_cap_usd']))
assets = [asset for asset in assets
if asset['name'].lower() not in self.blacklist
and asset['24h_volume_usd'] > self.min_volume
and asset['market_cap_usd'] > self.min_market_cap]
assets = assets[:self.max_assets]
# Portfolio value in common currency (either EUR or USD)
holdings_value = sum([amount*float(self.ticker[asset]['price_usd'])
for asset, amount in self.holdings.items()])
# Allocate asset holdings according to marketcap (or other attributes)
marketcap = sum([float(asset['market_cap_usd']) for asset in assets])
self.allocation = {asset['symbol']: (asset['market_cap_usd'] / marketcap)
*holdings_value for asset in assets}
The rebalancing code is simple: generate sell signals for any surplus assets and generate buy signals for any lacking assets. It is important to generate sell signals before buy signals, seeing as the lacking assets are bought with the revenue obtained from the sold assets.
def rebalance(self):
signals = []
for asset, allocation in self.allocation.items():
price = self.ticker[asset]['price_usd']
delta = self.holdings.get(asset, 0)*price - allocation
if delta > 0: # Holding too much of this asset
signal = Signal(self.exchanges, asset, "SELL", price, delta/price, time.time())
signals.append(signal)
elif delta < 0: # Holding too little of this asset
signal = Signal(self.exchanges, asset, "BUY", price, abs(delta/price), time.time())
signals.append(signal)
return signals
The signal generation function, which will presumably be called by an automated trading system, then becomes:
def generate_signals(self, ticker):
self.ticker = ticker
self.allocate()
signals = self.rebalance()
return signals