JPELGRIMS

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:

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:

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:

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