10Sept2018
In [1]:
#following part creates a button to hide code

from IPython.display import HTML
HTML('''<script>
function code_toggle() {
if (code_shown){
$('div.input').hide('500');
$('#toggleButton').val('Show Code')
} else {
$('div.input').show('500');
$('#toggleButton').val('Hide Code')
}
code_shown = !code_shown
}
$( document ).ready(function(){
code_shown=false;
$('div.input').hide()
});
</script>
<form action="javascript:code_toggle()"><input type="submit" id="toggleButton" value="Show Code"></form>''')
Out[1]:
In [2]:
!pip install python-highcharts
Requirement already satisfied: python-highcharts in /opt/conda/lib/python3.6/site-packages
Requirement already satisfied: future in /opt/conda/lib/python3.6/site-packages (from python-highcharts)
Requirement already satisfied: Jinja2 in /opt/conda/lib/python3.6/site-packages (from python-highcharts)
Requirement already satisfied: MarkupSafe>=0.23 in /opt/conda/lib/python3.6/site-packages (from Jinja2->python-highcharts)
You are using pip version 9.0.3, however version 18.0 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
In [3]:
from collections import defaultdict
from datetime import *
import numpy as np
import numpy
import os
import csv
import xlrd
import pandas as pd
from highcharts import Highchart, Highstock
import psycopg2
from scipy.stats import *

