Notebook
In [1]:
import pandas as pd
from datetime import datetime, timedelta
import numpy as np

import matplotlib.pyplot as plt
import matplotlib.cm as cm
In [2]:
# Each market is stored with properties like start, end, timezone (offest),
# and dst (whether or not they observe daylight savings)
class Market:
    def __init__(self, label, start, end, tz, dst, classification, hemisphere='north'):
        self.label = label
        self.start = start
        self.end = end
        self.tz = tz
        self.dst = dst
        self.classification = classification
        self.hemisphere = hemisphere
In [3]:
# Market data collected from these sources:
# https://www.msci.com/market-classification
# https://www.stockmarketclock.com/exchanges
# https://en.wikipedia.org/wiki/List_of_stock_exchange_trading_hours

# All markets of the MSCI ACWI index (https://www.msci.com/market-classification).
markets = {
    'Americas':{
        'US': Market(
            label='US',
            start=9.5,
            end=16,
            tz=-4,
            dst=True,
            classification='developed',
        ),
        'Canada': Market(
            label='Canada',
            start=9.5,
            end=16,
            tz=-4,
            dst=True,
            classification='developed',
        ),
        'Brazil': Market(
            label='Brazil',
            start=10,
            end=17.5,
            tz=-3,
            dst=True,
            classification='emerging',
            hemisphere='south',
        ),
        'Mexico': Market(
            label='Mexico',
            start=8.5,
            end=15,
            tz=-5,
            dst=True,
            classification='emerging',
        ),
        'Chile': Market(
            label='Chile',
            start=9.5,
            end=16,
            tz=-4,
            dst=True,
            classification='emerging',
            hemisphere='south',
        ),
        'Colombia': Market(
            label='Colombia',
            start=9.5,
            end=17,
            tz=-4,
            dst=True,
            classification='emerging',
        ),
        'Peru': Market(
            label='Peru',
            start=9,
            end=16,
            tz=-4,
            dst=True,
            classification='emerging',
        ),
    },
    'Europe, Middle East, & Africa':{
        'UK': Market(
            label='UK',
            start=8,
            end=16.5,
            tz=+1,
            dst=True,
            classification='developed',
        ),
        'Germany': Market(
            label='Germany',
            start=9,
            end=20,
            tz=+2,
            dst=True,
            classification='developed',
        ),
        'Austria': Market(
            label='Austria',
            start=9,
            end=17.5,
            tz=+2,
            dst=True,
            classification='developed',
        ),
        'Belgium': Market(
            label='Belgium',
            start=9,
            end=17.5,
            tz=+2,
            dst=True,
            classification='developed',
        ),
        'Denmark': Market(
            label='Denmark',
            start=9,
            end=17,
            tz=+2,
            dst=True,
            classification='developed',
        ),
        'Finland': Market(
            label='Finland',
            start=10,
            end=18.5,
            tz=+2,
            dst=True,
            classification='developed',
        ),
        'France': Market(
            label='France',
            start=9,
            end=17.5,
            tz=+2,
            dst=True,
            classification='developed',
        ),
        'Israel': Market(
            label='Israel',
            start=9,
            end=17.5,
            tz=+2,
            dst=True,
            classification='developed',
        ),
        'Ireland': Market(
            label='Ireland',
            start=8,
            end=16.5,
            tz=+2,
            dst=True,
            classification='developed',
        ),
        'Italy': Market(
            label='Italy',
            start=9,
            end=17.5,
            tz=+2,
            dst=True,
            classification='developed',
        ),
        'Netherlands': Market(
            label='Netherlands',
            start=9,
            end=17.5,
            tz=+2,
            dst=True,
            classification='developed',
        ),
        'Norway': Market(
            label='Norway',
            start=9,
            end=16.34,
            tz=+2,
            dst=True,
            classification='developed',
        ),
        'Portugal': Market(
            label='Portugal',
            start=8,
            end=16.5,
            tz=+1,
            dst=True,
            classification='developed',
        ),
        'Spain': Market(
            label='Spain',
            start=9,
            end=17.5,
            tz=+2,
            dst=True,
            classification='developed',
        ),
        'Sweden': Market(
            label='Sweden',
            start=9,
            end=17.5,
            tz=+2,
            dst=True,
            classification='developed',
        ),
        'Switzerland': Market(
            label='Switzerland',
            start=9,
            end=17.34,
            tz=+2,
            dst=True,
            classification='developed',
        ),
        'Czech Republic': Market(
            label='Czech Republic',
            start=9,
            end=16.34,
            tz=+2,
            dst=True,
            classification='emerging',
        ),
        'Egypt': Market(
            label='Egypt',
            start=10,
            end=14.5,
            tz=+2,
            dst=False,
            classification='emerging',
        ),
        'Greece': Market(
            label='Greece',
            start=10,
            end=17.42,
            tz=+3,
            dst=True,
            classification='emerging',
        ),
        'Hungary': Market(
            label='Hungary',
            start=10,
            end=17,
            tz=+2,
            dst=True,
            classification='emerging',
        ),
        'Poland': Market(
            label='Poland',
            start=8,
            end=16,
            tz=+2,
            dst=True,
            classification='emerging',
        ),
        'Qatar': Market(
            label='Qatar',
            start=9.5,
            end=13.25,
            tz=+3,
            dst=False,
            classification='emerging',
        ),
        'Russia': Market(
            label='Russia',
            start=10,
            end=18.75,
            tz=+3,
            dst=False,
            classification='emerging',
        ),
        'South Africa': Market(
            label='South Africa',
            start=9,
            end=17,
            tz=+2,
            dst=False,
            classification='emerging',
        ),
        'Turkey (AM)': Market(
            label='Turkey',
            start=9.5,
            end=12.5,
            tz=+3,
            dst=False,
            classification='emerging',
        ),
        'Turkey (PM)': Market(
            label='Turkey',
            start=14,
            end=17.5,
            tz=+3,
            dst=False,
            classification='emerging',
        ),
        'UAE': Market(
            label='UAE',
            start=10,
            end=14,
            tz=+4,
            dst=False,
            classification='emerging',
        ),
    },
    'Asia-Pacific': {
        'Japan (AM)': Market(
            label='Japan',
            start=9,
            end=11.5,
            tz=+9,
            dst=False,
            classification='developed',
        ),
        'Japan (PM)': Market(
            label='Japan',
            start=12.5,
            end=15,
            tz=+9,
            dst=False,
            classification='developed',
        ),
        'Singapore (AM)': Market(
            label='Singapore',
            start=9,
            end=12,
            tz=+8,
            dst=False,
            classification='developed',
        ),
        'Singapore (PM)': Market(
            label='Singapore',
            start=13,
            end=17,
            tz=+8,
            dst=False,
            classification='developed',
        ),
        'Hong Kong (AM)': Market(
            label='Hong Kong',
            start=9.5,
            end=12,
            tz=+8,
            dst=False,
            classification='developed',
        ),
        'Hong Kong (PM)': Market(
            label='Hong Kong',
            start=13,
            end=16,
            tz=+8,
            dst=False,
            classification='developed',
        ),
        'Australia': Market(
            label='Australia',
            start=10,
            end=17,
            tz=+11,
            dst=True,
            classification='developed',
            hemisphere='south',
        ),
        'New Zealand': Market(
            label='New Zealand',
            start=10,
            end=16,
            tz=+13,
            dst=True,
            classification='developed',
            hemisphere='south',
        ),
        'China (AM)': Market(
            label='China',
            start=9.5,
            end=11.5,
            tz=+8,
            dst=False,
            classification='emerging',
        ),
        'China (PM)': Market(
            label='China',
            start=13,
            end=15,
            tz=+8,
            dst=False,
            classification='emerging',
        ),
        'India': Market(
            label='India',
            start=9.25,
            end=15.5,
            tz=+5.5,
            dst=False,
            classification='emerging',
        ),
        'Indonesia (AM)': Market(
            label='Indonesia',
            start=9,
            end=12,
            tz=+7,
            dst=False,
            classification='emerging',
        ),
        'Indonesia (PM)': Market(
            label='Indonesia',
            start=13.5,
            end=16,
            tz=+7,
            dst=False,
            classification='emerging',
        ),
        'Korea': Market(
            label='Korea',
            start=9,
            end=15.5,
            tz=+9,
            dst=False,
            classification='emerging',
        ),
        'Malaysia (AM)': Market(
            label='Malaysia',
            start=9,
            end=12.5,
            tz=+8,
            dst=False,
            classification='emerging',
        ),
        'Malaysia (PM)': Market(
            label='Malaysia',
            start=14.5,
            end=17,
            tz=+8,
            dst=False,
            classification='emerging',
        ),
        'Pakistan': Market(
            label='Pakistan',
            start=9.5,
            end=15.5,
            tz=+5,
            dst=False,
            classification='emerging',
        ),
        'Philippines (AM)': Market(
            label='Philippines',
            start=9.5,
            end=12,
            tz=+8,
            dst=False,
            classification='emerging',
        ),
        'Philippines (PM)': Market(
            label='Philippines',
            start=13.5,
            end=16.5,
            tz=+8,
            dst=False,
            classification='emerging',
        ),
        'Taiwan': Market(
            label='Taiwan',
            start=9,
            end=13.5,
            tz=+8,
            dst=False,
            classification='emerging',
        ),
        'Thailand (AM)': Market(
            label='Thailand',
            start=10,
            end=12.5,
            tz=+7,
            dst=False,
            classification='emerging',
        ),
        'Thailand (PM)': Market(
            label='Thailand',
            start=14.5,
            end=16.5,
            tz=+7,
            dst=False,
            classification='emerging',
        ),
    }
}
In [4]:
# Gets the halfway point between two dates so that we can add root symbols as 
# labels in the center of each horizontal line on our chart.
def x_center(start, end):
    half_diff = (end - start) / 2
    midpoint = start + half_diff
    return midpoint


