import pandas as pd
from datetime import datetime, timedelta
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
# 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
# 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',
),
}
}
# 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)
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');
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
build_and_plot_market(market_data_list, mode='utc', min_x=0, max_x=24, tiling_offset=0)
build_and_plot_market(market_data_list, mode='et', min_x=0, max_x=24, tiling_offset=-7)
build_and_plot_market(market_data_list, mode='local', min_x=7, max_x=22, tiling_offset=0)