pd.set_option('display.float_format', lambda x: '%.8f' % x)
pd.set_option('display.max_columns',1000)
pd.set_option('display.max_rows',1000)
pd.options.mode.chained_assignment = None  # default='warn'
/opt/conda/lib/python3.6/site-packages/psycopg2/__init__.py:144: UserWarning: The psycopg2 wheel package will be renamed from release 2.8; in order to keep installing from binary please use "pip install psycopg2-binary" instead. For details see: <http://initd.org/psycopg/docs/install.html#binary-install-from-pypi>.
  """)
In [4]:
path='DATA_and_CODES/'
DailySTATS_F1_all=pd.read_csv(path+'DailySTATS_F1_all.csv')
DailySTATS_F1_act=pd.read_csv(path+'DailySTATS_F1_act.csv')
DailySTATS_F1_sus=pd.read_csv(path+'DailySTATS_F1_sus.csv')
DailySTATS_Mkt_F2_all=pd.read_csv(path+'DailySTATS_Mkt_F2_all.csv')
DailySTATS_Mkt_F2_act=pd.read_csv(path+'DailySTATS_Mkt_F2_act.csv')
DailySTATS_Mkt_F2_sus=pd.read_csv(path+'DailySTATS_Mkt_F2_sus.csv')
DailySTATS_FTSE_F1_all=pd.read_csv(path+'DailySTATS_FTSE_F1_all.csv')
DailySTATS_FTSE_F1_act=pd.read_csv(path+'DailySTATS_FTSE_F1_act.csv')
DailySTATS_FTSE_F1_sus=pd.read_csv(path+'DailySTATS_FTSE_F1_sus.csv')
DailySTATS_FTSE_Mkt_F2_all=pd.read_csv(path+'DailySTATS_FTSE_Mkt_F2_all.csv')
DailySTATS_FTSE_Mkt_F2_act=pd.read_csv(path+'DailySTATS_FTSE_Mkt_F2_act.csv')
DailySTATS_FTSE_Mkt_F2_sus=pd.read_csv(path+'DailySTATS_FTSE_Mkt_F2_sus.csv')
In [5]:
# Charts options stored locally
bar_option = eval(open(path+'bar_chart_option').read())
first_figure_options = eval(open(path+'first_figure_options').read())
chart_option = eval(open(path+'chart_option2').read())
options = eval(open(path+'options').read())
In [6]:
class Chart(Highchart):
    def __init__(self, title, height=None):
        super(Chart, self).__init__()
        options = {
            'tooltip': {
                "shared": True,
                "useHTML": True,
                #"headerFormat":'<span style="font-size: 10px">{point.key} days</span><br/>',
                "headerFormat":'<span style="font-size: 10px"><b>{point.key} days</span><br/>',
                "pointFormat":'<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.y}</b><br/>',
                      },
            'xAxis': {
                'labels': {
                    'format': '{value} days'}
                },
            'yAxis': {'opposite': False},
            'legend': {'enabled': True},
#             'rangeSelector': {'enabled': False},
#             'navigator': {'enabled': False},
            'chart': {
                'zoomType': 'xy',
                'marginTop': 60
            },
#             'scrollbar': {'enabled': False},
            'title': {'text': title},
            
        }
        if height:
            options['chart']['height'] = height
        self.set_dict_options(options=options)
In [7]:
# These values are mapping between var name in dataset and display name in charts
metric_display = {
    'MeanVolume':'Lit',
    'MeanDarkVolume': 'Dark',
    'MeanPAVolume':'Periodic Auction',
    'MeanSIVolume':'Systematic Internaliser',
    'Eff_Spread_valwgt':'',
 }
In [8]:
def plot_metrics_line(*,datasets, datasets_desc, metrics,summarise, yname='Value', title,limit=None):
    colors = [
    '#1A5276','#B9770E','#196F3D','#2874A6','#CA6F1E','#117864','#117A65','#196F3D','#239B56','#935116','#A04000']
    if not isinstance(datasets,list) or not isinstance(datasets,list):
        raise ValueError('Please pass the dataset and its description inside a list...!')
    if len(datasets) != len(datasets_desc):
        raise ValueError('length of datasets and description must match...!')
    chart = Chart(f'{title}')
    values = []
    if not isinstance(datasets,list):
        raise ValueError('Please pass the dataset inside a list...!')
    visible=True
    for m,metric in enumerate(metrics):
        for n, dataset in enumerate(datasets):                
            if summarise == 'sum':
                pic_data = dataset.groupby('count_date')[metric].sum().reset_index().values.tolist()  
                if metric=='MeanVolume':
                    visible=False
                else:
                    visible=True
                chart.add_data_set(pic_data, name=datasets_desc[n] + f' {metric_display[metric]}', visible=visible)

    chart.set_dict_options(options=chart_option)
    chart.set_dict_options(options={'yAxis': {'title':{'text': yname}}})
    
    return chart

def plot_metrics_bar(*,datasets, datasets_desc, metrics,yname='Value', summarise, title):
    colors = ['#66c2ff','#000066','#ff8566']
    if not isinstance(datasets,list) or not isinstance(datasets,list):
        raise ValueError('Please pass the dataset and its description inside a list...!')
    if len(datasets) != len(datasets_desc):
        raise ValueError('length of datasets and description must match...!')
    chart = Chart(f'{title}')
    values = []
    if not isinstance(datasets,list):
        raise ValueError('Please pass the dataset inside a list...!')
    for m,metric in enumerate(metrics):
        for n, dataset in enumerate(datasets):
            if summarise == 'sum':
                pic_data = dataset.groupby('count_date')[metric].sum().reset_index().values.tolist()            
                chart.add_data_set(pic_data, name=datasets_desc[n] + f' {metric_display[metric]}',
                                   visible=(n<2),type='column')

    chart.set_dict_options(options=bar_option)
    chart.set_dict_options(options={'yAxis': {'title':{'text': yname}}})
    return chart

def plot_venue(*,datasets, datasets_desc, metrics,yname='Value', title):
    if not isinstance(datasets,list) or not isinstance(datasets,list):
        raise ValueError('Please pass the dataset and its description inside a list...!')
    if len(datasets) != len(datasets_desc):
        raise ValueError('length of datasets and description must match...!')
    chart = Chart(f'{title}')
    values = []
    if not isinstance(datasets,list):
        raise ValueError('Please pass the dataset inside a list...!')
    for metric in metrics:
        for n,dataset in enumerate(datasets):
            pic_data = dataset[['count_date',metric]].values.tolist()            
            chart.add_data_set(pic_data, name=datasets_desc[n] + f'   {metric_display[metric]}',
                              visible = n<2)

    chart.set_dict_options(options=chart_option)
    chart.set_dict_options(options={'yAxis': {'title':{'text': yname}}})
    return chart

MiFID's Double Volume Cap Mechanism and its impact on UK trading markets

Highlights:

  • In the wake of suspensions resulting from the implementation of the Double Volume Cap Mechanism (DVCM), we observe increased trading activity in grey venues such as Periodic Auctions and Systematic Internaliser vis-a-vis lit venues, in stark contrast to the aims of MiFID II.
  • Transaction costs, measured by effective bid-ask spreads, increased for capped securities in venues operating dark and lit order books. Multivariate analysis identifies that capped securities, post the implementation of DVCM, experience lower effective spreads on the LSE and Aquis, suggesting the DVCM improves market liquidity on lit venues for affected securities.

I. Background

Following the release of MiFID II/ MiFIR, ESMA introduced a Double Volume Cap Mechanism (DVCM) (Art. 5 of MiFIR) to limit the quantity of dark trading and increase the overall transparency in EU markets. Specifically, the DVCM limits the use of two of the four Transparency Waivers (TW) available: (i) Reference Price and (ii) Negotiated Transaction, which in turn circumvent lit market trading.

Two mutually exclusive suspension criteria are applied as part of the DVCM: (i) if a security's trading volume in a venue via Price Reference or Negotiated Transaction TWs exceeds 4% of its total volume or (ii) if a security's trading volume via Reference or Negotiated Transaction TWs exceeds 8% of total volume across all EU trading venues. The first breach is referred to as Venue Level Suspension, while the second is an EU Level Suspension.

It is important to note that a security can breach EU Level caps without venue level limits being breached. Any security that breaches DVCM limits is suspended from trading in dark platforms via the two TWs for a period of six months. ESMA publishes a monthly report detailing the list of securities and their trading status (DVC Suspension File).

Presented below is an analysis of the impact of the DVCM based on ESMA’s report. We obtain data via the Market Quality Dashboard, which generates summary metrics of market fairness and efficiency derived from underlying Thomson Reuters Tick History Data (TRTH). TRTH data includes level 1 order book data in addition to qualifier information pertaining to Dark trades, Periodic Auctions and Systematic Internalisers.

II. The road to suspension

ESMA reports venue specific trading volume statistics for more than 20,000 securities traded across the EU. Suspensions enacted by the DVCM only became effective March 12, 2018, due to data-related issues encountered by ESMA in January and February. The first round of suspensions applied in March included 736 securities across the EU, and additional 19 were suspended at the Venue Level. On April 13, 2018, an additional 81 securities were suspended, 59 in May and 53 in June.

Reported below are summary statistics related to the proportion of suspended securities in each EU member state as of June 7, 2018.

In [9]:
import csv
def read_csv(filename, category_column):
    series = {}
    with open(filename, 'r') as fh:
        reader = csv.DictReader(fh, delimiter=',')
        for row in reader:
            for column in row:
                if column != category_column:
                    if column not in series:
                        series[column] = []
                    if row[column]:
                        series[column].append([row[category_column], float(row[column])])
    return series
In [10]:
#Specify colors via 'first_figure_options' and https://htmlcolorcodes.com/

chart = Highchart()
chart.set_dict_options(first_figure_options)
#CSV values derive from the esma suspension csv which is updated on a monthly base, country of origin of each isin 
#needs to be checked with the monthly report files and by research.
metric = read_csv(path+'CHART3_DVC_EU.csv', 'Country')

for serie_name in metric:
    if serie_name == 'Capped on 12th Mar 2018':
        chart.add_data_set(metric[serie_name], name=serie_name, type='column', turboThreshold=0,  )
    elif serie_name == 'Capped on 13th Apr 2018':
        chart.add_data_set(metric[serie_name], name=serie_name, type='column', turboThreshold=0, )
    elif serie_name == 'Capped on 14th May 2018':
        chart.add_data_set(metric[serie_name], name=serie_name, type='column', turboThreshold=0, )
    elif serie_name == 'Capped on 12th June 2018':
        chart.add_data_set(metric[serie_name], name=serie_name, type='column', turboThreshold=0, )
    elif serie_name == 'Remain uncapped after 12th June 2018':
        chart.add_data_set(metric[serie_name], name=serie_name, type='column', turboThreshold=0, )      
chart
Out[10]:

Figure 1 shows that by June 2018, Denmark applied the largest number of suspensions, followed by Finland, Czech Republic and The Netherlands. In terms of the UK, 17% of securities monitored by the Financial Conduct Authority have exceeded DVCM limits. Any such security is suspended from using TW for a period of 6 months unless they meet minimum trade size requirements.

III. How did the introduction of the DVCM impact the UK markets?

Given the significance of the LSE in terms of market capitalisation and liquidity, the subsequent analysis is limited to securities traded on major UK venues. By comparing capped and uncapped securities, we assess the impact of the DVCM on trading in lit and dark venues as well as in Periodic Auctions and Systematic Internalisers. We compare market quality metrics 21 days pre and post the imposition of security suspensions, aligning the four suspension events in March, April, May and June of 2018. Metric results are winsorised at a 5 % level to account for outliers.

III.A. Impact on Trading Activity

Figure 2 presents the aggregate trading volume across 5 UK venues, specifically LSE, Turquoise, CBOE BXE, CXE and Aquis. We observe that trading in Dark venues drops significantly following the imposition of TW caps, as expected. The daily trading volume in Periodic Auctions starts to increase 14 days prior the suspension of trading under TW and continues to increase after. We find a similar trend for trading via Systematic Internalisers. No significant changes are observed in trading volumes across lit venues which does not appear to be affected by the DVCM. Overall, the findings in Figure 2 do not reflect favourably on MiFID II’s aim to improve transparency by migrating dark trading to lit market venues.
Please note, the observed spike in lit markets on day 4, post the imposition of the caps, is due to the alignment of index expiries for Futures and Option Contracts scheduled every third Friday of the quarter.

In [11]:
DailySTATS_F1_all=pd.read_csv(path+'DailySTATS_F1_all.csv')
DailySTATS_F1_act=pd.read_csv(path+'DailySTATS_F1_act.csv')
DailySTATS_F1_sus=pd.read_csv(path+'DailySTATS_F1_sus.csv')
In [12]:
plot_metrics_line(datasets = [DailySTATS_F1_all],
     datasets_desc=[''                  
                   ],yname='Aggr. Daily Trade Volume', metrics = ['MeanVolume','MeanDarkVolume','MeanPAVolume','MeanSIVolume'], summarise = 'sum',
           title = 'Figure 2: Aggregated Lit, Dark, Periodic Auction and Systematic Internaliser Trading Volume', limit=1000)
Out[12]:

Figure 3, reports trading activity across Lit, Dark, Periodic Auctions and Systematic Internaliser venues, for capped and uncapped securities. Trading in lit venues by capped and uncapped securities depicts a similar pattern, which is somewhat unexpected, given the premise of the DVCM was to move trading of capped securities to lit venues. We find, for uncapped securities the trading volume in Periodic Auctions increases constantly over the event horizon, whereas for capped securities, trading volume in Periodic Auctions triples. A reversed trend can be observed in Dark trading: For capped securities, the daily trading volume exhibits a sharp decline on the day of suspension, this is also observed for securities not breaching caps. Surprisingly, trading volume via Systematic Internalisers does not appear affected for capped securities, however, trends upwards for uncapped securities. Hence, given in capped securities we observe no change in Lit or Systematic Internalisers volumes, we conclude the decline in dark trading is somewhat offset by an increase in Periodic Auctions.

In [13]:
plot_metrics_line(datasets = [DailySTATS_F1_act,DailySTATS_F1_sus],
     datasets_desc=['Uncapped securities:','Capped securities:'                  
                   ],yname='Aggr. Daily Trade Volume', metrics = ['MeanVolume','MeanDarkVolume','MeanPAVolume','MeanSIVolume'], summarise = 'sum',
           title = 'Figure 3: Aggregated Dark, Periodic Auction and Systematic Internaliser Trading Volume',limit=3)
Out[13]:

In Figure 4 we analyse the market share trading statistics across Dark venues and Grey markets (i.e. Period Auctions and Systematic Internalisers), excluding lit venues which seem unaffected. We find that for capped securities, the proportion of trading in Periodic Auctions increases by over 30%; with respect to Systematic Internalisers, which prior to the imposition of the DVCM represented 5% and now account for approximately 10% of trading. Following the implementation of DVCM, dark trading decreases by approximately 45%. Overall these results are consistent with views expressed by ESMA Chair, Steven Maijoor, that certain traders are exploiting Systematic Internalisers to circumvent restrictions imposed under the DVCM, since securities can still trade away from lit markets using other platforms or other waivers not monitored under the DVCM. For example, under MiFID II, trading by an investment bank using its capital and acting as a counterparty for their clients outside either a regulated market, a MTF (Multilateral Trading Facility) or OTF (Organised Trading Facility), and without the bank operating a multilateral system, is classified as SI and is consequently exempt from the DVCM.

In [14]:
plot_metrics_bar(datasets=[DailySTATS_F1_sus],
     datasets_desc=['Capped securities:'],yname='Share',
        metrics = ['MeanDarkVolume','MeanPAVolume','MeanSIVolume'], summarise = 'sum', 
                 title = 'Figure 4: Capped Securities: Trading Volume Proportion across Periodic Auctions, Systematic Internaliser and Dark Trade',)
Out[14]:

III.B. Impact on Liquidity and Volatility

Turning to an assessment of the DVCM under MiFID II/MiFIR on measures of market quality, Figure 5 reports spread measures for capped and uncapped securities across all venues. For each security $i$, we weight the daily effective spread measures $EffSprd_{i,d,v}$ by the venue $v$ specific trading volume to compute the daily volume-weighted $EffSprd_{d,v}$ across all securities on that venue. To compute a UK-wide effective spread, $EffSprd_{d}$, we value-weight daily measures across all lit venues.

\begin{align} vw avg. EffSprd_d=\sum_{v=1}^{V} (\sum_{i=1}^{I} ( EffSprd_{idv} \frac{Vol_{idv}}{\sum_{i=1}^{I} Vol_{idv}}))*\frac{Val_{dv}}{\sum_{v=1}^{V} Val_{dv}}) \end{align}

Figure 5 suggests, in isolation transaction costs for capped securities and uncapped securities exhibit no change as measured by effective spreads.

In [15]:
plot_metrics_line(datasets = [ DailySTATS_F1_act,DailySTATS_F1_sus],yname='Aggr. Effective Spread',
     datasets_desc=['Uncapped securities','Capped securities'], metrics = ['Eff_Spread_valwgt'], summarise = 'sum',  
           title = 'Figure 5: Liquidity Measures for Capped and Uncapped Securities')
Out[15]:
In [16]:
DailySTATS_Mkt_F2_all=pd.read_csv(path+'DailySTATS_Mkt_F2_all.csv')
DailySTATS_Mkt_F2_act=pd.read_csv(path+'DailySTATS_Mkt_F2_act.csv')
DailySTATS_Mkt_F2_sus=pd.read_csv(path+'DailySTATS_Mkt_F2_sus.csv')
In [17]:
stats_by_venue = \
[DailySTATS_Mkt_F2_act[DailySTATS_Mkt_F2_act.market_id==20],
DailySTATS_Mkt_F2_sus[DailySTATS_Mkt_F2_sus.market_id==20],
DailySTATS_Mkt_F2_act[DailySTATS_Mkt_F2_act.market_id==51],
DailySTATS_Mkt_F2_sus[DailySTATS_Mkt_F2_sus.market_id==51],
DailySTATS_Mkt_F2_act[DailySTATS_Mkt_F2_act.market_id==197],
DailySTATS_Mkt_F2_sus[DailySTATS_Mkt_F2_sus.market_id==197],
DailySTATS_Mkt_F2_act[DailySTATS_Mkt_F2_act.market_id==13],
DailySTATS_Mkt_F2_sus[DailySTATS_Mkt_F2_sus.market_id==13],
DailySTATS_Mkt_F2_act[DailySTATS_Mkt_F2_act.market_id==44],
DailySTATS_Mkt_F2_sus[DailySTATS_Mkt_F2_sus.market_id==44],
]

Figure 6, reports transaction costs separately for each of the 5 UK venues. Again we identify no discernible pattern when we do not control for other microstructure factors influencing spreads.

In [18]:
plot_venue(datasets = stats_by_venue,
     datasets_desc=['Turquoise: Uncapped securities','Turquoise: Capped securities',
                    'CBOE BXE: Uncapped securities','CBOE BXE: Capped securities',
                    'CBOE CXE: Uncapped securities','CBOE CXE: Capped securities',
                    'LSE: Uncapped securities','LSE: Capped securities',
                    'Aquis: Uncapped securities','Aquis: Capped securities',                   
                   ],yname='Effective Spread', metrics = ['Eff_Spread_valwgt'], 
           title = 'Figure 6: Liquidity Measures for Capped and Uncapped Securities across Lit Venues')
Out[18]:

To control for known determinants of transaction costs, such as volatility and trading volume, we estimate the following regression, specifically:

\begin{align} y_{ivd}=\beta_1 +\delta_1(LogVolume_{ivd}) +\delta_2(Volatility_{ivd}) +\delta_3(dummy_p) +\delta_4(dummy_c) +\delta_5(dummy_l) +\delta_6(dummy_p * dummy_c* dummy_l)+\varepsilon_{ivd}, \end{align}

The dependent variable $y_{ivd}$ refers to the daily value-weighted effective spread of security $i$ across either venues, $v$, which offer no dark trading or those doing so. We control for the daily trading volume and intraday volatility and include several indicator variables: $dummy_p$ is set equal to 1 for trading days post suspension dates and 0 otherwise; $dummy_c$ is equal to 1 if the security was suspended and 0 otherwise; and $ dummy_l$ equals 1 for venues that operate lit order books (ie. LSE and Aquis) and 0 otherwise.

Our variable of interest is the interaction of capped securities in venues that do not offer dark trading (i.e LSE and Aquis). Table 1 shows that such venues offer lower effective spreads post DVCM suspension by approximately 8.8 bps. However, this improvement is outweighted by the fact that "only lit" venues incur 25 bps higher transaction cost (on average) realtive to other venues which also operate dark pools.

In [19]:
def display_results(unit):
    with open(path+'TableES.csv', 'r') as input_file:
        reader = csv.reader(input_file)
        results = []

        count = 0
        for row in reader:
            count += 1
            if count > 29:
                break
            results.append([])
            for column in range(0, 3):
                results[count - 1].append(row[column])
    html = '<table style="width:55%; margin-left:19%">'
    for row_count, row in enumerate(results):
        if row_count > 29:
            break
        # add border
        if row_count in [1]:
            html += '<tr style="border-bottom: 2px solid black; border-top: 2px solid black;">'

        elif row_count in [1,9,16,23]:
            html += '<tr style="border-bottom: 1px solid black; border-top: 2px solid black;">'

        for column_count, column in enumerate(row):
            if row_count in [0] and column_count in [0]:
                html +='<td colspan="' + str(unit) + '" style="text-align: left"+> '
            elif row_count in [0]:
                #html +='<b>' + str(column) + '</b>'
                pass
            elif column_count in [0]:
                html += '<td style="text-align: left">'
            else:
               html += '<td>'
            # add content and finish this column

            if row_count == 1:
                html += '<i>' + str(column) + '</i>'
            elif row_count == 0:
                html += '<b>' +str(column)+ '</b>'
            else :
                html += str(column)
            html += '</td>'
        html += '</tr>'
    html += '</table>'
    display(HTML(html))

display_results(3)
Table 1: Impact of Suspension on Effective Spread
Coefficientt-Stat
Constant65.909***16.597
LogVolume-6.157***-17.154
Intraday Volatility0***37.128
Post 2.051***6.862
Capped-28.281***-13.558
Lit Venue25.478***15.569
Post*Capped*Lit Venue-8.831***-7.887

IV. Conclusion

Overall, the DVCM has proven to be effective in reducing Dark trading, particularly under the Reference Price and Negotiated Transaction pre-trade transparency waivers, however trading via Periodic Auction increased significantly. The DVCM does not seem to promote enhanced trading activity in pure lit order books, since despite lower transactions costs post a suspension on lit venues in general, the improvement is not significant enough to lead to a shift in trading activities.