# Plot a market as a timespan.
def plot_timespan(market, y, c, a, lw, tz_shift, dst_lines, mode, min_x, max_x, tiling_offset):
    
    if market.classification == 'emerging':
        a = a*0.4
        lw = lw*0.4
    
    # Sometimes, we want to denote that a timespan shifts depending on whether it is daylight
    # savings time or not. This block of code adds dashed lines on either end of a timespan
    # if we want to denote this.
    if dst_lines | ((market.hemisphere == 'south') & (not dst_lines) & (mode == 'et')):
        if ((market.hemisphere == 'south') & (not dst_lines)):
            adj_dst_start = (market.start + tz_shift - tiling_offset - 1) % 24 + tiling_offset
            adj_start = (market.start + tz_shift - tiling_offset + 1) % 24 + tiling_offset
            adj_end = (market.end + tz_shift - tiling_offset - 1) % 24 + tiling_offset
            adj_dst_end = (market.end + tz_shift - tiling_offset + 1) % 24 + tiling_offset
        else:
            adj_dst_start = (market.start + tz_shift - tiling_offset) % 24 + tiling_offset
            adj_start = (market.start + tz_shift - tiling_offset + 1) % 24 + tiling_offset
            adj_end = (market.end + tz_shift - tiling_offset) % 24 + tiling_offset
            adj_dst_end = (market.end + tz_shift - tiling_offset + 1) % 24 + tiling_offset
            
        plt.scatter(adj_dst_start, y, s=75, c=c, alpha=a)
        plt.scatter(adj_dst_end, y, s=75, c=c, alpha=a)

        # If the beginning of the timespan does not wrap around the end of the plot.
        if adj_dst_start <= adj_start:
            plt.hlines(y = y, xmin=adj_dst_start, xmax=adj_start, color=c, alpha=a, linestyles='dashed', linewidth=lw)
        # If the beginning of the timespan DOES wrap around the end of the plot, we need a special case.
        else:
            plt.hlines(y = y, xmin=adj_dst_start, xmax=max_x, color=c, alpha=a, linestyles='dashed', linewidth=lw)
            plt.hlines(y = y, xmin=min_x, xmax=adj_start, color=c, alpha=a, linestyles='dashed', linewidth=lw)

        # If the end of the timespan does not wrap around the end of the plot.
        if adj_end <= adj_dst_end:
            plt.hlines(y = y, xmin=adj_end, xmax=adj_dst_end, color=c, alpha=a, linestyles='dashed', linewidth=lw)
        # If the end of the timespan DOES wrap around the end of the plot, we need a special case.
        else:
            plt.hlines(y = y, xmin=adj_end, xmax=max_x, color=c, alpha=a, linestyles='dashed', linewidth=lw)
            plt.hlines(y = y, xmin=min_x, xmax=adj_dst_end, color=c, alpha=a, linestyles='dashed', linewidth=lw)
    # If it's not applicable, just plan it as a regular horizontal line between two scatter
    # points.
    else:
        adj_start = (market.start + tz_shift - tiling_offset) % 24 + tiling_offset
        adj_end = (market.end + tz_shift - tiling_offset) % 24 + tiling_offset

        plt.scatter(adj_start, y, s=75, c=c, alpha=a)
        plt.scatter(adj_end, y, s=75, c=c, alpha=a)
    
    # If the timespan does not wrap around the end of the plot.
    if adj_start <= adj_end:
        plt.hlines(y = y, xmin=adj_start, xmax=adj_end, color=c, alpha=a, linewidth=lw)
        plt.text(x_center(adj_start, adj_end), y+0.2, market.label)
    # If the timespan DOES wrap around the end of the plot, we need a special case.
    else:
        plt.hlines(y = y, xmin=adj_start, xmax=max_x, color=c, alpha=a, linewidth=lw)
        plt.hlines(y = y, xmin=min_x, xmax=adj_end, color=c, alpha=a, linewidth=lw)
        plt.text(x_center(adj_start, max_x), y+0.2, market.label)
        plt.text(x_center(min_x, adj_end), y+0.2, market.label)

# Function for plotting a horizontal line + 2 dots at each end for a given future.
def plot_market(market, y, c, a, lw, mode, min_x, max_x, tiling_offset):
    if mode == 'local':
        tz_shift = 0
    elif mode == 'utc':
        tz_shift = -market.tz
    elif mode == 'et':
        tz_shift = -market.tz - 4
      
    if mode == 'local':
        plot_timespan(market, y, c, a, lw, tz_shift, False, mode, min_x, max_x, 0)
            
    elif mode == 'utc':
        plot_timespan(market, y, c, a, lw, tz_shift, market.dst, mode, min_x, max_x, tiling_offset)
            
    elif mode == 'et':
        plot_timespan(market, y, c, a, lw, tz_shift, (not market.dst), mode, min_x, max_x, tiling_offset)

            
In [5]:
def build_and_plot_market(market_data_list, mode='local', min_x=7, max_x=22, tiling_offset=0):
# market_data_list: list of Market objects built from the dict of hard coded Markets
# earlier in the notebook.
# 
# mode: selects the time zone perspective to plot.
# 'local' displays market hours in their local time.
# 'utc' displays market hours in UTC.
# 'et' displays market hours in Eastern Time.
# 
# min_x: defines the minimum time (hour) on the x axis of the plot.
# 
# max_x: defines the maximum time (hour) on the x axis of the plot.
# 
# tiling_offset: shifts the UTC and ET plots so that you can customize
# which region is in the middle or at the end. tiling_offset is ignored
# in 'local' mode.
    
    min_x = min_x + tiling_offset
    max_x = max_x + tiling_offset
    
    # Sort markets by start date (might need to include timezone adjustment).
    if mode == 'local':
        market_data_list = sorted(market_data_list, key=lambda x: x.start)
    elif mode == 'utc':
        market_data_list = sorted(market_data_list, key=lambda x: x.start + x.tz)
    elif mode == 'et':
        market_data_list = sorted(market_data_list, key=lambda x: x.start + x.tz)

    fig = plt.figure(figsize=(16, 14))
    ax = plt.gca()

    # 24 xticks.
    xticks = plt.xticks(np.linspace(0, 24, num=25))

    # Hide the y axis since it doesn't represent anything.
    ax.get_yaxis().set_visible(False)

    # Need a special case for Japan since it is stored in 2 segments.
    y_coords = {}
    # y_counter is used to determine the vertical placement of a timespan.
    y_counter = 0
    for market_data in market_data_list:
        if market_data.label in y_coords:
            y_coord = y_coords[market_data.label]
            y_counter = y_counter -1
        else:
            y_coord = y_counter
            y_coords[market_data.label] = y_counter

        # Plot the market as a horizontal timespan on the plot.
        plot_market(market_data, y_coord*1.2, region_colors[market_regions[market_data.label]], 1, 3, 
                    mode, min_x, max_x, tiling_offset)

        # Increment the y counter.
        y_counter += 1

    # Show gridlines on each hour.
    ax.xaxis.grid(True, which='major')

    # Plot the legend (region --> color mappings).
    legend_elements = []
    for classification in ['emerging', 'developed']:
        if classification == 'developed':
            a = 1
            lw = 3
        else:
            a = 0.4
            lw = 1.2
        for region in regions:
            legend_elements.append(
                plt.Line2D([], [], color=region_colors[region],
                            markersize=15, label=region + ', %s' % classification, linewidth=lw, alpha=a))
    legend_elements.reverse()

    ax.set_ylim([-5, 60])

    # Various plot settings like title, axis label, and legend placement depend on the mode.    
    if mode == 'local':
        ax.set_title('Global Equity Market Hours in Local Time')
        ax.set_xlim([min_x, max_x])
        plt.xlabel('Local Time of Day')
        plt.legend(handles=legend_elements, bbox_to_anchor=(1, 0.17), fontsize='large');
    elif mode == 'utc':
        ax.set_title('Global Equity Market Hours in UTC')
        ax.set_xlim([min_x, max_x])
        ax.set_xticks(range(min_x, max_x))
        plt.xlabel('Time of Day (UTC)')
        plt.legend(handles=legend_elements, bbox_to_anchor=(0.32, 0.17), fontsize='medium');
    elif mode == 'et':
        ax.set_title('Global Equity Market Hours in ET')
        ax.set_xlim([min_x, max_x])
        ax.set_xticks(range(min_x, max_x))
        plt.xlabel('Time of Day (ET)')
        plt.legend(handles=legend_elements, bbox_to_anchor=(0.92, 1), fontsize='large');
In [6]:
regions = markets.keys()
colors = cm.rainbow(np.linspace(0, 1, len(regions)))

# Map the regions to the colors.
region_colors = {}
for i in range(len(regions)):
    region_colors[regions[i]] = colors[i]

# Get a list of markets.
market_data_list = []
market_regions = {}
for region in markets:
    for market_data in markets[region].values():
        market_data_list.append(market_data)
        market_regions[market_data.label] = region
In [7]:
build_and_plot_market(market_data_list, mode='utc', min_x=0, max_x=24, tiling_offset=0)
In [8]:
build_and_plot_market(market_data_list, mode='et', min_x=0, max_x=24, tiling_offset=-7)
In [9]:
build_and_plot_market(market_data_list, mode='local', min_x=7, max_x=22, tiling_offset=0)