├── .gitignore ├── LICENSE ├── README.md └── rts ├── RTS2_Worked_Examples.ipynb ├── Trade data generation and testing.ipynb ├── Using sample trades in an SI calculation.ipynb ├── rts23_table2.py ├── rts2_annex3.py └── rts2_annex3_model.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # Mac stuff 104 | .DS_Store 105 | 106 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Bruce Badger 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mifid2-rts 2 | 3 | The aim of the mifid2-rts project is to persuade regulators to work in a different way, to communicate regulatory intent using working software examples rather than relying solely on legal tomes. 4 | 5 | As an example of how regulatory intent could be expressed in software this project implements parts of the [MiFID II RTS (Regulatory Technical Standard) documents](http://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:32017R0583&rid=1). 6 | 7 | Here are some introductory videos: 8 | * [An overview of the context and the objectives of the project](https://www.youtube.com/watch?v=kmV2jDNgH-Q). 9 | * [How to download the code from GitHub and run it](https://youtu.be/Hoo31-LJi4s). 10 | * [Using the mifid2-rts code to generate lots of sample trade data](https://youtu.be/1kUJu1-snVY). 11 | * [Running an SI calculation using the mifid2-rts code](https://youtu.be/hsOs4hFc9lA). 12 | 13 | And here are some Jupyter Notebook examples of the code in use: 14 | * [Working through the RTS 2 Annex II implementation](https://github.com/bwbadger/mifid2-rts/blob/master/rts/RTS2_Worked_Examples.ipynb). 15 | * [Generating test trade data](https://github.com/bwbadger/mifid2-rts/blob/master/rts/Trade%20data%20generation%20and%20testing.ipynb). 16 | * [Running the SI calculator over generated trade data](https://github.com/bwbadger/mifid2-rts/blob/master/rts/Using%20sample%20trades%20in%20an%20SI%20calculation.ipynb). 17 | 18 | 19 | ## Context for the mifid2-rts project 20 | 21 | [MiFID (Markets in Financial Instruments Directive)](https://en.wikipedia.org/wiki/Markets_in_Financial_Instruments_Directive_2004) is EU law for the regulation of the financial industry. The [original MiFID](http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=CELEX:32004L0039:EN:HTML) was introduced in 2004, and following the 2007/8 financial crisis a second version, [MiFID II](https://www.esma.europa.eu/policy-rules/mifid-ii-and-mifir), was introduced in 2014. 22 | 23 | The regulator [ESMA (European Securities and Markets Authority)](https://en.wikipedia.org/wiki/European_Securities_and_Markets_Authority) have been entrusted with enacting MiFID II. ESMA have produced documents which define what firms need to do to comply with MiFID II, a key set of which are the RTS (Regulatory Technical Standards) documents. Here we are focused on the [final 14th July 2016 version of RTS 2](http://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:32017R0583&rid=1). An early 2015 draft of the full set of RTS documents can be read [here](https://www.esma.europa.eu/sites/default/files/library/2015/11/2015-esma-1464_annex_i_-_draft_rts_and_its_on_mifid_ii_and_mifir.pdf). 24 | 25 | The code in this GitHub repository implements the taxonomies defined in RTS 2 Annex 3 and in RTS 23 table 2. 26 | 27 | The hope is that we can use this code either as an example of, or as the basis of, a body of code which is a working executable model of all MiFID II Regulatory Technical Standards, together with a body of test data which can illustrate how the technical standards are intended to work in practice. 28 | 29 | There are [some slides](https://docs.google.com/presentation/d/1sVgeO3IAO7ZMrbzAYWZtbu21MxFlESL0ONvJHUuFgBc/edit?usp=sharing) which present the the intent of the project. 30 | 31 | If you would like to see some trivial examples of the code in use, just run rts2_annex3.py from the command line. This will build the taxonomies and run a few simple example ‘trades’ through. The module dumps a representation of the RTS 2 taxonomy and the test trade classifications to stdout. 32 | 33 | ## A simple example 34 | 35 | Here is an example of building and classifying a sample trade. 36 | 37 | ```python 38 | import datetime 39 | import rts2_annex3 40 | 41 | class SampleTrade(object): 42 | pass 43 | 44 | sample_trade = SampleTrade() 45 | sample_trade.asset_class_name = 'Foreign Exchange Derivatives' 46 | sample_trade.sub_asset_class_name= 'Deliverable FX options (DO)' 47 | sample_trade.underlying_currency_pair = ('GBP~USD') 48 | sample_trade.from_date = datetime.date(2017, 8, 13) 49 | sample_trade.to_date = datetime.date(2017, 10, 12) 50 | 51 | sample_classification = rts2_annex3.class_root.classification_for(sample_trade) 52 | sample_classification.classification_dict() 53 | { 54 | 'RTS2 version': 'Brussels, 14.7.2016 C(2016) 4301 final ANNEXES 1 to 4', 55 | 'Asset class': 'Foreign Exchange Derivatives', 56 | 'Sub-asset class': 'Deliverable FX options (DO)', 57 | 'Segmentation criterion 1 description': 'underlying currency pair defined as combination of the two currencies underlying the derivative contract', 58 | 'Segmentation criterion 1': "('GBP~USD')", 59 | 'Segmentation criterion 2 description': 'time to maturity bucket of the swap defined as follows:', 60 | 'Segmentation criterion 2': 'Maturity bucket 2: 1 week to 3 months', 61 | } 62 | 63 | # ... or as Json: 64 | print(sample_classification.as_json(indent=4)) 65 | { 66 | "RTS2 version": "EU 2017/583 of 14 July 2016", 67 | "Asset class": "Foreign Exchange Derivatives", 68 | "Sub-asset class": "Deliverable FX options (DO)", 69 | "Segmentation criterion 1 description": "underlying currency pair defined as combination of the two currencies underlying the derivative contract", 70 | "Segmentation criterion 1": "GBP~USD", 71 | "Segmentation criterion 2 description": "time to maturity bucket of the swap defined as follows:", 72 | "Segmentation criterion 2": "Maturity bucket 2: 1 week to 3 months" 73 | } 74 | ``` 75 | 76 | -------------------------------------------------------------------------------- /rts/RTS2_Worked_Examples.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Read me\n", 8 | "\n", 9 | "The objective of [this project](https://github.com/bwbadger/mifid2-rts) is to show that it is better to express regulatory requirements using executable expressions which all interested parties can share and run and test, rather than to express regulatory requirements as text documents which everyone has to interpret for themselves.\n", 10 | "\n", 11 | "This page is a Jupyter notebook. It is a combination of a document and some live software which you can execute if you are running your own jupyter-notebook server. If you are not running a Jupyter server you can still read the document and see the code examples - you just can't run them.\n", 12 | "\n", 13 | "If you see something wrong on this page or in the code, please [create an issue in the GitHub project](https://github.com/bwbadger/mifid2-rts/issues).\n", 14 | "\n", 15 | "\n", 16 | "## MiFID II classification of trades using the RTS 2 Annex 3 taxonomy.\n", 17 | "\n", 18 | "Governments would prefer to avoid another financial crisis like the one in 2008 and believe that making the big players operate in a more open and transparent way will help avoid another crash.\n", 19 | "\n", 20 | "Markets in Financial Instruments Directive II (MiFID II) is an EU law which has market transparency as its key objective. The predecessor law, MiFID I, only looked at a part of what banking firms do. MiFID II aims to cover most mainstream activity.\n", 21 | "\n", 22 | "Governments rely on regulators to make sure that their laws are being followed. For MiFID II the primary regulator is ESMA. ESMA have produced a number of Regulatory Technical Standard (RTS) documents which aim to explain what banking firms must do to comply with the MiFID II law.\n", 23 | "\n", 24 | "One of the RTS documents, RTS 2, explains how different kinds of trading activity can be identified. Having a clear way to say what has been traded is an important part of making the markets more transparent.\n", 25 | "\n", 26 | "Some kinds of trading activity are already pretty transparent, for example buying and selling shares in a public company. Trades of this kind are mostly done using a public exchange, such as the New York or London stock exchanges. The 'price' for a given stock is simply the amount of money paid in the most recent trade and this price is made public by the exchange so everyone can see what the latest price is. It is pretty easy to identify what has been traded because each stock has a identifier, e.g. 'AAPL' identifies Apple Inc. shares.\n", 27 | "\n", 28 | "Not all trades happen on public exchanges. Many trades happen directly between two parties and these are known as over the counter (OTC) trades. Each OTC trade can be fine-tuned, for example setting payment dates and interest rates. The fine tuning of OTC trades makes it hard to give an identity to what has been traded, but this is where RTS 2 comes in. \n", 29 | "\n", 30 | "The easiest way to understand what RTS 2 is all about is to use it to classify some trades, and you can do just that below.\n", 31 | "\n", 32 | "### A Python Implementation of RTS 2 Annex 3\n", 33 | "\n", 34 | "It would be nice if ESMA published a working software implememtation of the RTS rules along with some test data so people can see exactly how the rules are supposed to work, and how reports are supposed to look. But ESMA don't do that. Each participant must somehow get an implementation of the RTS rules, either by writing it themselves or buying an implementation.\n", 35 | "\n", 36 | "One market participant implemented the RTS rules themselves and have now released part of that implementation under an open source license, the BSD license, so anyone can see the implementaion and use it. This document forms a part of that release.\n", 37 | "\n", 38 | "Hopefully this software will encourage ESMA to produce reference implementaions of their rules in future. They could even take this software as a starting point.\n", 39 | "\n", 40 | "The software here is written in the Python programming language. Python was chosen because the language is ubiquitous, that is it can be used easily and immediately on most modern computers; everything from a Raspberry Pi to the largest of big data clusters.\n", 41 | "\n", 42 | "### Running a really simple initial classification\n", 43 | "\n", 44 | "The box below contains python code which runs the classification software. If you are just viewing this page then you won't be able to run the code, but if you start the page using your own local Jupyter notebook server then the code will really run if you select the box below and press control+enter. If you can run the code you might like to try changing the values of the attributes below (e.g. to_date) to see what happens.\n" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": 1, 50 | "metadata": { 51 | "collapsed": false 52 | }, 53 | "outputs": [ 54 | { 55 | "data": { 56 | "text/plain": [ 57 | "OrderedDict([('RTS2 version', 'EU 2017/583 of 14 July 2016'),\n", 58 | " ('Asset class', 'Foreign Exchange Derivatives'),\n", 59 | " ('Sub-asset class', 'Deliverable FX options (DO)'),\n", 60 | " ('Segmentation criterion 1 description',\n", 61 | " 'underlying currency pair defined as combination of the two currencies underlying the derivative contract'),\n", 62 | " ('Segmentation criterion 1', 'GBP~USD'),\n", 63 | " ('Segmentation criterion 2 description',\n", 64 | " 'time to maturity bucket of the swap defined as follows:'),\n", 65 | " ('Segmentation criterion 2',\n", 66 | " 'Maturity bucket 2: 1 week to 3 months')])" 67 | ] 68 | }, 69 | "execution_count": 1, 70 | "metadata": {}, 71 | "output_type": "execute_result" 72 | } 73 | ], 74 | "source": [ 75 | "# Import the RTS 2 module and the Python date & time tools module\n", 76 | "import rts2_annex3\n", 77 | "import datetime\n", 78 | "\n", 79 | "# Create a simple Python object to represent a trade.\n", 80 | "class SampleTrade(object):\n", 81 | " pass\n", 82 | "sample_trade = SampleTrade()\n", 83 | "sample_trade.asset_class_name = 'Foreign Exchange Derivatives'\n", 84 | "sample_trade.sub_asset_class_name= 'Deliverable FX options (DO)'\n", 85 | "sample_trade.underlying_currency_pair = ('GBP~USD')\n", 86 | "sample_trade.from_date = datetime.date(2017, 9, 13)\n", 87 | "sample_trade.to_date = datetime.date(2017, 10, 12)\n", 88 | "\n", 89 | "# Now classify the trade\n", 90 | "sample_classification = rts2_annex3.class_root.classification_for(sample_trade)\n", 91 | "\n", 92 | "# Lastly, display the classificaton\n", 93 | "sample_classification.classification_dict()" 94 | ] 95 | }, 96 | { 97 | "cell_type": "markdown", 98 | "metadata": {}, 99 | "source": [ 100 | "### Understanding a classification\n", 101 | "\n", 102 | "The classification is shown here as a Python dictionary, but one could imagine presenting this classification in many ways ... and this is a problem. What is the official accepted literal form of an RTS 2 classification? Nobody seems to know. So let's go with this dictionary for now.\n", 103 | "\n", 104 | "Another point about the above representation is that it is very big, and the example is not one of the biggest! The reason for the size is that the text which appears is exactly as it appears in RTS 2. There is no obvious way to shorten the classification without inventing something, and that would open the door to arguments about what is right. This way, the classification links back to the RTS document in an extremely obvious, if verbose, way. No arguments.\n", 105 | "\n", 106 | "To a large degree, the classification is simply repeating the information we gave our sample_trade object in the code above, but information has been checked and other information added.\n", 107 | "\n", 108 | "This classification first confirms the identity of the RTS document the classification is based upon. The RTS rules may change over time, so it is important to know which version of the RTS a particular classification is based upon.\n", 109 | "\n", 110 | "Next we see the Asset class and Sub-asset class, which is repeating just what we said above. When classifying a trade there are some things you just have to know. There will be some help on how to choose Asset classes and Sub-asset classes below.\n", 111 | "\n", 112 | "Then we see something we didn't include in our trade object. The RTS 2 Annex 3 document defines a number of criteria for each kind of Sub-asset class. The Sub-asset class in this case has two criteria, and the classification included the description, the exact text, from the RTS document to explain what the criteria mean.\n", 113 | "\n", 114 | "The values for the criteria do come from the values on our object, but some involve calculation. The currency pair criterion, criterion 1, is simply the name of underlying_currency_pair value we provided. Criterion 2 gets its value from date calculations which use the from and to dates we gave; the resulting value is a date bucket, bucket 2 in this case.\n", 115 | "\n", 116 | "### Would Json be a better format for classifications?\n", 117 | "\n", 118 | "Because the classification is just a Python object we can change its implementation to render the classification in any way we please, or we can take the dictionary it currently produces and convert it to something else. Here, the classification above is shown as json:" 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": 2, 124 | "metadata": { 125 | "collapsed": false 126 | }, 127 | "outputs": [ 128 | { 129 | "name": "stdout", 130 | "output_type": "stream", 131 | "text": [ 132 | "{\n", 133 | " \"RTS2 version\": \"EU 2017/583 of 14 July 2016\",\n", 134 | " \"Asset class\": \"Foreign Exchange Derivatives\",\n", 135 | " \"Sub-asset class\": \"Deliverable FX options (DO)\",\n", 136 | " \"Segmentation criterion 1 description\": \"underlying currency pair defined as combination of the two currencies underlying the derivative contract\",\n", 137 | " \"Segmentation criterion 1\": \"GBP~USD\",\n", 138 | " \"Segmentation criterion 2 description\": \"time to maturity bucket of the swap defined as follows:\",\n", 139 | " \"Segmentation criterion 2\": \"Maturity bucket 2: 1 week to 3 months\"\n", 140 | "}\n" 141 | ] 142 | } 143 | ], 144 | "source": [ 145 | "print(sample_classification.as_json(indent=4))" 146 | ] 147 | }, 148 | { 149 | "cell_type": "markdown", 150 | "metadata": {}, 151 | "source": [ 152 | "### RTS 2 Annex 3 defines a taxonomy. A Tree.¶\n", 153 | "\n", 154 | "To understand how the classification process works we need to look at what the RTS says.\n", 155 | "\n", 156 | "The RTS 2 Annex 3 taxonomy is made up of Asset classes which get broken down into Sub-asset classes which are further broken down by combinations of criteria values.\n", 157 | "\n", 158 | "Here is some code to list the names of elements of the taxonomy:" 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": 3, 164 | "metadata": { 165 | "collapsed": false 166 | }, 167 | "outputs": [ 168 | { 169 | "data": { 170 | "text/plain": [ 171 | "['Bonds (all bond types except ETCs and ETNs)',\n", 172 | " 'Bonds (ETC and ETN bond types)',\n", 173 | " 'Structured Finance Products (SFPs)',\n", 174 | " 'Securitised Derivatives',\n", 175 | " 'Interest Rate Derivatives',\n", 176 | " 'Equity Derivatives',\n", 177 | " 'Commodity Derivatives',\n", 178 | " 'Foreign Exchange Derivatives',\n", 179 | " 'Credit Derivatives',\n", 180 | " 'C10 Derivatives',\n", 181 | " 'Financial contracts for differences (CFDs)',\n", 182 | " 'Emission Allowances',\n", 183 | " 'Emission Allowance Derivatives']" 184 | ] 185 | }, 186 | "execution_count": 3, 187 | "metadata": {}, 188 | "output_type": "execute_result" 189 | } 190 | ], 191 | "source": [ 192 | "# The root of the taxonomy is rts2_annex3.class_root. Here we ask the\n", 193 | "# root for the asset classes, and then ask each asset class for its name.\n", 194 | "# The names are exactly the names of the Asset classes you'll see in the RTS document.\n", 195 | "[asset_class.name for asset_class in rts2_annex3.class_root.asset_classes]" 196 | ] 197 | }, 198 | { 199 | "cell_type": "code", 200 | "execution_count": 4, 201 | "metadata": { 202 | "collapsed": false 203 | }, 204 | "outputs": [ 205 | { 206 | "data": { 207 | "text/plain": [ 208 | "['Non-deliverable forward (NDF)',\n", 209 | " 'Deliverable forward (DF)',\n", 210 | " 'Non-Deliverable FX options (NDO)',\n", 211 | " 'Deliverable FX options (DO)',\n", 212 | " 'Non-Deliverable FX swaps (NDS)',\n", 213 | " 'Deliverable FX swaps (DS)',\n", 214 | " 'FX futures',\n", 215 | " 'Other Foreign Exchange Derivatives']" 216 | ] 217 | }, 218 | "execution_count": 4, 219 | "metadata": {}, 220 | "output_type": "execute_result" 221 | } 222 | ], 223 | "source": [ 224 | "# Each asset class is broken down into Sub-asset classes. \n", 225 | "# So now we take the FX Derivatives asset class and display the names of \n", 226 | "# its children, the sub-asset classes.\n", 227 | "fx_asset_class = rts2_annex3.class_root.asset_class_by_name('Foreign Exchange Derivatives')\n", 228 | "[sub_asset_class.name for sub_asset_class in fx_asset_class.children]" 229 | ] 230 | }, 231 | { 232 | "cell_type": "code", 233 | "execution_count": 5, 234 | "metadata": { 235 | "collapsed": false 236 | }, 237 | "outputs": [ 238 | { 239 | "data": { 240 | "text/plain": [ 241 | "['underlying currency pair defined as combination of the two currencies underlying the derivative contract',\n", 242 | " 'time to maturity bucket of the swap defined as follows:']" 243 | ] 244 | }, 245 | "execution_count": 5, 246 | "metadata": {}, 247 | "output_type": "execute_result" 248 | } 249 | ], 250 | "source": [ 251 | "# Each sub-asset class has a number of criteria.\n", 252 | "# Here we ask the Deliverable FX Options sub-asset class to list its\n", 253 | "# criteria:\n", 254 | "fx_do_sub_asset_class = fx_asset_class.sub_asset_class_by_name('Deliverable FX options (DO)')\n", 255 | "[criterion.description for criterion in fx_do_sub_asset_class.criteria]" 256 | ] 257 | }, 258 | { 259 | "cell_type": "markdown", 260 | "metadata": {}, 261 | "source": [ 262 | "### Viewing the RTS 2 taxonomy using ipywidgets\n", 263 | "\n", 264 | "If you are running this notebook on a live Jupyter server then you can run the code below to display widgets which let you navigate the RTS 2 taxonomy.\n", 265 | "\n", 266 | "You can select an asset class in a drop-down widget. This then populates the sub-asset classes drop-down widget for the selected asset class. Selecting a sub-asset class causes the criteria for that sub-asset class to be displayed.\n", 267 | "\n", 268 | "Here is a screen shot of how the widgets look in action. In this example I have selected Energy Commodity Swaps which has seven criteria:\n", 269 | "\n", 270 | "![Screenshot of Energy Commodity Swaps](https://drive.google.com/uc?export=view&id=0B2yYXKtCVfspQTNUZlU2Mnh3a0U)" 271 | ] 272 | }, 273 | { 274 | "cell_type": "code", 275 | "execution_count": 6, 276 | "metadata": { 277 | "collapsed": false 278 | }, 279 | "outputs": [], 280 | "source": [ 281 | "import rts2_annex3\n", 282 | "import collections\n", 283 | "import ipywidgets as widgets\n", 284 | "from IPython.display import display\n", 285 | "\n", 286 | "asset_classes = rts2_annex3.class_root.asset_classes\n", 287 | "asset_class_dict = collections.OrderedDict([\n", 288 | " (an_asset_class.name, an_asset_class) \n", 289 | " for an_asset_class \n", 290 | " in asset_classes])\n", 291 | "\n", 292 | "asset_class_widget = widgets.Dropdown(\n", 293 | " options=asset_class_dict,\n", 294 | " description='Asset Classes:',\n", 295 | " disabled=False,\n", 296 | ")\n", 297 | "\n", 298 | "def sub_asset_class_dict(asset_class):\n", 299 | " return collections.OrderedDict([\n", 300 | " (sub_asset_class.name, sub_asset_class) \n", 301 | " for sub_asset_class \n", 302 | " in asset_class.sub_asset_classes])\n", 303 | "\n", 304 | "sub_asset_class_widget = widgets.Dropdown(\n", 305 | " options=sub_asset_class_dict(asset_class_widget.value),\n", 306 | " description='Sub-asset Classes:',\n", 307 | " disabled=False,\n", 308 | ")\n", 309 | "\n", 310 | "criteria_vbox = widgets.VBox([])\n", 311 | "\n", 312 | "def criteria_widgets(sub_asset_class):\n", 313 | " # OK, in here I need to look up the criteria for the\n", 314 | " # sub-asset class and build the widgets in rows of HBox es\n", 315 | " return [widgets.Label(criterion.display(prefix=\"\"))\n", 316 | " for criterion\n", 317 | " in sub_asset_class.criteria]\n", 318 | " \n", 319 | "def asset_class_changed(change):\n", 320 | " if change['type'] == 'change' and change['name'] == 'value':\n", 321 | " selected_asset_class = change['new']\n", 322 | " sub_asset_class_widget.options = sub_asset_class_dict(selected_asset_class)\n", 323 | " \n", 324 | "def sub_asset_class_changed(change):\n", 325 | " if change['type'] == 'change' and change['name'] == 'value':\n", 326 | " selected_sub_asset_class = change['new']\n", 327 | " criteria_vbox.children = criteria_widgets(selected_sub_asset_class)\n", 328 | "\n", 329 | "asset_class_widget.observe(asset_class_changed)\n", 330 | "sub_asset_class_widget.observe(sub_asset_class_changed)\n", 331 | "display(asset_class_widget)\n", 332 | "display(sub_asset_class_widget)\n", 333 | "criteria_vbox.children = criteria_widgets(sub_asset_class_widget.value)\n", 334 | "display(criteria_vbox)\n" 335 | ] 336 | }, 337 | { 338 | "cell_type": "markdown", 339 | "metadata": {}, 340 | "source": [ 341 | "### Viewing the RTS 2 Annex 3 taxonomy as a tree\n", 342 | "\n", 343 | "The following code walks the RTS 2 Annex 3 taxonomy building up a string which presents the taxonomy as a tree, in the same kind of way that nested file folders on a computer could be shown as a tree.\n", 344 | "\n", 345 | "The names are all trimmed to 50 characters just to force each item onto a single line." 346 | ] 347 | }, 348 | { 349 | "cell_type": "code", 350 | "execution_count": 7, 351 | "metadata": { 352 | "collapsed": false 353 | }, 354 | "outputs": [ 355 | { 356 | "name": "stdout", 357 | "output_type": "stream", 358 | "text": [ 359 | "\n", 360 | "Don't forget, all strings have been trimmed to 50 characters! ... \n", 361 | "\n", 362 | "Root\n", 363 | " Asset class: \"Bonds (all bond types except ETCs and ETNs)\"\n", 364 | " Sub-asset class: \"Sovereign Bond\"\n", 365 | " Sub-asset class: \"Other Public Bond\"\n", 366 | " Sub-asset class: \"Convertible Bond\"\n", 367 | " Sub-asset class: \"Covered Bond\"\n", 368 | " Sub-asset class: \"Corporate Bond\"\n", 369 | " Sub-asset class: \"Other Bond\"\n", 370 | " Asset class: \"Bonds (ETC and ETN bond types)\"\n", 371 | " Sub-asset class: \"Exchange Traded Commodities (ETCs)\"\n", 372 | " Sub-asset class: \"Exchange Traded Notes (ETNs)\"\n", 373 | " Asset class: \"Structured Finance Products (SFPs)\"\n", 374 | " Asset class: \"Securitised Derivatives\"\n", 375 | " Asset class: \"Interest Rate Derivatives\"\n", 376 | " Sub-asset class: \"Bond futures/forwards\"\n", 377 | " Critrion: 1 \"issuer of the underlying\"\n", 378 | " Critrion: 2 \"term of the underlying deliverable bond defined as\"\n", 379 | " Critrion: 3 \"time to maturity bucket of the swap defined as fol\"\n", 380 | " Sub-asset class: \"Bond options\"\n", 381 | " Critrion: 1 \"underlying bond or underlying bond future/forward\"\n", 382 | " Critrion: 2 \"time to maturity bucket of the swap defined as fol\"\n", 383 | " Sub-asset class: \"IR futures and FRA\"\n", 384 | " Critrion: 1 \"underlying interest rate\"\n", 385 | " Critrion: 2 \"term of the underlying interest rate\"\n", 386 | " Critrion: 3 \"time to maturity bucket of the swap defined as fol\"\n", 387 | " Sub-asset class: \"IR options\"\n", 388 | " Critrion: 1 \"underlying interest rate or underlying interest ra\"\n", 389 | " Critrion: 2 \"term of the underlying interest rate\"\n", 390 | " Critrion: 3 \"time to maturity bucket of the swap defined as fol\"\n", 391 | " Sub-asset class: \"Swaptions\"\n", 392 | " Critrion: 1 \"underlying swap type defined as follows: fixed-to-\"\n", 393 | " Critrion: 2 \"notional currency defined as the currency in which\"\n", 394 | " Critrion: 3 \"inflation index if the underlying swap type is eit\"\n", 395 | " Critrion: 4 \"time to maturity bucket of the swap defined as fol\"\n", 396 | " Critrion: 5 \"time to maturity bucket of the option defined as f\"\n", 397 | " Sub-asset class: \"Fixed-to-Float 'multi-currency swaps' or 'cross-cu\"\n", 398 | " Critrion: 1 \"notional currency pair defined as combination of t\"\n", 399 | " Critrion: 2 \"time to maturity bucket of the swap defined as fol\"\n", 400 | " Sub-asset class: \"Float-to-Float 'multi-currency swaps' or 'cross-cu\"\n", 401 | " Critrion: 1 \"notional currency pair defined as combination of t\"\n", 402 | " Critrion: 2 \"time to maturity bucket of the swap defined as fol\"\n", 403 | " Sub-asset class: \"Fixed-to-Fixed 'multi-currency swaps' or 'cross-cu\"\n", 404 | " Critrion: 1 \"notional currency pair defined as combination of t\"\n", 405 | " Critrion: 2 \"time to maturity bucket of the swap defined as fol\"\n", 406 | " Sub-asset class: \"Overnight Index Swap (OIS) 'multi-currency swaps' \"\n", 407 | " Critrion: 1 \"notional currency pair defined as combination of t\"\n", 408 | " Critrion: 2 \"time to maturity bucket of the swap defined as fol\"\n", 409 | " Sub-asset class: \"Inflation 'multi-currency swaps' or 'cross-currenc\"\n", 410 | " Critrion: 1 \"notional currency pair defined as combination of t\"\n", 411 | " Critrion: 2 \"time to maturity bucket of the swap defined as fol\"\n", 412 | " Sub-asset class: \"Fixed-to-Float 'single currency swaps' and futures\"\n", 413 | " Critrion: 1 \"notional currency in which the two legs of the swa\"\n", 414 | " Critrion: 2 \"time to maturity bucket of the swap defined as fol\"\n", 415 | " Sub-asset class: \"Float-to-Float 'single currency swaps' and futures\"\n", 416 | " Critrion: 1 \"notional currency in which the two legs of the swa\"\n", 417 | " Critrion: 2 \"time to maturity bucket of the swap defined as fol\"\n", 418 | " Sub-asset class: \"Fixed-to-Fixed 'single currency swaps' and futures\"\n", 419 | " Critrion: 1 \"notional currency in which the two legs of the swa\"\n", 420 | " Critrion: 2 \"time to maturity bucket of the swap defined as fol\"\n", 421 | " Sub-asset class: \"Overnight Index Swap (OIS) 'single currency swaps'\"\n", 422 | " Critrion: 1 \"notional currency in which the two legs of the swa\"\n", 423 | " Critrion: 2 \"time to maturity bucket of the swap defined as fol\"\n", 424 | " Sub-asset class: \"Inflation 'single currency swaps' and futures/forw\"\n", 425 | " Critrion: 1 \"notional currency in which the two legs of the swa\"\n", 426 | " Critrion: 2 \"time to maturity bucket of the swap defined as fol\"\n", 427 | " Sub-asset class: \"Other Interest Rate Derivatives\"\n", 428 | " Asset class: \"Equity Derivatives\"\n", 429 | " Sub-asset class: \"Stock index options\"\n", 430 | " Critrion: 1 \"underlying stock index\"\n", 431 | " Sub-asset class: \"Stock index futures/ forwards\"\n", 432 | " Critrion: 1 \"underlying stock index\"\n", 433 | " Sub-asset class: \"Stock options\"\n", 434 | " Critrion: 1 \"underlying share\"\n", 435 | " Sub-asset class: \"Stock futures/ forwards\"\n", 436 | " Critrion: 1 \"underlying share\"\n", 437 | " Sub-asset class: \"Stock dividend options\"\n", 438 | " Critrion: 1 \"underlying share entitling to dividends\"\n", 439 | " Sub-asset class: \"Stock dividend futures/ forwards\"\n", 440 | " Critrion: 1 \"underlying share entitling to dividends\"\n", 441 | " Sub-asset class: \"Dividend index options\"\n", 442 | " Critrion: 1 \"underlying dvidend index\"\n", 443 | " Sub-asset class: \"Dividend index futures/ forwards\"\n", 444 | " Critrion: 1 \"underlying dividend index\"\n", 445 | " Sub-asset class: \"Volatility index options\"\n", 446 | " Critrion: 1 \"underlying volatility index\"\n", 447 | " Sub-asset class: \"Volatility index futures/ forwards\"\n", 448 | " Critrion: 1 \"underlying volatility index\"\n", 449 | " Sub-asset class: \"ETF options\"\n", 450 | " Critrion: 1 \"underlying ETF\"\n", 451 | " Sub-asset class: \"ETF futures/ forwards\"\n", 452 | " Critrion: 1 \"underlying ETF\"\n", 453 | " Sub-asset class: \"Swaps\"\n", 454 | " Critrion: 1 \"underlying type: single name, index, basket\"\n", 455 | " Critrion: 2 \"underlying single name, index, basket\"\n", 456 | " Critrion: 3 \"parameter: price return basic performance paramete\"\n", 457 | " Critrion: 4 \"time to maturity bucket of the swap defined as fol\"\n", 458 | " Sub-asset class: \"Portfolio Swaps\"\n", 459 | " Critrion: 1 \"underlying type: single name, index, basket\"\n", 460 | " Critrion: 2 \"underlying single name, index, basket\"\n", 461 | " Critrion: 3 \"parameter: price return basic performance paramete\"\n", 462 | " Critrion: 4 \"Price return basic performance parameter\"\n", 463 | " Sub-asset class: \"Other equity derivatives\"\n", 464 | " Asset class: \"Commodity Derivatives\"\n", 465 | " Sub-asset class: \"Metal commodity futures/forwards\"\n", 466 | " Critrion: 1 \"metal type: precious metal, non-precious metal\"\n", 467 | " Critrion: 2 \"underlying metal\"\n", 468 | " Critrion: 3 \"notional currency defined as the currency in which\"\n", 469 | " Critrion: 4 \"time to maturity bucket of the future/forward defi\"\n", 470 | " Sub-asset class: \"Metal commodity options\"\n", 471 | " Critrion: 1 \"metal type: precious metal, non-precious metal\"\n", 472 | " Critrion: 2 \"underlying metal\"\n", 473 | " Critrion: 3 \"notional currency defined as the currency in which\"\n", 474 | " Critrion: 4 \"time to maturity bucket of the option defined as f\"\n", 475 | " Sub-asset class: \"Metal commodity swaps\"\n", 476 | " Critrion: 1 \"metal type: precious metal, non-precious metal\"\n", 477 | " Critrion: 2 \"underlying metal\"\n", 478 | " Critrion: 3 \"notional currency defined as the currency in which\"\n", 479 | " Critrion: 4 \"settlement type defined as cash, physical or other\"\n", 480 | " Critrion: 5 \"time to maturity bucket of the swap defined as fol\"\n", 481 | " Sub-asset class: \"Energy commodity futures/forwards\"\n", 482 | " Critrion: 1 \"energy type: oil, oil distillates, coal, oil light\"\n", 483 | " Critrion: 2 \"underlying energy\"\n", 484 | " Critrion: 3 \"notional currency defined as the currency in which\"\n", 485 | " Critrion: 4 \"load type defined as baseload, peakload, off-peak \"\n", 486 | " Critrion: 5 \"delivery/ cash settlement location applicable to e\"\n", 487 | " Critrion: 6 \"time to maturity bucket of the future/forward defi\"\n", 488 | " Sub-asset class: \"Energy commodity options\"\n", 489 | " Critrion: 1 \"energy type: oil, oil distillates, coal, oil light\"\n", 490 | " Critrion: 2 \"underlying energy\"\n", 491 | " Critrion: 3 \"notional currency defined as the currency in which\"\n", 492 | " Critrion: 4 \"load type defined as baseload, peakload, off-peak \"\n", 493 | " Critrion: 5 \"delivery/ cash settlement location applicable to e\"\n", 494 | " Critrion: 6 \"time to maturity bucket of the option defined as f\"\n", 495 | " Sub-asset class: \"Energy commodity swaps\"\n", 496 | " Critrion: 1 \"energy type: oil, oil distillates, coal, oil light\"\n", 497 | " Critrion: 2 \"underlying energy\"\n", 498 | " Critrion: 3 \"notional currency defined as the currency in which\"\n", 499 | " Critrion: 4 \"settlement type defined as cash, physical or other\"\n", 500 | " Critrion: 5 \"load type defined as baseload, peakload, off-peak \"\n", 501 | " Critrion: 6 \"delivery/ cash settlement location applicable to e\"\n", 502 | " Critrion: 7 \"time to maturity bucket of the swap defined as fol\"\n", 503 | " Sub-asset class: \"Agricultural commodity futures/forwards\"\n", 504 | " Critrion: 1 \"underlying agricultural commodity\"\n", 505 | " Critrion: 2 \"notional currency defined as the currency in which\"\n", 506 | " Critrion: 3 \"time to maturity bucket of the future/forward defi\"\n", 507 | " Sub-asset class: \"Agricultural commodity options\"\n", 508 | " Critrion: 1 \"underlying agricultural commodity\"\n", 509 | " Critrion: 2 \"notional currency defined as the currency in which\"\n", 510 | " Critrion: 3 \"time to maturity bucket of the option defined as f\"\n", 511 | " Sub-asset class: \"Agricultural commodity swaps\"\n", 512 | " Critrion: 1 \"underlying agricultural commodity\"\n", 513 | " Critrion: 2 \"notional currency defined as the currency in which\"\n", 514 | " Critrion: 3 \"settlement type defined as cash, physical or other\"\n", 515 | " Critrion: 4 \"time to maturity bucket of the swap defined as fol\"\n", 516 | " Sub-asset class: \"Other commodity derivatives\"\n", 517 | " Asset class: \"Foreign Exchange Derivatives\"\n", 518 | " Sub-asset class: \"Non-deliverable forward (NDF)\"\n", 519 | " Critrion: 1 \"underlying currency pair defined as combination of\"\n", 520 | " Critrion: 2 \"time to maturity bucket of the swap defined as fol\"\n", 521 | " Sub-asset class: \"Deliverable forward (DF)\"\n", 522 | " Critrion: 1 \"underlying currency pair defined as combination of\"\n", 523 | " Critrion: 2 \"time to maturity bucket of the swap defined as fol\"\n", 524 | " Sub-asset class: \"Non-Deliverable FX options (NDO)\"\n", 525 | " Critrion: 1 \"underlying currency pair defined as combination of\"\n", 526 | " Critrion: 2 \"time to maturity bucket of the swap defined as fol\"\n", 527 | " Sub-asset class: \"Deliverable FX options (DO)\"\n", 528 | " Critrion: 1 \"underlying currency pair defined as combination of\"\n", 529 | " Critrion: 2 \"time to maturity bucket of the swap defined as fol\"\n", 530 | " Sub-asset class: \"Non-Deliverable FX swaps (NDS)\"\n", 531 | " Critrion: 1 \"underlying currency pair defined as combination of\"\n", 532 | " Critrion: 2 \"time to maturity bucket of the swap defined as fol\"\n", 533 | " Sub-asset class: \"Deliverable FX swaps (DS)\"\n", 534 | " Critrion: 1 \"underlying currency pair defined as combination of\"\n", 535 | " Critrion: 2 \"time to maturity bucket of the swap defined as fol\"\n", 536 | " Sub-asset class: \"FX futures\"\n", 537 | " Critrion: 1 \"underlying currency pair defined as combination of\"\n", 538 | " Critrion: 2 \"time to maturity bucket of the swap defined as fol\"\n", 539 | " Sub-asset class: \"Other Foreign Exchange Derivatives\"\n", 540 | " Asset class: \"Credit Derivatives\"\n", 541 | " Sub-asset class: \"Index credit default swap (CDS)\"\n", 542 | " Critrion: 1 \"underlying index\"\n", 543 | " Critrion: 2 \"notional currency defined as the currency in which\"\n", 544 | " Critrion: 3 \"time maturity bucket of the CDS defined as follows\"\n", 545 | " Sub-asset class: \"Single name credit default swap (CDS)\"\n", 546 | " Critrion: 1 \"underlying reference entity\"\n", 547 | " Critrion: 2 \"underlying reference entity type defined as follow\"\n", 548 | " Critrion: 3 \"notional currency defined as the currency in which\"\n", 549 | " Critrion: 4 \"time maturity bucket of the CDS defined as follows\"\n", 550 | " Sub-asset class: \"Bespoke basket credit default swap (CDS)\"\n", 551 | " Sub-asset class: \"CDS index options\"\n", 552 | " Critrion: 1 \"CDS index sub-class as specified for the sub-asset\"\n", 553 | " Critrion: 2 \"time maturity bucket of the option defined as foll\"\n", 554 | " Sub-asset class: \"Single name CDS options\"\n", 555 | " Critrion: 1 \"single name CDS sub-class as specified for the sub\"\n", 556 | " Critrion: 2 \"time maturity bucket of the option defined as foll\"\n", 557 | " Sub-asset class: \"Other credit derivatives\"\n", 558 | " Asset class: \"C10 Derivatives\"\n", 559 | " Sub-asset class: \"Freight derivatives\"\n", 560 | " Critrion: 1 \"contract type: Forward Freight Agreements (FFAs) o\"\n", 561 | " Critrion: 2 \"freight type: wet freight, dry freight\"\n", 562 | " Critrion: 3 \"freight sub-type: dry bulk carriers, tanker, conta\"\n", 563 | " Critrion: 4 \"specification of the size related to the freight s\"\n", 564 | " Critrion: 5 \"specific route or time charter average\"\n", 565 | " Critrion: 6 \"time maturity bucket of the derivative defined as \"\n", 566 | " Sub-asset class: \"Other C10 derivatives\"\n", 567 | " Asset class: \"Financial contracts for differences (CFDs)\"\n", 568 | " Sub-asset class: \"Currency CFDs\"\n", 569 | " Critrion: 1 \"a currency CFD sub-class is defined by the underly\"\n", 570 | " Sub-asset class: \"Commodity CFDs\"\n", 571 | " Critrion: 1 \"a commodity CFD sub-class is defined by the underl\"\n", 572 | " Sub-asset class: \"Equity CFDs\"\n", 573 | " Critrion: 1 \"an equity CFD sub-class is defined by the underlyi\"\n", 574 | " Sub-asset class: \"Bond CFDs\"\n", 575 | " Critrion: 1 \"a bond CFD sub-class is defined by the underlying \"\n", 576 | " Sub-asset class: \"CFDs on an equity future/forward\"\n", 577 | " Critrion: 1 \"a CFD on an equity future/forward sub-class is def\"\n", 578 | " Sub-asset class: \"CFDs on an equity option\"\n", 579 | " Critrion: 1 \"a CFD on an equity option sub-class is defined by \"\n", 580 | " Sub-asset class: \"Other CFDs\"\n", 581 | " Asset class: \"Emission Allowances\"\n", 582 | " Sub-asset class: \"European Union Allowances (EUA)\"\n", 583 | " Sub-asset class: \"European Union Aviation Allowances (EUAA)\"\n", 584 | " Sub-asset class: \"Certified Emission Reductions (CER)\"\n", 585 | " Sub-asset class: \"Emission Reduction Units (ERU)\"\n", 586 | " Asset class: \"Emission Allowance Derivatives\"\n", 587 | " Sub-asset class: \"Emission allowance derivatives whose underlying is\"\n", 588 | " Sub-asset class: \"Emission allowance derivatives whose underlying is\"\n", 589 | " Sub-asset class: \"Emission allowance derivatives whose underlying is\"\n", 590 | " Sub-asset class: \"Emission allowance derivatives whose underlying is\"\n", 591 | "\n" 592 | ] 593 | } 594 | ], 595 | "source": [ 596 | "import rts2_annex3\n", 597 | "from IPython.display import display\n", 598 | "\n", 599 | "max_name_length = 50\n", 600 | "target_string = ''\n", 601 | "root = rts2_annex3.class_root\n", 602 | "target_string += 'Root\\n'\n", 603 | "for asset_class in root.asset_classes:\n", 604 | " target_string += ' Asset class: \"' + asset_class.name + '\"\\n'\n", 605 | " for sub_asset_class in asset_class.children:\n", 606 | " target_string += ' Sub-asset class: \"' \\\n", 607 | " + sub_asset_class.name[:max_name_length] \\\n", 608 | " + '\"\\n'\n", 609 | " for criterion in sub_asset_class.criteria:\n", 610 | " target_string += ' Critrion: ' \\\n", 611 | " + str(criterion.criterion_number) \\\n", 612 | " + ' \"' \\\n", 613 | " + criterion.description[:max_name_length] \\\n", 614 | " + '\"\\n'\n", 615 | "\n", 616 | "print(\"\\nDon't forget, all strings have been trimmed to {limit} characters! ... \\n\".format(\n", 617 | " limit=max_name_length))\n", 618 | "print(target_string)" 619 | ] 620 | } 621 | ], 622 | "metadata": { 623 | "kernelspec": { 624 | "display_name": "Python 3", 625 | "language": "python", 626 | "name": "python3" 627 | }, 628 | "language_info": { 629 | "codemirror_mode": { 630 | "name": "ipython", 631 | "version": 3 632 | }, 633 | "file_extension": ".py", 634 | "mimetype": "text/x-python", 635 | "name": "python", 636 | "nbconvert_exporter": "python", 637 | "pygments_lexer": "ipython3", 638 | "version": "3.5.3" 639 | }, 640 | "widgets": { 641 | "state": { 642 | "00ae0aba665b4793b55d9ac6eb940434": { 643 | "views": [ 644 | { 645 | "cell_index": 9 646 | } 647 | ] 648 | }, 649 | "875c181a79774cbe84152fbe7974ac44": { 650 | "views": [ 651 | { 652 | "cell_index": 9 653 | } 654 | ] 655 | }, 656 | "cbebf02554a840d48333bbf70bf688b4": { 657 | "views": [ 658 | { 659 | "cell_index": 9 660 | } 661 | ] 662 | } 663 | }, 664 | "version": "1.2.0" 665 | } 666 | }, 667 | "nbformat": 4, 668 | "nbformat_minor": 2 669 | } 670 | -------------------------------------------------------------------------------- /rts/Trade data generation and testing.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Generate and use test trade data\n", 8 | "\n", 9 | "Here we use the RTS 2 Annex 3 model to generate test trade data which we then classify.\n", 10 | "\n", 11 | "In these first examples we generate a couple of really simple cases. Read the comments in the code which explain what is happening at each step.\n", 12 | "\n", 13 | "Do run the steps in order. The later steps build on work done by the earlier steps ... if you skip a step something may break. If that happens, just start again from the top." 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": 1, 19 | "metadata": { 20 | "collapsed": false 21 | }, 22 | "outputs": [ 23 | { 24 | "name": "stdout", 25 | "output_type": "stream", 26 | "text": [ 27 | "\n" 28 | ] 29 | } 30 | ], 31 | "source": [ 32 | "# Import the RTS 2 Annex III taxonomy \n", 33 | "# ... and get hold of the object which represents the root of the taxonomy tree.\n", 34 | "import rts2_annex3\n", 35 | "root = rts2_annex3.class_root\n", 36 | "\n", 37 | "# Now we print the root object to see what it is.\n", 38 | "print(root)" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": 2, 44 | "metadata": { 45 | "collapsed": false 46 | }, 47 | "outputs": [ 48 | { 49 | "name": "stdout", 50 | "output_type": "stream", 51 | "text": [ 52 | "OrderedDict([('RTS2 version', 'EU 2017/583 of 14 July 2016'), ('Asset class', 'Bonds (ETC and ETN bond types)'), ('Sub-asset class', 'Exchange Traded Notes (ETNs)')])\n" 53 | ] 54 | } 55 | ], 56 | "source": [ 57 | "# Using the taxonomy to generate some test data:\n", 58 | "\n", 59 | "# Starting with a Sub-asset class which has no criteria:\n", 60 | "sub_asset_class = root.sub_asset_class_by_name(\"Exchange Traded Notes (ETNs)\")\n", 61 | "\n", 62 | "# Ask the Sub-asset class to generate a single sample trade\n", 63 | "sample_trades = sub_asset_class.make_test_samples(number=1)\n", 64 | "sample_trade = sample_trades[0]\n", 65 | "\n", 66 | "# Get the RTS 2 Annex III taxonomy to classify the trade\n", 67 | "classification = root.classification_for(subject=sample_trade)\n", 68 | "\n", 69 | "print(classification.classification_dict())" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": 3, 75 | "metadata": { 76 | "collapsed": false 77 | }, 78 | "outputs": [ 79 | { 80 | "name": "stdout", 81 | "output_type": "stream", 82 | "text": [ 83 | "{\n", 84 | " \"RTS2 version\": \"EU 2017/583 of 14 July 2016\",\n", 85 | " \"Asset class\": \"Bonds (ETC and ETN bond types)\",\n", 86 | " \"Sub-asset class\": \"Exchange Traded Notes (ETNs)\"\n", 87 | "}\n" 88 | ] 89 | } 90 | ], 91 | "source": [ 92 | "# Note that RTS 2 taxonomy is implemented in Python so the printed result above is \n", 93 | "# simply the literal form of a Python OrderedDict.\n", 94 | "\n", 95 | "# We can make the classification easier to understand using JSON.\n", 96 | "# ... but note that the content is just the same as above.\n", 97 | "\n", 98 | "import json\n", 99 | "print(classification.as_json(indent=4))\n" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": 4, 105 | "metadata": { 106 | "collapsed": false 107 | }, 108 | "outputs": [ 109 | { 110 | "name": "stdout", 111 | "output_type": "stream", 112 | "text": [ 113 | "{\n", 114 | " \"RTS2 version\": \"EU 2017/583 of 14 July 2016\",\n", 115 | " \"Asset class\": \"Bonds (ETC and ETN bond types)\",\n", 116 | " \"Sub-asset class\": \"Exchange Traded Notes (ETNs)\"\n", 117 | "}\n" 118 | ] 119 | } 120 | ], 121 | "source": [ 122 | "# Now we generate a sample trade for an Asset class.\n", 123 | "\n", 124 | "# We select an Asset class which has two child Sub-asset classes, including\n", 125 | "# the Sub-asset class used as an example above.\n", 126 | "asset_class = root.asset_class_by_name(\"Bonds (ETC and ETN bond types)\")\n", 127 | "\n", 128 | "# When we ask the asset class to make a sample it randomly chooses one of\n", 129 | "# its two children.\n", 130 | "sample = asset_class.make_test_samples(1)[0]\n", 131 | "\n", 132 | "# This new sample trade is classified.\n", 133 | "classification = root.classification_for(subject=sample)\n", 134 | "print(classification.as_json(indent=4))\n", 135 | "\n", 136 | "# Run the this cell many times and note that the Sub-asset class is selected randomly\n" 137 | ] 138 | }, 139 | { 140 | "cell_type": "markdown", 141 | "metadata": {}, 142 | "source": [ 143 | "The above example uses Sub-asset classes which have no criteria. This makes for a simpler example but is not the most common case, most Sub-asset classes have criteria.\n", 144 | "\n", 145 | "The next examples generate trades for Sub-asset classes which do have criteria. Some criteria are defined in the RTS to have a fixed nuber of options, for example segmentation criteria 1 for the \"Energy commodity swaps\" Sub-asset class is an energy type and must be one of 'ELEC', 'NGAS', 'OILP', 'COAL', 'INRG', 'RNNG', 'LGHT' and 'DIST' from RTS 23 table 2. Other criteria do not have value ranges defined in the RTS. In cases where the implementation can select a reasonable random option it does so e.g. 'NGAS', in cases where the values are not defined in RTS 2 generated trades contain values like 'settlement_type.value' which is not realistic, but is obvious.\n", 146 | "\n", 147 | "For date buckets the code always generates the from_date as today and then chooses a random duration from a small set of options. This means the date buckets are alway valid and vary a bit to add some variety.\n", 148 | "\n", 149 | "Note that it is possible to generate a sample trade and then modify the trade with more realistic values. We'll do that a bit later." 150 | ] 151 | }, 152 | { 153 | "cell_type": "code", 154 | "execution_count": 5, 155 | "metadata": { 156 | "collapsed": false 157 | }, 158 | "outputs": [ 159 | { 160 | "name": "stdout", 161 | "output_type": "stream", 162 | "text": [ 163 | "{'settlement_type': 'settlement_type.value', 'delivery': 'delivery.value', 'notional_currency': 'notional_currency.value', 'asset_class_name': 'Commodity Derivatives', 'load_type': 'load_type.value', 'to_date': datetime.date(2019, 6, 17), 'sub_asset_class_name': 'Energy commodity swaps', 'from_date': datetime.date(2018, 6, 17), 'underlying_energy': 'underlying_energy.value', 'energy_type': 'RNNG'}\n", 164 | "{\n", 165 | " \"RTS2 version\": \"EU 2017/583 of 14 July 2016\",\n", 166 | " \"Asset class\": \"Commodity Derivatives\",\n", 167 | " \"Sub-asset class\": \"Energy commodity swaps\",\n", 168 | " \"Segmentation criterion 1 description\": \"energy type: oil, oil distillates, coal, oil light ends, natural gas, electricity, inter-energy\",\n", 169 | " \"Segmentation criterion 1\": \"RNNG\",\n", 170 | " \"Segmentation criterion 2 description\": \"underlying energy\",\n", 171 | " \"Segmentation criterion 2\": \"underlying_energy.value\",\n", 172 | " \"Segmentation criterion 3 description\": \"notional currency defined as the currency in which the notional amount of the swap is denominated\",\n", 173 | " \"Segmentation criterion 3\": \"notional_currency.value\",\n", 174 | " \"Segmentation criterion 4 description\": \"settlement type defined as cash, physical or other\",\n", 175 | " \"Segmentation criterion 4\": \"settlement_type.value\",\n", 176 | " \"Segmentation criterion 5 description\": \"load type defined as baseload, peakload, off-peak or others, applicable to energy type: electricity\",\n", 177 | " \"Segmentation criterion 5\": \"load_type.value\",\n", 178 | " \"Segmentation criterion 6 description\": \"delivery/ cash settlement location applicable to energy types: oil, oil distillates, oil light ends, electricity, inter-energy\",\n", 179 | " \"Segmentation criterion 6\": \"delivery.value\",\n", 180 | " \"Segmentation criterion 7 description\": \"Natural Gas/'Electricity/Inter-energy\",\n", 181 | " \"Segmentation criterion 7\": \"Maturity bucket 2: 1 month to 1 year\"\n", 182 | "}\n" 183 | ] 184 | } 185 | ], 186 | "source": [ 187 | "# We now make a sample trade which has many criteria. First \n", 188 | "# get hold of the Sub-asset class\n", 189 | "sub_asset_class = root.sub_asset_class_by_name(\"Energy commodity swaps\")\n", 190 | "\n", 191 | "# Then generate a sample trade\n", 192 | "sample_trade = sub_asset_class.make_test_samples(1)[0]\n", 193 | "\n", 194 | "# Notice that the sample trade has some values which change randomly, e.g. to_date\n", 195 | "print(vars(sample_trade))\n", 196 | "\n", 197 | "# ... so when the trade is classified the maturity bucket criteion varies\n", 198 | "classification = root.classification_for(subject=sample_trade)\n", 199 | "print(classification.as_json(indent=4))\n" 200 | ] 201 | }, 202 | { 203 | "cell_type": "code", 204 | "execution_count": 6, 205 | "metadata": { 206 | "collapsed": false 207 | }, 208 | "outputs": [ 209 | { 210 | "name": "stdout", 211 | "output_type": "stream", 212 | "text": [ 213 | "\n", 214 | "Sample trade 0 looks like this:\n", 215 | "{'sub_asset_class_name': 'Stock index futures/ forwards', 'asset_class_name': 'Equity Derivatives', 'underlying_stock_index': 'underlying_stock_index.value'}\n", 216 | "\n", 217 | "The classification of sample trade 0 looks like this:\n", 218 | "{\n", 219 | " \"RTS2 version\": \"EU 2017/583 of 14 July 2016\",\n", 220 | " \"Asset class\": \"Equity Derivatives\",\n", 221 | " \"Sub-asset class\": \"Stock index futures/ forwards\",\n", 222 | " \"Segmentation criterion 1 description\": \"underlying stock index\",\n", 223 | " \"Segmentation criterion 1\": \"underlying_stock_index.value\"\n", 224 | "}\n", 225 | "\n", 226 | "Sample trade 1 looks like this:\n", 227 | "{'notional_currency_pair': 'notional_currency_pair.value', 'sub_asset_class_name': \"Overnight Index Swap (OIS) 'multi-currency swaps' or 'cross-currency swaps' and futures/forwards on Overnight Index Swap (OIS) 'multi-currency swaps' or 'cross-currency swaps'\", 'to_date': datetime.date(2018, 7, 17), 'asset_class_name': 'Interest Rate Derivatives', 'from_date': datetime.date(2018, 6, 17)}\n", 228 | "\n", 229 | "The classification of sample trade 1 looks like this:\n", 230 | "{\n", 231 | " \"RTS2 version\": \"EU 2017/583 of 14 July 2016\",\n", 232 | " \"Asset class\": \"Interest Rate Derivatives\",\n", 233 | " \"Sub-asset class\": \"Overnight Index Swap (OIS) 'multi-currency swaps' or 'cross-currency swaps' and futures/forwards on Overnight Index Swap (OIS) 'multi-currency swaps' or 'cross-currency swaps'\",\n", 234 | " \"Segmentation criterion 1 description\": \"notional currency pair defined as combination of the two currencies in which the two legs of the swap are denominated\",\n", 235 | " \"Segmentation criterion 1\": \"notional_currency_pair.value\",\n", 236 | " \"Segmentation criterion 2 description\": \"time to maturity bucket of the swap defined as follows:\",\n", 237 | " \"Segmentation criterion 2\": \"Maturity bucket 1: Zero to 1 month\"\n", 238 | "}\n" 239 | ] 240 | } 241 | ], 242 | "source": [ 243 | "# Now to generate and clasify lots of sample trades\n", 244 | "\n", 245 | "# Generate a sample trade for each Sub-asset class in the taxonomy\n", 246 | "samples = []\n", 247 | "for sub_asset_class in root.all_sub_asset_classes():\n", 248 | " samples.extend(sub_asset_class.make_test_samples(1))\n", 249 | "\n", 250 | "# Classify each sample\n", 251 | "classifications = [root.classification_for(subject=sample) for sample in samples]\n", 252 | "\n", 253 | "# Show a couple of random examples of the classifications\n", 254 | "import random\n", 255 | "for n in range(2):\n", 256 | " a_classification = random.choice(classifications)\n", 257 | " print(\"\\nSample trade {n} looks like this:\".format(n=n))\n", 258 | " print(vars(a_classification.subject))\n", 259 | " print(\"\\nThe classification of sample trade {n} looks like this:\".format(n=n))\n", 260 | " print(a_classification.as_json(indent=4))\n" 261 | ] 262 | } 263 | ], 264 | "metadata": { 265 | "kernelspec": { 266 | "display_name": "Python 3", 267 | "language": "python", 268 | "name": "python3" 269 | }, 270 | "language_info": { 271 | "codemirror_mode": { 272 | "name": "ipython", 273 | "version": 3 274 | }, 275 | "file_extension": ".py", 276 | "mimetype": "text/x-python", 277 | "name": "python", 278 | "nbconvert_exporter": "python", 279 | "pygments_lexer": "ipython3", 280 | "version": "3.5.3" 281 | } 282 | }, 283 | "nbformat": 4, 284 | "nbformat_minor": 2 285 | } 286 | -------------------------------------------------------------------------------- /rts/Using sample trades in an SI calculation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "collapsed": true 7 | }, 8 | "source": [ 9 | "# SI Calculation: an example using generated sample trades\n", 10 | "\n", 11 | "Here we will generate sample trades from the RTS 2 Annex III taxonomy. Each sample trade is then enriched with the information needed run an SI calculation.\n", 12 | "\n", 13 | "Once the trade data is assembled the the data normally provided by the regulator is synthesised.\n", 14 | "\n", 15 | "Lastly, the SI calculations are run\n", 16 | "\n", 17 | "The SI calculation includes a number of tests. See Article 15 (page 39) of [Brussels, 25.4.2016 C(2016) 2398 final](https://ec.europa.eu/transparency/regdoc/rep/3/2016/EN/3-2016-2398-EN-F1-1.PDF) for the three derivatives tests: a, b and c.\n" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "# Generate some trade data\n", 25 | "\n", 26 | "The first step is to use the RTS 2 Annex III taxonomy to generate some sample trades." 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": 1, 32 | "metadata": { 33 | "collapsed": false 34 | }, 35 | "outputs": [], 36 | "source": [ 37 | "# First we need to import the libraries we'll be needing\n", 38 | "import rts2_annex3\n", 39 | "import pandas as pd\n", 40 | "\n", 41 | "import random\n", 42 | "random.seed()" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 2, 48 | "metadata": { 49 | "collapsed": false 50 | }, 51 | "outputs": [], 52 | "source": [ 53 | "# Get the root of the RTS 2 Annex III taxonomy\n", 54 | "root = rts2_annex3.class_root\n", 55 | "\n", 56 | "# Get the Asset Class we would like to generate trades for\n", 57 | "asset_class = root.asset_class_by_name(\"Credit Derivatives\")\n", 58 | "\n", 59 | "# Ask the Asset class to generate some sample trade\n", 60 | "sample_trades = asset_class.make_test_samples(number=500)" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": 3, 66 | "metadata": { 67 | "collapsed": false 68 | }, 69 | "outputs": [ 70 | { 71 | "name": "stdout", 72 | "output_type": "stream", 73 | "text": [ 74 | "Generated 500 trades. here is one example:\n", 75 | "\n", 76 | "{'cds_index_sub_class': 'cds_index_sub_class.value', 'to_date': datetime.date(2021, 3, 13), 'asset_class_name': 'Credit Derivatives', 'sub_asset_class_name': 'CDS index options', 'from_date': datetime.date(2018, 6, 17)}\n" 77 | ] 78 | } 79 | ], 80 | "source": [ 81 | "print(\"Generated {count} trades. here is one example:\\n\".format(count=len(sample_trades)))\n", 82 | "print(vars(random.choice(sample_trades)))" 83 | ] 84 | }, 85 | { 86 | "cell_type": "markdown", 87 | "metadata": {}, 88 | "source": [ 89 | "## LEIs\n", 90 | "\n", 91 | "In a real firm with real trades we would need to know the [LEI (Legal Entity Identifier - ISO 17442)](https://en.wikipedia.org/wiki/Legal_Entity_Identifier) of the legal entity which did each trade because SI status is reported distinctly for each legal entity, identified by an LEI.\n", 92 | "\n", 93 | "Firms may do trades within a single legal entity, perhaps to move risk from one trading desk to another. These are called intra-entity trades and must be filtered out before the SI calculation. For this example we'll say that all the trades we generated are inter-entity trades (i.e. trades *between* distinct legal entities), so we count them all.\n", 94 | "\n", 95 | "In this example we'll use just one LEI, and not even a valid one, but it will suffice for the example." 96 | ] 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": 4, 101 | "metadata": { 102 | "collapsed": false 103 | }, 104 | "outputs": [ 105 | { 106 | "name": "stdout", 107 | "output_type": "stream", 108 | "text": [ 109 | "Our_bank_LEI\n" 110 | ] 111 | } 112 | ], 113 | "source": [ 114 | "# Typically trades invole two parties, the bank and a counterparty (the client).\n", 115 | "# For the SI calculation we just need the bank LEI.\n", 116 | "\n", 117 | "for sample_trade in sample_trades:\n", 118 | " sample_trade.lei = 'Our_bank_LEI'\n", 119 | "\n", 120 | "# Print the LEI from one of the trades (they're all the same!)\n", 121 | "\n", 122 | "print(random.choice(sample_trades).lei)" 123 | ] 124 | }, 125 | { 126 | "cell_type": "markdown", 127 | "metadata": {}, 128 | "source": [ 129 | "## Trade Date\n", 130 | "The SI calculation includes checks for frequency, the number of trades done in a single week. To work that out we need a trade date for each trade. Here we'll just use a few dates and add these to our sample trades." 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": 5, 136 | "metadata": { 137 | "collapsed": false 138 | }, 139 | "outputs": [ 140 | { 141 | "name": "stdout", 142 | "output_type": "stream", 143 | "text": [ 144 | "2018-05-28\n", 145 | "22\n" 146 | ] 147 | } 148 | ], 149 | "source": [ 150 | "# We give each sample trade a trade date in a 30 day range of dates\n", 151 | "# and an ISO week number (c.f. https://en.wikipedia.org/wiki/ISO_week_date)\n", 152 | "\n", 153 | "import datetime\n", 154 | "\n", 155 | "sample_dates = []\n", 156 | "today = datetime.date.today()\n", 157 | "for day_number in range(-30, 0):\n", 158 | " a_date = today + datetime.timedelta(day_number)\n", 159 | " if a_date.weekday() < 6:\n", 160 | " sample_dates.append(a_date)\n", 161 | "\n", 162 | "for sample_trade in sample_trades:\n", 163 | " selected_date = random.choice(sample_dates)\n", 164 | " sample_trade.trade_date = selected_date\n", 165 | " sample_trade.trade_date_week = selected_date.isocalendar()[1]\n", 166 | " \n", 167 | "# Print the one of the modified sample trades\n", 168 | "\n", 169 | "a_trade = random.choice(sample_trades)\n", 170 | "print(a_trade.trade_date)\n", 171 | "print(a_trade.trade_date_week)" 172 | ] 173 | }, 174 | { 175 | "cell_type": "markdown", 176 | "metadata": {}, 177 | "source": [ 178 | "## MIC\n", 179 | "A [MIC (Market Identifier Code - ISO 10383)](https://en.wikipedia.org/wiki/Market_Identifier_Code) is an ID for a trading venue, for example a stock exchange. The regulator is expected to provide a list of MIC values which identify venues which are recognised for the purposes of the SI calculation. Trades which are done on vs. off recognised venues are counted differently." 180 | ] 181 | }, 182 | { 183 | "cell_type": "code", 184 | "execution_count": 6, 185 | "metadata": { 186 | "collapsed": false 187 | }, 188 | "outputs": [ 189 | { 190 | "name": "stdout", 191 | "output_type": "stream", 192 | "text": [ 193 | "EEA1\n" 194 | ] 195 | } 196 | ], 197 | "source": [ 198 | "# We define our MICs. A MIC value is always 4 charcters in length. The values used\n", 199 | "# here are made-up nonsense, but good enough for an illustration\n", 200 | "\n", 201 | "eea_mics = ['EEA1', 'EEA2', 'EEA3']\n", 202 | "non_eea_mics = ['OFF1', 'OFF2', 'OFF3', 'OFF4']\n", 203 | "all_mics = eea_mics + non_eea_mics\n", 204 | "\n", 205 | "# Add a MIC to each sample trade\n", 206 | "for sample_trade in sample_trades:\n", 207 | " sample_trade.mic = random.choice(all_mics)\n", 208 | "\n", 209 | "# Print the one of the modified sample trades\n", 210 | "a_trade = random.choice(sample_trades)\n", 211 | "print(a_trade.mic)" 212 | ] 213 | }, 214 | { 215 | "cell_type": "markdown", 216 | "metadata": {}, 217 | "source": [ 218 | "## Own Account\n", 219 | "\n", 220 | "We need to know if a trade was done on the firms own account. Such trades are counted differently. " 221 | ] 222 | }, 223 | { 224 | "cell_type": "code", 225 | "execution_count": 7, 226 | "metadata": { 227 | "collapsed": false 228 | }, 229 | "outputs": [ 230 | { 231 | "name": "stdout", 232 | "output_type": "stream", 233 | "text": [ 234 | "False\n" 235 | ] 236 | } 237 | ], 238 | "source": [ 239 | "# Own Account is a boolean; either this is a trade which the regulator views as being\n", 240 | "# on the bank's own account, or not. I use a random boolean with a probability.\n", 241 | "\n", 242 | "own_account_probability = 0.25\n", 243 | "\n", 244 | "for sample_trade in sample_trades:\n", 245 | " sample_trade.own_account = random.random() < own_account_probability\n", 246 | " \n", 247 | "# Print the one of the modified sample trades\n", 248 | "a_trade = random.choice(sample_trades)\n", 249 | "print(a_trade.own_account)" 250 | ] 251 | }, 252 | { 253 | "cell_type": "markdown", 254 | "metadata": {}, 255 | "source": [ 256 | "## Client Order\n", 257 | "\n", 258 | "We need to know if a trade was done in response to a client order. Such trades are counted differently. " 259 | ] 260 | }, 261 | { 262 | "cell_type": "code", 263 | "execution_count": 8, 264 | "metadata": { 265 | "collapsed": false 266 | }, 267 | "outputs": [ 268 | { 269 | "name": "stdout", 270 | "output_type": "stream", 271 | "text": [ 272 | "False\n" 273 | ] 274 | } 275 | ], 276 | "source": [ 277 | "# Client Order is also simply a boolean. Either this is a trade which was done\n", 278 | "# in response to a client order, or not. I use a random boolean.\n", 279 | "\n", 280 | "client_order_probability = 0.5\n", 281 | "\n", 282 | "for sample_trade in sample_trades:\n", 283 | " sample_trade.client_order = random.random() < client_order_probability\n", 284 | " \n", 285 | "# Print the one of the modified sample trades\n", 286 | "a_trade = random.choice(sample_trades)\n", 287 | "print(a_trade.client_order)" 288 | ] 289 | }, 290 | { 291 | "cell_type": "markdown", 292 | "metadata": {}, 293 | "source": [ 294 | "## EUR Notional\n", 295 | "Another measure used by the SI calculation is the EUR notional value of each trade. Here we assign a notional value to each trade." 296 | ] 297 | }, 298 | { 299 | "cell_type": "code", 300 | "execution_count": 9, 301 | "metadata": { 302 | "collapsed": false 303 | }, 304 | "outputs": [ 305 | { 306 | "name": "stdout", 307 | "output_type": "stream", 308 | "text": [ 309 | "25000000\n" 310 | ] 311 | } 312 | ], 313 | "source": [ 314 | "# Add a random-ish Euro Notional amount of n million EUR to each trade\n", 315 | "\n", 316 | "notional_amounts = [x * 1000000 for x in [1, 1, 1, 2, 2, 5, 10, 25]]\n", 317 | "\n", 318 | "for sample_trade in sample_trades:\n", 319 | " sample_trade.eur_notional = random.choice(notional_amounts)\n", 320 | "\n", 321 | "# Print the one of the modified sample trades\n", 322 | "a_trade = random.choice(sample_trades)\n", 323 | "print(a_trade.eur_notional)" 324 | ] 325 | }, 326 | { 327 | "cell_type": "markdown", 328 | "metadata": {}, 329 | "source": [ 330 | "## RTS 2 Annex III Classification\n", 331 | "The last step before we start the SI calculation is to add the RTS 2 Annex III classification to each trade." 332 | ] 333 | }, 334 | { 335 | "cell_type": "code", 336 | "execution_count": 10, 337 | "metadata": { 338 | "collapsed": false 339 | }, 340 | "outputs": [ 341 | { 342 | "name": "stdout", 343 | "output_type": "stream", 344 | "text": [ 345 | "{\n", 346 | " \"RTS2 version\": \"EU 2017/583 of 14 July 2016\",\n", 347 | " \"Asset class\": \"Credit Derivatives\",\n", 348 | " \"Sub-asset class\": \"Single name credit default swap (CDS)\",\n", 349 | " \"Segmentation criterion 1 description\": \"underlying reference entity\",\n", 350 | " \"Segmentation criterion 1\": \"underlying_ref_entity.value\",\n", 351 | " \"Segmentation criterion 2 description\": \"underlying reference entity type defined as follows: \\\"Issuer of sovereign and public type\\\" means an issuer entity which is either: (a) the Union; (b) a Member State including a government department, an agency or a special purpose vehicle of a Member State; (c) a sovereign entity which is not listed under points (a) and (b); (d) in the case of a federal Member State, a member of that federation; (e) a special purpose vehicle for several Member States; (f) an international financial institution established by two or more Member States which have the purpose of mobilising funding and providing financial assistance to the benefit of its members that are experiencing or are threatened by severe financial problems; (g) the European Investment Bank; (h) a public entity which is not a sovereign issuer as specified in the points (a) to (c). \\\"Issuer of corporate type\\\" means an issuer entity which is not an issuer of sovereign and public type.\",\n", 352 | " \"Segmentation criterion 2\": \"ref_entity_type.value\",\n", 353 | " \"Segmentation criterion 3 description\": \"notional currency defined as the currency in which the notional amount of the derivative is denominated\",\n", 354 | " \"Segmentation criterion 3\": \"notional_currency.value\",\n", 355 | " \"Segmentation criterion 4 description\": \"time maturity bucket of the CDS defined as follows:\",\n", 356 | " \"Segmentation criterion 4\": \"Maturity bucket 1: Zero to 1 year\"\n", 357 | "}\n" 358 | ] 359 | } 360 | ], 361 | "source": [ 362 | "# Now classify each trade and add the JSON classification back to the trade\n", 363 | "for sample_trade in sample_trades:\n", 364 | " classification = root.classification_for(subject=sample_trade)\n", 365 | " sample_trade.rts2_classification = classification\n", 366 | "\n", 367 | "# Print the one of the modified sample trades\n", 368 | "a_trade = random.choice(sample_trades)\n", 369 | "print(a_trade.rts2_classification.as_json(indent=4))" 370 | ] 371 | }, 372 | { 373 | "cell_type": "markdown", 374 | "metadata": {}, 375 | "source": [ 376 | "# Do the SI calculation\n", 377 | "\n", 378 | "The \"calculation\" is really a set of filters 3 filters (a, b & c as shown below), which might identify an firm as being an SI for a particular RTS 2 subclass.\n", 379 | "\n", 380 | "The filters all focus on the count and notional sums of trades which are OTC (not traded on EEA recognised venue), own account (traded using the banks money) and in response to a client order. Here we'll call this subset of trades **SI-trades**.\n", 381 | "\n", 382 | "Tests a & b also ask if a particulat RTS 2 sub class is liquid. Whether an instrument is liquid or not is determined by the regulator, and the regulator must publish this, together with the total trade count and total notional, for each sub class.\n", 383 | "\n", 384 | "a. If the RTS 2 Annex III sub class is liquid\n", 385 | " - and the count of SI-trades >= 2.5% of eu_rts2_trade_count\n", 386 | " - and average weekly number of SI-trades >= 1\n", 387 | " \n", 388 | "b. If the RTS 2 Annex III sub class is not liquid \n", 389 | " - and average weekly number of SI-trades >= 1\n", 390 | "\n", 391 | "c. If the sum of EUR notional for SI-trades is\n", 392 | " - \\>= 25% of all trades notional for the LEI\n", 393 | " - **or** >= 1% of EU trade notional" 394 | ] 395 | }, 396 | { 397 | "cell_type": "markdown", 398 | "metadata": {}, 399 | "source": [ 400 | "# An Object Oriented Calculator\n", 401 | "\n", 402 | "Here we build a tiny Python application to do the SI calculation on the generated trades.\n", 403 | "\n", 404 | "The result of the calculation is a JSON report of the RTS 2 Annex III sub classes for which our LEI is a Systematic Internaliser.\n", 405 | "\n", 406 | "We define 3 classes:\n", 407 | "* SIReport - This represents the report spanning all RTS 2 classes for one LEI\n", 408 | "* RTS2SubClass - This represents the information in a report for just one RTS 2 sub class\n", 409 | "* Aggregations - Represents the various counts and sums for one RTS 2 sub class\n", 410 | "\n", 411 | "If you want to see the report, scroll past the code to the next text box." 412 | ] 413 | }, 414 | { 415 | "cell_type": "code", 416 | "execution_count": 11, 417 | "metadata": { 418 | "collapsed": false 419 | }, 420 | "outputs": [], 421 | "source": [ 422 | "import collections\n", 423 | "import json\n", 424 | "\n", 425 | "class SIReport(object):\n", 426 | " \n", 427 | " @classmethod\n", 428 | " def for_trades(cls, trades):\n", 429 | " new_report = cls()\n", 430 | " new_report.add_trades(trades)\n", 431 | " return new_report\n", 432 | " \n", 433 | " def __init__(self):\n", 434 | " self.trades = []\n", 435 | " self.sub_classes = dict()\n", 436 | " self._number_of_weeks = None\n", 437 | " \n", 438 | " def add_trades(self, trades):\n", 439 | " self.trades.extend(trades)\n", 440 | " for trade in trades:\n", 441 | " rts2_string = trade.rts2_classification.as_json()\n", 442 | " if not rts2_string in self.sub_classes:\n", 443 | " self.sub_classes[rts2_string] = RTS2SubClass(self, trade)\n", 444 | " sub_class = self.sub_classes[rts2_string]\n", 445 | " sub_class.add_trade(trade)\n", 446 | " self._number_of_weeks = None\n", 447 | "\n", 448 | " @property\n", 449 | " def number_of_weeks(self):\n", 450 | " if self._number_of_weeks is None:\n", 451 | " min_week = min(self.trades, key=lambda t: t.trade_date_week).trade_date_week\n", 452 | " max_week = max(self.trades, key=lambda t: t.trade_date_week).trade_date_week\n", 453 | " self._number_of_weeks = max_week - min_week + 1\n", 454 | " return self._number_of_weeks\n", 455 | "\n", 456 | " def si_sub_classes(self):\n", 457 | " return [sub_class for sub_class in self.sub_classes.values() \n", 458 | " if sub_class.si_status()]\n", 459 | " \n", 460 | " def report(self):\n", 461 | " si_sub_classes = self.si_sub_classes()\n", 462 | " report_items = [\n", 463 | " 'This LEI is an SI for {si_count} of {all_count} '\n", 464 | " 'sub classes traded over {weeks} weeks.'.format(\n", 465 | " si_count=len(si_sub_classes),\n", 466 | " all_count=len(self.sub_classes),\n", 467 | " weeks=self.number_of_weeks)]\n", 468 | " for sub_class in si_sub_classes:\n", 469 | " report_items.append(sub_class.report())\n", 470 | " return json.dumps(report_items, indent=4)\n", 471 | "\n", 472 | "\n", 473 | "class RTS2SubClass(object):\n", 474 | " def __init__(self, si_report, sample_trade):\n", 475 | " self.si_report = si_report\n", 476 | " self.sample_trade = sample_trade\n", 477 | " self.is_liquid = random.random() < 0.5\n", 478 | " self.trades = []\n", 479 | " self._aggregations = None\n", 480 | "\n", 481 | " @property\n", 482 | " def aggregations(self):\n", 483 | " # I keep all the computed results in one object so I can drop the cache\n", 484 | " # if a trade is added\n", 485 | " if self._aggregations is None:\n", 486 | " self._aggregations = Aggregations(sub_class=self)\n", 487 | " return self._aggregations\n", 488 | " \n", 489 | " def add_trade(self, trade):\n", 490 | " self.trades.append(trade)\n", 491 | " self._aggregations = None\n", 492 | "\n", 493 | " def si_status(self):\n", 494 | " \"\"\"\n", 495 | " This is the SI Calculation. It is applied distinctly to each sub class.\n", 496 | " It's quite simple once everything is aggregated.\n", 497 | " Note that these are the rules for derivatives trades only.\n", 498 | " \"\"\"\n", 499 | " agg = self.aggregations\n", 500 | " if self.is_liquid \\\n", 501 | " and agg.trade_count >= (0.025 * agg.eu_trade_count) \\\n", 502 | " and agg.avg_weekly_trades >= 1:\n", 503 | " return \"SI - (a) Liquid instrument test\"\n", 504 | " if not self.is_liquid \\\n", 505 | " and agg.avg_weekly_trades >= 1:\n", 506 | " return \"SI - (b) Non-liquid instrument test\"\n", 507 | " if agg.notional_sum >= (0.25 * agg.lei_notional_sum) \\\n", 508 | " or agg.notional_sum >= (0.01 * agg.eu_notional_sum):\n", 509 | " return \"SI - (c) Notional size test\"\n", 510 | " return None\n", 511 | " \n", 512 | " def report(self):\n", 513 | " report_list = ['Status: {status}.'.format(status=self.si_status())]\n", 514 | " agg_dict = vars(self.aggregations).copy()\n", 515 | " del agg_dict['sub_class']\n", 516 | " del agg_dict['si_trades']\n", 517 | " report_list.append(agg_dict)\n", 518 | " report_list.append(self.sample_trade.rts2_classification.classification_dict())\n", 519 | " return report_list\n", 520 | "\n", 521 | "\n", 522 | "class Aggregations(object):\n", 523 | " \"\"\"\n", 524 | " Each instance of Aggregations represents the subset of the trades for\n", 525 | " a sub class which are OTC client orders on the own account of the LEI.\n", 526 | " The SI calculation tests are with respect to this subset of the trades.\n", 527 | " \"\"\"\n", 528 | "\n", 529 | " def __init__(self, sub_class):\n", 530 | " self.sub_class = sub_class\n", 531 | " \n", 532 | " # Build the aggregations for this sub class\n", 533 | " self.si_trades = [\n", 534 | " trade for trade in self.sub_class.trades\n", 535 | " if (not trade.mic in eea_mics) # OTC\n", 536 | " and trade.own_account # Traded on own account\n", 537 | " and trade.client_order] # in response to a client order\n", 538 | " self.trade_count = len(self.si_trades)\n", 539 | " self.notional_sum = sum([abs(trade.eur_notional) for trade in self.si_trades])\n", 540 | " self.avg_weekly_trades = self.trade_count / self.sub_class.si_report.number_of_weeks\n", 541 | " \n", 542 | " # Now I synthesise the EU figures which should really come from the regulator\n", 543 | " self.eu_trade_count = self.trade_count * 40 + random.choice([\n", 544 | " self.trade_count * -1, self.trade_count])\n", 545 | " if self.notional_sum:\n", 546 | " self.eu_notional_sum = self.notional_sum * 100 + random.choice([\n", 547 | " self.notional_sum * -1, self.notional_sum])\n", 548 | " else:\n", 549 | " self.eu_notional_sum = 1\n", 550 | "\n", 551 | " # I keep this sub class sum here so it gets flushed if trades added\n", 552 | " self.lei_notional_sum = sum(\n", 553 | " [abs(trade.eur_notional) for trade in self.sub_class.trades])" 554 | ] 555 | }, 556 | { 557 | "cell_type": "markdown", 558 | "metadata": {}, 559 | "source": [ 560 | "## Run the report\n", 561 | "Having defined the classes we should be able to just run the report.\n", 562 | "\n", 563 | "Note that the result is different every time the report is run because the EU totals are re-synthesised each time and are random. With real data the report would be stable, and indeed this test could be changed to always produce the same results; the current implementaion is intended to show some variety." 564 | ] 565 | }, 566 | { 567 | "cell_type": "code", 568 | "execution_count": 12, 569 | "metadata": { 570 | "collapsed": false 571 | }, 572 | "outputs": [ 573 | { 574 | "name": "stdout", 575 | "output_type": "stream", 576 | "text": [ 577 | "[\n", 578 | " \"This LEI is an SI for 6 of 12 sub classes traded over 5 weeks.\",\n", 579 | " [\n", 580 | " \"Status: SI - (b) Non-liquid instrument test.\",\n", 581 | " {\n", 582 | " \"eu_notional_sum\": 4554000000,\n", 583 | " \"notional_sum\": 46000000,\n", 584 | " \"avg_weekly_trades\": 1.0,\n", 585 | " \"trade_count\": 5,\n", 586 | " \"lei_notional_sum\": 304000000,\n", 587 | " \"eu_trade_count\": 205\n", 588 | " },\n", 589 | " {\n", 590 | " \"RTS2 version\": \"EU 2017/583 of 14 July 2016\",\n", 591 | " \"Asset class\": \"Credit Derivatives\",\n", 592 | " \"Sub-asset class\": \"Single name CDS options\",\n", 593 | " \"Segmentation criterion 1 description\": \"single name CDS sub-class as specified for the sub-asset class of single name CDS\",\n", 594 | " \"Segmentation criterion 1\": \"cds_sub_class.value\",\n", 595 | " \"Segmentation criterion 2 description\": \"time maturity bucket of the option defined as follows:\",\n", 596 | " \"Segmentation criterion 2\": \"Maturity bucket 1: Zero to 6 months\"\n", 597 | " }\n", 598 | " ],\n", 599 | " [\n", 600 | " \"Status: SI - (c) Notional size test.\",\n", 601 | " {\n", 602 | " \"eu_notional_sum\": 693000000,\n", 603 | " \"notional_sum\": 7000000,\n", 604 | " \"avg_weekly_trades\": 0.6,\n", 605 | " \"trade_count\": 3,\n", 606 | " \"lei_notional_sum\": 184000000,\n", 607 | " \"eu_trade_count\": 123\n", 608 | " },\n", 609 | " {\n", 610 | " \"RTS2 version\": \"EU 2017/583 of 14 July 2016\",\n", 611 | " \"Asset class\": \"Credit Derivatives\",\n", 612 | " \"Sub-asset class\": \"CDS index options\",\n", 613 | " \"Segmentation criterion 1 description\": \"CDS index sub-class as specified for the sub-asset class of index credit default swap (CDS )\",\n", 614 | " \"Segmentation criterion 1\": \"cds_index_sub_class.value\",\n", 615 | " \"Segmentation criterion 2 description\": \"time maturity bucket of the option defined as follows:\",\n", 616 | " \"Segmentation criterion 2\": \"Maturity bucket 1: Zero to 6 months\"\n", 617 | " }\n", 618 | " ],\n", 619 | " [\n", 620 | " \"Status: SI - (a) Liquid instrument test.\",\n", 621 | " {\n", 622 | " \"eu_notional_sum\": 4752000000,\n", 623 | " \"notional_sum\": 48000000,\n", 624 | " \"avg_weekly_trades\": 1.0,\n", 625 | " \"trade_count\": 5,\n", 626 | " \"lei_notional_sum\": 448000000,\n", 627 | " \"eu_trade_count\": 195\n", 628 | " },\n", 629 | " {\n", 630 | " \"RTS2 version\": \"EU 2017/583 of 14 July 2016\",\n", 631 | " \"Asset class\": \"Credit Derivatives\",\n", 632 | " \"Sub-asset class\": \"Single name credit default swap (CDS)\",\n", 633 | " \"Segmentation criterion 1 description\": \"underlying reference entity\",\n", 634 | " \"Segmentation criterion 1\": \"underlying_ref_entity.value\",\n", 635 | " \"Segmentation criterion 2 description\": \"underlying reference entity type defined as follows: \\\"Issuer of sovereign and public type\\\" means an issuer entity which is either: (a) the Union; (b) a Member State including a government department, an agency or a special purpose vehicle of a Member State; (c) a sovereign entity which is not listed under points (a) and (b); (d) in the case of a federal Member State, a member of that federation; (e) a special purpose vehicle for several Member States; (f) an international financial institution established by two or more Member States which have the purpose of mobilising funding and providing financial assistance to the benefit of its members that are experiencing or are threatened by severe financial problems; (g) the European Investment Bank; (h) a public entity which is not a sovereign issuer as specified in the points (a) to (c). \\\"Issuer of corporate type\\\" means an issuer entity which is not an issuer of sovereign and public type.\",\n", 636 | " \"Segmentation criterion 2\": \"ref_entity_type.value\",\n", 637 | " \"Segmentation criterion 3 description\": \"notional currency defined as the currency in which the notional amount of the derivative is denominated\",\n", 638 | " \"Segmentation criterion 3\": \"notional_currency.value\",\n", 639 | " \"Segmentation criterion 4 description\": \"time maturity bucket of the CDS defined as follows:\",\n", 640 | " \"Segmentation criterion 4\": \"Maturity bucket 1: Zero to 1 year\"\n", 641 | " }\n", 642 | " ],\n", 643 | " [\n", 644 | " \"Status: SI - (b) Non-liquid instrument test.\",\n", 645 | " {\n", 646 | " \"eu_notional_sum\": 3232000000,\n", 647 | " \"notional_sum\": 32000000,\n", 648 | " \"avg_weekly_trades\": 1.2,\n", 649 | " \"trade_count\": 6,\n", 650 | " \"lei_notional_sum\": 444000000,\n", 651 | " \"eu_trade_count\": 246\n", 652 | " },\n", 653 | " {\n", 654 | " \"RTS2 version\": \"EU 2017/583 of 14 July 2016\",\n", 655 | " \"Asset class\": \"Credit Derivatives\",\n", 656 | " \"Sub-asset class\": \"Index credit default swap (CDS)\",\n", 657 | " \"Segmentation criterion 1 description\": \"underlying index\",\n", 658 | " \"Segmentation criterion 1\": \"underlying_index.value\",\n", 659 | " \"Segmentation criterion 2 description\": \"notional currency defined as the currency in which the notional amount of the derivative is denominated\",\n", 660 | " \"Segmentation criterion 2\": \"notional_currency.value\",\n", 661 | " \"Segmentation criterion 3 description\": \"time maturity bucket of the CDS defined as follows:\",\n", 662 | " \"Segmentation criterion 3\": \"Maturity bucket 1: Zero to 1 year\"\n", 663 | " }\n", 664 | " ],\n", 665 | " [\n", 666 | " \"Status: SI - (a) Liquid instrument test.\",\n", 667 | " {\n", 668 | " \"eu_notional_sum\": 2323000000,\n", 669 | " \"notional_sum\": 23000000,\n", 670 | " \"avg_weekly_trades\": 1.6,\n", 671 | " \"trade_count\": 8,\n", 672 | " \"lei_notional_sum\": 400000000,\n", 673 | " \"eu_trade_count\": 312\n", 674 | " },\n", 675 | " {\n", 676 | " \"RTS2 version\": \"EU 2017/583 of 14 July 2016\",\n", 677 | " \"Asset class\": \"Credit Derivatives\",\n", 678 | " \"Sub-asset class\": \"Other credit derivatives\"\n", 679 | " }\n", 680 | " ],\n", 681 | " [\n", 682 | " \"Status: SI - (c) Notional size test.\",\n", 683 | " {\n", 684 | " \"eu_notional_sum\": 5050000000,\n", 685 | " \"notional_sum\": 50000000,\n", 686 | " \"avg_weekly_trades\": 0.4,\n", 687 | " \"trade_count\": 2,\n", 688 | " \"lei_notional_sum\": 121000000,\n", 689 | " \"eu_trade_count\": 78\n", 690 | " },\n", 691 | " {\n", 692 | " \"RTS2 version\": \"EU 2017/583 of 14 July 2016\",\n", 693 | " \"Asset class\": \"Credit Derivatives\",\n", 694 | " \"Sub-asset class\": \"CDS index options\",\n", 695 | " \"Segmentation criterion 1 description\": \"CDS index sub-class as specified for the sub-asset class of index credit default swap (CDS )\",\n", 696 | " \"Segmentation criterion 1\": \"cds_index_sub_class.value\",\n", 697 | " \"Segmentation criterion 2 description\": \"time maturity bucket of the option defined as follows:\",\n", 698 | " \"Segmentation criterion 2\": \"Maturity bucket 2: 6 months to 1 year\"\n", 699 | " }\n", 700 | " ]\n", 701 | "]\n" 702 | ] 703 | } 704 | ], 705 | "source": [ 706 | "# First create an instance of our report\n", 707 | "\n", 708 | "report = SIReport.for_trades(sample_trades)\n", 709 | "\n", 710 | "# Then get the JSON report and print it\n", 711 | "\n", 712 | "print(report.report())\n" 713 | ] 714 | }, 715 | { 716 | "cell_type": "markdown", 717 | "metadata": {}, 718 | "source": [ 719 | "# A Declarative Calculator\n", 720 | "\n", 721 | "Here we use Pandas to run the calculation in a more declarative, relational kind of way.\n", 722 | "\n", 723 | "First we pick up the EU data from the OO model so the results here will be the same as the results in the OO report above." 724 | ] 725 | }, 726 | { 727 | "cell_type": "code", 728 | "execution_count": 13, 729 | "metadata": { 730 | "collapsed": false 731 | }, 732 | "outputs": [], 733 | "source": [ 734 | "def eu_data_for_sub_class(sub_class):\n", 735 | " return dict(\n", 736 | " rts2_classification=sub_class.sample_trade.rts2_classification.as_json(),\n", 737 | " is_liquid=sub_class.is_liquid,\n", 738 | " eu_trade_count=sub_class.aggregations.eu_trade_count,\n", 739 | " eu_notional_sum=sub_class.aggregations.eu_notional_sum,\n", 740 | " )\n", 741 | "\n", 742 | "# The set of all trades (by LEI if there is more than one)\n", 743 | "sub_classes = pd.DataFrame\\\n", 744 | " .from_records([eu_data_for_sub_class(s) for s in list(report.sub_classes.values())])\\\n", 745 | " .set_index('rts2_classification')" 746 | ] 747 | }, 748 | { 749 | "cell_type": "code", 750 | "execution_count": 14, 751 | "metadata": { 752 | "collapsed": false 753 | }, 754 | "outputs": [], 755 | "source": [ 756 | "# Put the essential information for each trade into a Pandas table.\n", 757 | "\n", 758 | "def si_details_from_sample(sample_trade):\n", 759 | " return dict(\n", 760 | " lei=sample_trade.lei,\n", 761 | " trade_date=sample_trade.trade_date,\n", 762 | " trade_date_week=sample_trade.trade_date_week,\n", 763 | " mic=sample_trade.mic,\n", 764 | " own_account=sample_trade.own_account,\n", 765 | " client_order=sample_trade.client_order,\n", 766 | " eur_notional=sample_trade.eur_notional,\n", 767 | " rts2_classification=sample_trade.rts2_classification.as_json(),\n", 768 | " )\n", 769 | "\n", 770 | "all_trades = pd.DataFrame.from_records([si_details_from_sample(s) for s in sample_trades])" 771 | ] 772 | }, 773 | { 774 | "cell_type": "code", 775 | "execution_count": 15, 776 | "metadata": { 777 | "collapsed": false 778 | }, 779 | "outputs": [], 780 | "source": [ 781 | "# Get the sum of all trades by RTS 2 sub class and add it as a column to the \n", 782 | "# This should exactly match the figure in the OO report.\n", 783 | "\n", 784 | "lei_notional_sum_series = \\\n", 785 | " all_trades[['rts2_classification', 'eur_notional']]\\\n", 786 | " .groupby(by='rts2_classification')\\\n", 787 | " .sum()\n", 788 | "\n", 789 | "sub_classes['lei_notional_sum'] = lei_notional_sum_series" 790 | ] 791 | }, 792 | { 793 | "cell_type": "code", 794 | "execution_count": 16, 795 | "metadata": { 796 | "collapsed": false 797 | }, 798 | "outputs": [], 799 | "source": [ 800 | "# Get the trades which are OTC own account client trades\n", 801 | "si_trades = all_trades[\n", 802 | " ~all_trades.mic.isin(eea_mics)\n", 803 | " & all_trades.own_account\n", 804 | " & all_trades.client_order]" 805 | ] 806 | }, 807 | { 808 | "cell_type": "code", 809 | "execution_count": 17, 810 | "metadata": { 811 | "collapsed": false 812 | }, 813 | "outputs": [], 814 | "source": [ 815 | "# For the SI trades, group by RTS 2 classification geting counts and notional sums\n", 816 | "si_agg_series = si_trades[['rts2_classification', 'eur_notional']]\\\n", 817 | " .groupby(by='rts2_classification')\\\n", 818 | " .agg(['count', 'sum'])\n", 819 | "\n", 820 | "agg_df = pd.DataFrame(si_agg_series)\n", 821 | "agg_df.columns = ['trade_count', 'notional_sum']\n", 822 | "\n", 823 | "sub_classes2 = pd.merge(\n", 824 | " sub_classes.reset_index(), \n", 825 | " agg_df.reset_index(), \n", 826 | " how='inner', on='rts2_classification')" 827 | ] 828 | }, 829 | { 830 | "cell_type": "code", 831 | "execution_count": 18, 832 | "metadata": { 833 | "collapsed": false 834 | }, 835 | "outputs": [], 836 | "source": [ 837 | "# Add a column for the average number of trades per week\n", 838 | "min_week_number = all_trades['trade_date_week'].min()\n", 839 | "max_week_number = all_trades['trade_date_week'].max()\n", 840 | "number_of_weeks = max_week_number - min_week_number + 1\n", 841 | "\n", 842 | "sub_classes3 = sub_classes2.copy()\n", 843 | "sub_classes3['avg_weekly_trades'] = sub_classes3['trade_count']\\\n", 844 | " .apply(lambda x: x / number_of_weeks)" 845 | ] 846 | }, 847 | { 848 | "cell_type": "markdown", 849 | "metadata": {}, 850 | "source": [ 851 | "## This is the calculation" 852 | ] 853 | }, 854 | { 855 | "cell_type": "code", 856 | "execution_count": 19, 857 | "metadata": { 858 | "collapsed": false 859 | }, 860 | "outputs": [ 861 | { 862 | "data": { 863 | "text/html": [ 864 | "
\n", 865 | "\n", 866 | " \n", 867 | " \n", 868 | " \n", 869 | " \n", 870 | " \n", 871 | " \n", 872 | " \n", 873 | " \n", 874 | " \n", 875 | " \n", 876 | " \n", 877 | " \n", 878 | " \n", 879 | " \n", 880 | " \n", 881 | " \n", 882 | " \n", 883 | " \n", 884 | " \n", 885 | " \n", 886 | " \n", 887 | " \n", 888 | " \n", 889 | " \n", 890 | " \n", 891 | " \n", 892 | " \n", 893 | " \n", 894 | " \n", 895 | " \n", 896 | " \n", 897 | " \n", 898 | " \n", 899 | " \n", 900 | " \n", 901 | " \n", 902 | " \n", 903 | "
rts2_classificationeu_notional_sumeu_trade_countis_liquidlei_notional_sumtrade_countnotional_sumavg_weekly_trades
2{\"RTS2 version\": \"EU 2017/583 of 14 July 2016\"...4752000000195True4480000005480000001.0
5{\"RTS2 version\": \"EU 2017/583 of 14 July 2016\"...2323000000312True4000000008230000001.6
\n", 904 | "
" 905 | ], 906 | "text/plain": [ 907 | " rts2_classification eu_notional_sum \\\n", 908 | "2 {\"RTS2 version\": \"EU 2017/583 of 14 July 2016\"... 4752000000 \n", 909 | "5 {\"RTS2 version\": \"EU 2017/583 of 14 July 2016\"... 2323000000 \n", 910 | "\n", 911 | " eu_trade_count is_liquid lei_notional_sum trade_count notional_sum \\\n", 912 | "2 195 True 448000000 5 48000000 \n", 913 | "5 312 True 400000000 8 23000000 \n", 914 | "\n", 915 | " avg_weekly_trades \n", 916 | "2 1.0 \n", 917 | "5 1.6 " 918 | ] 919 | }, 920 | "execution_count": 19, 921 | "metadata": {}, 922 | "output_type": "execute_result" 923 | } 924 | ], 925 | "source": [ 926 | "# Filter a\n", 927 | "\n", 928 | "fa = sub_classes3.copy()\n", 929 | "fa[(fa.is_liquid) \n", 930 | " & (fa.trade_count >= (fa.eu_trade_count * 0.025))\n", 931 | " & (fa.avg_weekly_trades >= 1)]\n" 932 | ] 933 | }, 934 | { 935 | "cell_type": "code", 936 | "execution_count": 20, 937 | "metadata": { 938 | "collapsed": false 939 | }, 940 | "outputs": [ 941 | { 942 | "data": { 943 | "text/html": [ 944 | "
\n", 945 | "\n", 946 | " \n", 947 | " \n", 948 | " \n", 949 | " \n", 950 | " \n", 951 | " \n", 952 | " \n", 953 | " \n", 954 | " \n", 955 | " \n", 956 | " \n", 957 | " \n", 958 | " \n", 959 | " \n", 960 | " \n", 961 | " \n", 962 | " \n", 963 | " \n", 964 | " \n", 965 | " \n", 966 | " \n", 967 | " \n", 968 | " \n", 969 | " \n", 970 | " \n", 971 | " \n", 972 | " \n", 973 | " \n", 974 | " \n", 975 | " \n", 976 | " \n", 977 | " \n", 978 | " \n", 979 | " \n", 980 | " \n", 981 | " \n", 982 | " \n", 983 | "
rts2_classificationeu_notional_sumeu_trade_countis_liquidlei_notional_sumtrade_countnotional_sumavg_weekly_trades
0{\"RTS2 version\": \"EU 2017/583 of 14 July 2016\"...4554000000205False3040000005460000001.0
4{\"RTS2 version\": \"EU 2017/583 of 14 July 2016\"...3232000000246False4440000006320000001.2
\n", 984 | "
" 985 | ], 986 | "text/plain": [ 987 | " rts2_classification eu_notional_sum \\\n", 988 | "0 {\"RTS2 version\": \"EU 2017/583 of 14 July 2016\"... 4554000000 \n", 989 | "4 {\"RTS2 version\": \"EU 2017/583 of 14 July 2016\"... 3232000000 \n", 990 | "\n", 991 | " eu_trade_count is_liquid lei_notional_sum trade_count notional_sum \\\n", 992 | "0 205 False 304000000 5 46000000 \n", 993 | "4 246 False 444000000 6 32000000 \n", 994 | "\n", 995 | " avg_weekly_trades \n", 996 | "0 1.0 \n", 997 | "4 1.2 " 998 | ] 999 | }, 1000 | "execution_count": 20, 1001 | "metadata": {}, 1002 | "output_type": "execute_result" 1003 | } 1004 | ], 1005 | "source": [ 1006 | "# Filter b\n", 1007 | "\n", 1008 | "fb = sub_classes3.copy()\n", 1009 | "fb[(~fb.is_liquid) \n", 1010 | " & (fb.avg_weekly_trades >= 1)]\n" 1011 | ] 1012 | }, 1013 | { 1014 | "cell_type": "code", 1015 | "execution_count": 21, 1016 | "metadata": { 1017 | "collapsed": false 1018 | }, 1019 | "outputs": [ 1020 | { 1021 | "data": { 1022 | "text/html": [ 1023 | "
\n", 1024 | "\n", 1025 | " \n", 1026 | " \n", 1027 | " \n", 1028 | " \n", 1029 | " \n", 1030 | " \n", 1031 | " \n", 1032 | " \n", 1033 | " \n", 1034 | " \n", 1035 | " \n", 1036 | " \n", 1037 | " \n", 1038 | " \n", 1039 | " \n", 1040 | " \n", 1041 | " \n", 1042 | " \n", 1043 | " \n", 1044 | " \n", 1045 | " \n", 1046 | " \n", 1047 | " \n", 1048 | " \n", 1049 | " \n", 1050 | " \n", 1051 | " \n", 1052 | " \n", 1053 | " \n", 1054 | " \n", 1055 | " \n", 1056 | " \n", 1057 | " \n", 1058 | " \n", 1059 | " \n", 1060 | " \n", 1061 | " \n", 1062 | " \n", 1063 | " \n", 1064 | " \n", 1065 | " \n", 1066 | " \n", 1067 | " \n", 1068 | " \n", 1069 | " \n", 1070 | " \n", 1071 | " \n", 1072 | " \n", 1073 | " \n", 1074 | " \n", 1075 | " \n", 1076 | " \n", 1077 | " \n", 1078 | " \n", 1079 | " \n", 1080 | " \n", 1081 | " \n", 1082 | " \n", 1083 | " \n", 1084 | "
rts2_classificationeu_notional_sumeu_trade_countis_liquidlei_notional_sumtrade_countnotional_sumavg_weekly_trades
0{\"RTS2 version\": \"EU 2017/583 of 14 July 2016\"...4554000000205False3040000005460000001.0
1{\"RTS2 version\": \"EU 2017/583 of 14 July 2016\"...693000000123True184000000370000000.6
2{\"RTS2 version\": \"EU 2017/583 of 14 July 2016\"...4752000000195True4480000005480000001.0
7{\"RTS2 version\": \"EU 2017/583 of 14 July 2016\"...505000000078True1210000002500000000.4
\n", 1085 | "
" 1086 | ], 1087 | "text/plain": [ 1088 | " rts2_classification eu_notional_sum \\\n", 1089 | "0 {\"RTS2 version\": \"EU 2017/583 of 14 July 2016\"... 4554000000 \n", 1090 | "1 {\"RTS2 version\": \"EU 2017/583 of 14 July 2016\"... 693000000 \n", 1091 | "2 {\"RTS2 version\": \"EU 2017/583 of 14 July 2016\"... 4752000000 \n", 1092 | "7 {\"RTS2 version\": \"EU 2017/583 of 14 July 2016\"... 5050000000 \n", 1093 | "\n", 1094 | " eu_trade_count is_liquid lei_notional_sum trade_count notional_sum \\\n", 1095 | "0 205 False 304000000 5 46000000 \n", 1096 | "1 123 True 184000000 3 7000000 \n", 1097 | "2 195 True 448000000 5 48000000 \n", 1098 | "7 78 True 121000000 2 50000000 \n", 1099 | "\n", 1100 | " avg_weekly_trades \n", 1101 | "0 1.0 \n", 1102 | "1 0.6 \n", 1103 | "2 1.0 \n", 1104 | "7 0.4 " 1105 | ] 1106 | }, 1107 | "execution_count": 21, 1108 | "metadata": {}, 1109 | "output_type": "execute_result" 1110 | } 1111 | ], 1112 | "source": [ 1113 | "# Filter c\n", 1114 | "\n", 1115 | "fc = sub_classes3.copy()\n", 1116 | "fc[ (fc.notional_sum >= (fc.lei_notional_sum * 0.25))\n", 1117 | " | (fc.notional_sum >= (fc.eu_notional_sum * 0.01))]\n" 1118 | ] 1119 | } 1120 | ], 1121 | "metadata": { 1122 | "kernelspec": { 1123 | "display_name": "Python 3", 1124 | "language": "python", 1125 | "name": "python3" 1126 | }, 1127 | "language_info": { 1128 | "codemirror_mode": { 1129 | "name": "ipython", 1130 | "version": 3 1131 | }, 1132 | "file_extension": ".py", 1133 | "mimetype": "text/x-python", 1134 | "name": "python", 1135 | "nbconvert_exporter": "python", 1136 | "pygments_lexer": "ipython3", 1137 | "version": "3.5.3" 1138 | } 1139 | }, 1140 | "nbformat": 4, 1141 | "nbformat_minor": 2 1142 | } 1143 | -------------------------------------------------------------------------------- /rts/rts23_table2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Three clause BSD license: https://opensource.org/licenses/BSD-3-Clause 4 | 5 | # Copyright (c) 2017, Bruce Badger All rights reserved. 6 | 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions 9 | # are met: 10 | 11 | # 1. Redistributions of source code must retain the above copyright 12 | # notice, this list of conditions and the following disclaimer. 13 | 14 | # 2. Redistributions in binary form must reproduce the above copyright 15 | # notice, this list of conditions and the following disclaimer in the 16 | # documentation and/or other materials provided with the distribution. 17 | 18 | # 3. Neither the name of the copyright holder nor the names of its 19 | # contributors may be used to endorse or promote products derived from 20 | # this software without specific prior written permission. 21 | 22 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 26 | # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 27 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 28 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 30 | # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | 34 | __author__ = "Bruce Badger" 35 | __copyright__ = "Copyright (c) 2017 Bruce Badger" 36 | __license__ = "BSD-3-Clause" 37 | 38 | 39 | """ 40 | This is an implementation of the rules defined here: 41 | http://ec.europa.eu/finance/securities/docs/isd/mifid/rts/160714-rts-23-annex_en.pdf 42 | This is the annex to RTS 23. The index to all MiFID Technical Standards documents is here: 43 | http://ec.europa.eu/finance/securities/docs/isd/mifid/its-rts-overview-table_en.pdf 44 | As far as possible the terminology is taken directly from the Annex to RTS 23. 45 | """ 46 | 47 | 48 | class ProductClassificationElement(object): 49 | 50 | def __init__(self, children=None): 51 | self.children = children or [] 52 | for child in self.children: 53 | child.parent = self 54 | 55 | 56 | class ProductClassificationSet(ProductClassificationElement): 57 | 58 | _cls_singleton = None 59 | 60 | @classmethod 61 | def singleton(cls): 62 | if cls._cls_singleton is None: 63 | cls._cls_singleton = cls() 64 | return cls._cls_singleton 65 | 66 | def node_for_triplet(self, triplet): 67 | """ 68 | Examples requests might be: 69 | .root.node_for_triplet(['OTHR', None, None]) 70 | .root.node_for_triplet(['PAPR', 'CBRD', None]) 71 | .root.node_for_triplet(['METL', 'PRME', 'GOLD']) 72 | .root.node_for_triplet(['NRGY', 'COAL', None]) 73 | """ 74 | for child in self.children: 75 | selected = child.node_for_triplet(triplet) 76 | if selected: 77 | return selected 78 | return None 79 | 80 | def child_with_code(self, a_code): 81 | for child in self.children: 82 | if child.code == a_code: 83 | return child 84 | return None 85 | 86 | 87 | class ProductClassificationNode(ProductClassificationElement): 88 | 89 | def __init__(self, code, description, children=None): 90 | super(ProductClassificationNode, self).__init__(children) 91 | self.code = code 92 | self.description = description 93 | self.parent = None 94 | 95 | def node_for_triplet(self, triplet): 96 | """ 97 | c.f. ProductClassificationSet.node_for_triplet() 98 | """ 99 | if self.matches_triplet(triplet): 100 | # print('Selected!') 101 | # print('My code = ' + str(self.code)) 102 | if self.children: 103 | for child in self.children: 104 | selected = child.node_for_triplet(triplet) 105 | if selected: 106 | return selected 107 | else: 108 | return self 109 | return None 110 | 111 | def matches_triplet(self, triplet): 112 | """ 113 | This must be implemented by my subclasses. 114 | """ 115 | raise NotImplementedError( 116 | 'matches_triplet() must be implemented in concrete subclass {my_class}' 117 | .format(my_class=type(self))) 118 | 119 | @property 120 | def base_product(self): 121 | return None 122 | 123 | @property 124 | def sub_product(self): 125 | return None 126 | 127 | @property 128 | def further_sub_product(self): 129 | return None 130 | 131 | 132 | class BaseProductClassification(ProductClassificationNode): 133 | 134 | def matches_triplet(self, triplet): 135 | return self.code == triplet[0] 136 | 137 | def triplet(self): 138 | return [self.code, None, None] 139 | 140 | @property 141 | def base_product(self): 142 | return self.code 143 | 144 | 145 | class SubProductClassification(ProductClassificationNode): 146 | 147 | def matches_triplet(self, triplet): 148 | return self.code == triplet[1] 149 | 150 | def triplet(self): 151 | return [ 152 | self.parent.code, 153 | self.code, 154 | None] 155 | 156 | @property 157 | def base_product(self): 158 | return self.parent 159 | 160 | @property 161 | def sub_product(self): 162 | return self 163 | 164 | 165 | class FurtherSubProductClassification(ProductClassificationNode): 166 | 167 | def matches_triplet(self, triplet): 168 | return self.code == triplet[2] 169 | 170 | def triplet(self): 171 | return [ 172 | self.parent.parent.code, 173 | self.parent.code, 174 | self.code] 175 | 176 | @property 177 | def base_product(self): 178 | return self.parent.parent 179 | 180 | @property 181 | def sub_product(self): 182 | return self.parent 183 | 184 | @property 185 | def further_sub_product(self): 186 | return self 187 | 188 | 189 | root = ProductClassificationSet(children=[ 190 | 191 | BaseProductClassification( 192 | code='AGRI', 193 | description='Agricultural', 194 | children=[ 195 | SubProductClassification( 196 | code='GROS', 197 | description='Grains and Oil Seeds', 198 | children=[ 199 | FurtherSubProductClassification(code='FWHT', description='Feed Wheat'), 200 | FurtherSubProductClassification(code='SOYB', description='Soybeans'), 201 | FurtherSubProductClassification(code='CORN', description='Maize'), 202 | FurtherSubProductClassification(code='RPSD', description='Rapeseed'), 203 | FurtherSubProductClassification(code='RICE', description='Rice'), 204 | FurtherSubProductClassification(code='OTHR', description='Other'), 205 | ] 206 | ), 207 | 208 | SubProductClassification( 209 | code='SOFT', 210 | description='Softs', 211 | children=[ 212 | FurtherSubProductClassification(code='CCOA', description='Cocoa'), 213 | FurtherSubProductClassification(code='ROBU', description='Robusta Coffee'), 214 | FurtherSubProductClassification(code='WHSG', description='White Sugar'), 215 | FurtherSubProductClassification(code='BRWN', description='Raw Sugar'), 216 | FurtherSubProductClassification(code='OTHR', description='Other'), 217 | ] 218 | ), 219 | 220 | SubProductClassification(code='POTA', description='Potato'), 221 | 222 | SubProductClassification( 223 | code='OOLI', 224 | description='Olive oil', 225 | children=[ 226 | FurtherSubProductClassification(code='LAMP', description='Lampante'), 227 | ] 228 | ), 229 | 230 | SubProductClassification(code='DIRY', description='Dairy'), 231 | 232 | SubProductClassification(code='FRST', description='Forestry'), 233 | 234 | SubProductClassification(code='SEAF', description='Seafood'), 235 | 236 | SubProductClassification(code='LSTK', description='Livestock'), 237 | 238 | SubProductClassification( 239 | code='GRIN', 240 | description='Grain', 241 | children=[ 242 | FurtherSubProductClassification(code='MWHT', description='Milling Wheat'), 243 | ] 244 | ), 245 | 246 | ] 247 | ), 248 | 249 | 250 | BaseProductClassification( 251 | code='NRGY', 252 | description='Energy', 253 | children=[ 254 | 255 | SubProductClassification( 256 | code='ELEC', 257 | description='Electricity', 258 | children=[ 259 | FurtherSubProductClassification(code='BSLD', description='Base load'), 260 | FurtherSubProductClassification(code='FITR', description='Financial Transmission Rights'), 261 | FurtherSubProductClassification(code='PKLD', description='Peak load'), 262 | FurtherSubProductClassification(code='OFFP', description='Off-peak'), 263 | FurtherSubProductClassification(code='OTHR', description='Other'), 264 | ] 265 | ), 266 | 267 | SubProductClassification( 268 | code='NGAS', 269 | description='Natural Gas', 270 | children=[ 271 | FurtherSubProductClassification(code='GASP', description='GASPOOL'), 272 | FurtherSubProductClassification(code='LNGG', description='LNG'), 273 | FurtherSubProductClassification(code='NBPG', description='NBP'), 274 | FurtherSubProductClassification(code='NCGG', description='NCG'), 275 | FurtherSubProductClassification(code='TTFG', description='TTF'), 276 | ] 277 | ), 278 | 279 | SubProductClassification( 280 | code='OILP', 281 | description='Oil', 282 | children=[ 283 | FurtherSubProductClassification(code='BAKK', description='Bakken'), 284 | FurtherSubProductClassification(code='BDSL', description='Biodiesel'), 285 | FurtherSubProductClassification(code='BRNT', description='Brent'), 286 | FurtherSubProductClassification(code='BRNX', description='Brent NX'), 287 | FurtherSubProductClassification(code='CNDA', description='Canadian'), 288 | FurtherSubProductClassification(code='COND', description='Condensate'), 289 | FurtherSubProductClassification(code='DSEL', description='Diesel'), 290 | FurtherSubProductClassification(code='DUBA', description='Dubai'), 291 | FurtherSubProductClassification(code='ESPO', description='ESPO'), 292 | FurtherSubProductClassification(code='ETHA', description='Ethanol'), 293 | FurtherSubProductClassification(code='FUEL', description='Fuel'), 294 | FurtherSubProductClassification(code='FOIL', description='Fuel Oil'), 295 | FurtherSubProductClassification(code='GOIL', description='Gasoil'), 296 | FurtherSubProductClassification(code='GSLN', description='Gasoline'), 297 | FurtherSubProductClassification(code='HEAT', description='Heating Oil'), 298 | FurtherSubProductClassification(code='JTFL', description='Jet Fuel'), 299 | FurtherSubProductClassification(code='KERO', description='Kerosene'), 300 | FurtherSubProductClassification(code='LLSO', description='Light Louisiana Sweet (LLS)'), 301 | FurtherSubProductClassification(code='MARS', description='Mars'), 302 | FurtherSubProductClassification(code='NAPH', description='Naptha'), 303 | FurtherSubProductClassification(code='NGLO', description='NGL'), 304 | FurtherSubProductClassification(code='TAPI', description='Tapis'), 305 | FurtherSubProductClassification(code='URAL', description='Urals'), 306 | FurtherSubProductClassification(code='WTIO', description='WTI'), 307 | FurtherSubProductClassification(code='COAL', description='Coal'), 308 | FurtherSubProductClassification(code='INRG', description='Inter Energy'), 309 | FurtherSubProductClassification(code='RNNG', description='Renewable energy'), 310 | FurtherSubProductClassification(code='LGHT', description='Light ends'), 311 | FurtherSubProductClassification(code='DIST', description='Distillates'), 312 | ] 313 | ), 314 | 315 | SubProductClassification(code='COAL', description='Coal'), 316 | 317 | SubProductClassification(code='INRG', description='Inter Energy'), 318 | 319 | SubProductClassification(code='RNNG', description='Renewable energy'), 320 | 321 | SubProductClassification(code='LGHT', description='Light ends'), 322 | 323 | SubProductClassification(code='DIST', description='Distillates'), 324 | 325 | ] 326 | ), 327 | 328 | 329 | BaseProductClassification( 330 | code='ENVR', 331 | description='Environmental', 332 | children=[ 333 | 334 | SubProductClassification( 335 | code='EMIS', 336 | description='Emissions', 337 | children=[ 338 | FurtherSubProductClassification(code='CERE', description='CER'), 339 | FurtherSubProductClassification(code='ERUE', description='ERU'), 340 | FurtherSubProductClassification(code='EUAE', description='EUA'), 341 | FurtherSubProductClassification(code='EUAA', description='EUAA'), 342 | FurtherSubProductClassification(code='OTHR', description='Other'), 343 | ] 344 | ), 345 | 346 | SubProductClassification(code='WTHR', description='Weather'), 347 | 348 | SubProductClassification(code='CRBR', description='Carbon related'), 349 | 350 | ] 351 | ), 352 | 353 | 354 | BaseProductClassification( 355 | code='FRGT', 356 | description='Freight', 357 | children=[ 358 | 359 | SubProductClassification( 360 | code='WETF', 361 | description='Wet', 362 | children=[ 363 | FurtherSubProductClassification(code='TNKR', description='Tankers'), 364 | 365 | ] 366 | ), 367 | 368 | SubProductClassification( 369 | code='DRYF', 370 | description='Dry', 371 | children=[ 372 | FurtherSubProductClassification(code='DBCR', description='Dry bulk carriers'), 373 | 374 | ] 375 | ), 376 | 377 | SubProductClassification(code='CSHP', description='Container ships'), 378 | 379 | ] 380 | ), 381 | 382 | 383 | BaseProductClassification( 384 | code='FRTL', 385 | description='Fertilizer', 386 | children=[ 387 | SubProductClassification(code='AMMO', description='Ammonia'), 388 | SubProductClassification(code='DAPH', description='DAP (Diammonium Phosphate)'), 389 | SubProductClassification(code='PTSH', description='Potash'), 390 | SubProductClassification(code='SLPH', description='Sulphur'), 391 | SubProductClassification(code='UREA', description='Urea'), 392 | SubProductClassification(code='UAAN', description='UAN (urea and ammonium nitrate)'), 393 | ] 394 | ), 395 | 396 | 397 | BaseProductClassification( 398 | code='INDP', 399 | description='Industrial products', 400 | children=[ 401 | SubProductClassification(code='CSTR', description='Construction'), 402 | SubProductClassification(code='MFTG', description='Manufacturing'), 403 | ] 404 | ), 405 | 406 | 407 | BaseProductClassification( 408 | code='METL', 409 | description='Metals', 410 | children=[ 411 | SubProductClassification( 412 | code='NPRM', 413 | description='Non Precious', 414 | children=[ 415 | FurtherSubProductClassification(code='ALUM', description='Aluminium'), 416 | FurtherSubProductClassification(code='ALUA', description='Aluminium Alloy'), 417 | FurtherSubProductClassification(code='CBLT', description='Cobalt'), 418 | FurtherSubProductClassification(code='COPR', description='Copper'), 419 | FurtherSubProductClassification(code='IRON', description='Iron ore'), 420 | FurtherSubProductClassification(code='LEAD', description='Lead'), 421 | FurtherSubProductClassification(code='MOLY', description='Molybdenum'), 422 | FurtherSubProductClassification(code='NASC', description='NASAAC'), 423 | FurtherSubProductClassification(code='NICK', description='Nickel'), 424 | FurtherSubProductClassification(code='STEL', description='Steel'), 425 | FurtherSubProductClassification(code='TINN', description='Tin'), 426 | FurtherSubProductClassification(code='ZINC', description='Zinc'), 427 | FurtherSubProductClassification(code='OTHR', description='Other'), 428 | ] 429 | ), 430 | 431 | SubProductClassification( 432 | code='PRME', 433 | description='Precious', 434 | children=[ 435 | FurtherSubProductClassification(code='GOLD', description='Gold'), 436 | FurtherSubProductClassification(code='SLVR', description='Silver'), 437 | FurtherSubProductClassification(code='PTNM', description='Platinum'), 438 | FurtherSubProductClassification(code='PLDM', description='Palladium'), 439 | FurtherSubProductClassification(code='OTHR', description='Other'), 440 | ] 441 | ), 442 | ] 443 | ), 444 | 445 | 446 | BaseProductClassification(code='MCEX', description='Multi Commodity Exotic'), 447 | 448 | 449 | BaseProductClassification( 450 | code='PAPR', 451 | description='Paper', 452 | children=[ 453 | SubProductClassification(code='CBRD', description='Containerboard'), 454 | SubProductClassification(code='NSPT', description='Newsprint'), 455 | SubProductClassification(code='PULP', description='Pulp'), 456 | SubProductClassification(code='RCVP', description='Recovered paper'), 457 | ] 458 | ), 459 | 460 | 461 | BaseProductClassification( 462 | code='POLY', 463 | description='Polypropylene', 464 | children=[ 465 | SubProductClassification(code='PLST', description='Plastic'), 466 | ] 467 | ), 468 | 469 | 470 | BaseProductClassification(code='INFL', description='Inflation'), 471 | 472 | 473 | BaseProductClassification(code='OEST', description='Official economic statistics'), 474 | 475 | 476 | BaseProductClassification( 477 | code='OTHC', 478 | description="Other C10 'as defined in Table 10.1 of Section 10 of Annex III to Commission " 479 | "Delegated Regulation supplementing Regulation (EU) No 600/2014 of the European " 480 | "Parliament and of the Council with regard to regulatory technical standards on " 481 | "transparency requirements for trading venues and investment firms in respect of " 482 | "bonds, structured finance products, emission allowances and derivatives"), 483 | 484 | 485 | BaseProductClassification(code='OTHR', description='Other'), 486 | ]) 487 | -------------------------------------------------------------------------------- /rts/rts2_annex3_model.py: -------------------------------------------------------------------------------- 1 | # Three clause BSD license: https://opensource.org/licenses/BSD-3-Clause 2 | 3 | # Copyright (c) 2017, Bruce Badger All rights reserved. 4 | 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions 7 | # are met: 8 | 9 | # 1. Redistributions of source code must retain the above copyright 10 | # notice, this list of conditions and the following disclaimer. 11 | 12 | # 2. Redistributions in binary form must reproduce the above copyright 13 | # notice, this list of conditions and the following disclaimer in the 14 | # documentation and/or other materials provided with the distribution. 15 | 16 | # 3. Neither the name of the copyright holder nor the names of its 17 | # contributors may be used to endorse or promote products derived from 18 | # this software without specific prior written permission. 19 | 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | __author__ = "Bruce Badger" 33 | __copyright__ = "Copyright (c) 2017-2018 Bruce Badger" 34 | __license__ = "BSD-3-Clause" 35 | 36 | import datetime 37 | import calendar 38 | import collections 39 | import random 40 | import json 41 | 42 | import rts23_table2 43 | 44 | import locale 45 | locale.setlocale(locale.LC_ALL, '') 46 | 47 | 48 | class SampleTrade(object): 49 | """ 50 | This class is used by the test data generation code. The classes 51 | have no instance variables by default, they are force fed the 52 | values they need. 53 | """ 54 | pass 55 | 56 | 57 | class TaxonomyNode(object): 58 | """ 59 | This class defines the API which much be implemented by all nodes. 60 | Essentially this means requiring that every node knows it's parent 61 | and children. Handy for presenting the taxonomy as a tree. 62 | """ 63 | 64 | @property 65 | def name(self): 66 | raise NotImplementedError('name() must be implemented in concrete subclass {my_class}' 67 | .format(my_class=type(self))) 68 | 69 | @property 70 | def parent(self): 71 | raise NotImplementedError('parent() must be implemented in concrete subclass {my_class}' 72 | .format(my_class=type(self))) 73 | 74 | @parent.setter 75 | def parent(self, value): 76 | raise NotImplementedError('*setter* of @property parent() must be implemented in concrete subclass {my_class}' 77 | .format(my_class=type(self))) 78 | 79 | @property 80 | def children(self): 81 | raise NotImplementedError('children() must be implemented in concrete subclass {my_class}' 82 | .format(my_class=type(self))) 83 | 84 | @property 85 | def parents(self): 86 | """ 87 | I return the list of my parents with the root first and me last. 88 | """ 89 | if self.parent is None: 90 | return [self] 91 | else: 92 | return self.parent.parents.append(self) 93 | 94 | def make_test_samples(self, number, sink_function=None): 95 | """ 96 | Synthesize a number of sample trades from me. If a sink 97 | function (or lambda) is given this will be called with each 98 | sample as it is generated and an empty list will be returned. 99 | If no sink_function is given the samples will be collected in a list 100 | and the complete list will be returned. 101 | """ 102 | sample_list=[] 103 | sink = sink_function or (lambda x: sample_list.append(x)) 104 | for _ in range(number): 105 | if self.should_delegate_sample_generation(): 106 | random_child = random.choice(self.children) 107 | random_child.make_test_samples(number=1, sink_function=sink) 108 | else: 109 | new_sample = SampleTrade() 110 | self.init_sample(new_sample) 111 | sink(new_sample) 112 | return sample_list 113 | 114 | def should_delegate_sample_generation(self): 115 | """ 116 | The asset class set and asset classes which have children can simply 117 | delegate the job of generating a sample to a child. 118 | See also the sub-asset class ... which can't simply delegate. 119 | """ 120 | return self.children # Note: this is the pythonic way vs using "len(x) > 0" 121 | 122 | def init_sample(self, sample): 123 | """ 124 | I initialise sample to look like my kind of trade ... well, my subclasses do. 125 | """ 126 | raise NotImplementedError('init_sample() must be implemented in concrete subclass {my_class}' 127 | .format(my_class=type(self))) 128 | 129 | 130 | class TaxonomyRoot(TaxonomyNode): 131 | 132 | def __init__(self, version_id, asset_classes=None): 133 | self.version_id = version_id 134 | self._asset_classes = [] 135 | given_asset_classes = asset_classes or [] 136 | self._asset_classes.extend(given_asset_classes) 137 | 138 | @property 139 | def name(self): 140 | return "root" 141 | 142 | @property 143 | def asset_classes(self): 144 | return self._asset_classes 145 | 146 | def leaf_for(self, subject): 147 | """ 148 | This method simply delegates to the newer visitor-pattern based classification_for() 149 | Consider this method deprecated. 150 | """ 151 | return self.classification_for(subject) 152 | 153 | def classification_for(self, subject): 154 | classification = Classification(subject=subject, root=self) 155 | asset_class = self.asset_class_by_name(subject.asset_class_name) 156 | if asset_class: 157 | asset_class.extend_classification(classification) 158 | else: 159 | classification.errors.append("RTS 2 has no Asset Class named '{asset_class_name}'.".format( 160 | asset_class_name=subject.asset_class_name 161 | )) 162 | return classification 163 | 164 | def asset_class_by_name(self, asset_class_name): 165 | return next((asset_class 166 | for asset_class 167 | in self.asset_classes 168 | if asset_class_name == asset_class.name), 169 | None) 170 | 171 | def append(self, asset_class): 172 | self._asset_classes.append(asset_class) 173 | 174 | def display(self, prefix=""): 175 | target = "The set of all Asset Classes:" 176 | for asset_class in self._asset_classes: 177 | target += "\n" 178 | target += asset_class.display(prefix=prefix+'- ') 179 | return target 180 | 181 | @property 182 | def parent(self): 183 | return None 184 | 185 | @property 186 | def children(self): 187 | return self.asset_classes 188 | 189 | def all_sub_asset_classes(self): 190 | sub_asset_class_list = [] 191 | for asset_class in self.children: 192 | sub_asset_class_list.extend(asset_class.children) 193 | return sub_asset_class_list 194 | 195 | def sub_asset_class_by_name(self, sub_asset_class_name): 196 | return next((sub_asset_class 197 | for sub_asset_class 198 | in self.all_sub_asset_classes() 199 | if sub_asset_class_name == sub_asset_class.name), 200 | None) 201 | 202 | 203 | class AssetClass(TaxonomyNode): 204 | def __init__(self, name, ref, sub_asset_classes, description=None): 205 | self._name = name 206 | self.description = description 207 | self.ref = ref 208 | # Claim ownership of the sub-asset classes, then assign them as my children 209 | for sub_asset_class in sub_asset_classes: 210 | sub_asset_class.parent = self 211 | self.sub_asset_classes = sub_asset_classes 212 | 213 | @property 214 | def name(self): 215 | return self._name 216 | 217 | def matches(self, subject): 218 | return subject.asset_class_name == self.name 219 | 220 | def extend_classification(self, classification): 221 | classification.asset_class = self 222 | if self.children: 223 | sub_asset_class = self.sub_asset_class_by_name(classification.subject.sub_asset_class_name) 224 | if sub_asset_class: 225 | sub_asset_class.extend_classification(classification) 226 | else: 227 | classification.errors.append( 228 | "Asset class '{asset_class_name}' has no Sub-asset Class named '{sub_asset_class_name}'.".format( 229 | asset_class_name=self.name, 230 | sub_asset_class_name=classification.subject.sub_asset_class_name 231 | ) 232 | ) 233 | return classification 234 | 235 | def sub_asset_class_by_name(self, sub_asset_class_name): 236 | return next((sub_asset_class 237 | for sub_asset_class 238 | in self.sub_asset_classes 239 | if sub_asset_class_name == sub_asset_class.name), 240 | None) 241 | 242 | def full_name(self): 243 | return "Asset class: " + self.name 244 | 245 | def path_name(self): 246 | return "\n" + self.full_name() 247 | 248 | def display(self, prefix=""): 249 | target = prefix + self.full_name() 250 | if self.description: 251 | target += "\n" + self.description 252 | for sub_asset_class in self.sub_asset_classes: 253 | target += "\n" 254 | target += sub_asset_class.display(prefix=prefix+'- ') 255 | return target 256 | 257 | def classification_dict(self): 258 | return {'Asset class': self.name} 259 | 260 | @property 261 | def parent(self): 262 | return None 263 | 264 | @property 265 | def children(self): 266 | return self.sub_asset_classes 267 | 268 | def init_sample(self, sample): 269 | """ 270 | I initialise sample to look like my kind of trade. 271 | """ 272 | sample.asset_class_name = self.name 273 | 274 | 275 | class SubAssetClass(TaxonomyNode): 276 | def __init__( 277 | self, 278 | name, 279 | ref=None, 280 | description=None, 281 | criteria=None, 282 | thresholds=None,): 283 | self.ref = ref 284 | self._name = name 285 | self.description = description 286 | # Claim ownership of the criteria, then assign them as my children 287 | given_criteria = [] if criteria is None else criteria 288 | for criterion in given_criteria: 289 | criterion.parent = self 290 | self._criteria = given_criteria 291 | self._thresholds = thresholds 292 | self._parent = None 293 | 294 | @property 295 | def name(self): 296 | return self._name 297 | 298 | @property 299 | def parent(self): 300 | return self._parent 301 | 302 | @parent.setter 303 | def parent(self, value): 304 | if self._parent is None: 305 | self._parent = value 306 | else: 307 | raise AttributeError("Parent already assigned. You may not change it.") 308 | 309 | @property 310 | def children(self): 311 | return self.criteria 312 | 313 | @property 314 | def criteria(self): 315 | return self._criteria 316 | 317 | @property 318 | def thresholds(self): 319 | return self._thresholds 320 | 321 | def matches(self, subject): 322 | return subject.sub_asset_class_name == self.name 323 | 324 | def extend_classification(self, classification): 325 | classification.sub_asset_class = self 326 | for a_criterion in self.criteria: 327 | a_criterion.extend_classification(classification) 328 | return classification 329 | 330 | def full_name(self): 331 | if self.ref: 332 | ref_string = "(ref={ref})".format(ref=self.ref) 333 | else: 334 | ref_string = "" 335 | return "Sub-asset class: {name} {ref}".format(name=self.name, ref=ref_string) 336 | 337 | def path_name(self): 338 | return self.parent.path_name() + "\n" + self.full_name() 339 | 340 | def display(self, prefix): 341 | target = prefix + self.full_name() 342 | for criterion in self.criteria: 343 | target += "\n" 344 | target += criterion.display(prefix=prefix+'- ') 345 | return target 346 | 347 | def classification_dict(self): 348 | return {'Sub-asset class': self.name} 349 | 350 | def criterion_number_for(self, criterion): 351 | index = self.criteria.index(criterion) 352 | return index + 1 353 | 354 | def should_delegate_sample_generation(self): 355 | """ 356 | The asset class set and asset classes can delegate the generation 357 | of samples, but I can't because below me are the criteria ... and I need 358 | to generate data for each of them, not just one. 359 | """ 360 | return False 361 | 362 | def init_sample(self, sample): 363 | """ 364 | I initialise sample to look like my kind of trade. 365 | """ 366 | self.parent.init_sample(sample) 367 | sample.sub_asset_class_name = self.name 368 | for a_criterion in self.criteria: 369 | a_criterion.init_sample(sample) 370 | 371 | 372 | class Classification(object): 373 | """ 374 | A Classification is a specific combination of allowable Asset Class Sub-asset class and 375 | criterion options for a single subject. 376 | 377 | A complete classification is the identity of an RTS 2 sub-class. A sub-class can be 378 | looked as as a sort of virtual ISIN. Trades with the same sub-class are for the same 379 | instrument in SI terms. 380 | 381 | Incomplete classifications indicate a trade which could not be classified. Classification 382 | problems are noted in the errors dictionary. 383 | """ 384 | 385 | def __init__(self, subject=None, root=None, sub_asset_class=None, options=None): 386 | self._subject = subject 387 | self._root = root 388 | self._asset_class = None 389 | self._sub_asset_class = sub_asset_class 390 | self._options = options 391 | self._errors = None 392 | 393 | @property 394 | def subject(self): 395 | return self._subject 396 | 397 | @property 398 | def root(self): 399 | return self._root 400 | 401 | @property 402 | def asset_class(self): 403 | """ 404 | TODO: Remove getting the asset class from the sub-asset class once classification code 405 | is stable, since there should always be an asset class if there is a sub-asset class. 406 | """ 407 | if not self._asset_class: 408 | if self.sub_asset_class: 409 | self._asset_class = self.sub_asset_class.parent 410 | return self._asset_class 411 | 412 | @asset_class.setter 413 | def asset_class(self, value): 414 | if not self._asset_class: 415 | self._asset_class = value 416 | return None 417 | 418 | @property 419 | def sub_asset_class(self): 420 | return self._sub_asset_class 421 | 422 | @sub_asset_class.setter 423 | def sub_asset_class(self, value): 424 | if not self._sub_asset_class: 425 | self._sub_asset_class = value 426 | return None 427 | 428 | @property 429 | def options(self): 430 | if self._options is None: 431 | self._options = [] 432 | return self._options 433 | 434 | @property 435 | def errors(self): 436 | if self._errors is None: 437 | self._errors = [] 438 | return self._errors 439 | 440 | def full_name(self): 441 | full_name_string = self.sub_asset_class.path_name() 442 | full_name_string += "\n Segmentation criteria options:" 443 | for option in self.options: 444 | full_name_string += "\n" + option.full_name() 445 | return full_name_string 446 | 447 | def classification_dict(self): 448 | """ 449 | This dictionary is the stored form of the classification. The string literal form of 450 | the dictionary is the literal name used to identify RTS 2 sub-classes. 451 | """ 452 | target_dict = collections.OrderedDict() 453 | if self.root: 454 | target_dict['RTS2 version'] = self.root.version_id 455 | if self.asset_class: 456 | target_dict.update(self.asset_class.classification_dict()) 457 | if self.sub_asset_class: 458 | target_dict.update(self.sub_asset_class.classification_dict()) 459 | for option in self.options: 460 | target_dict.update(option.classification_dict()) 461 | if self.errors: 462 | target_dict['errors'] = str(self.errors) 463 | return target_dict 464 | 465 | def as_json(self, indent=None): 466 | return json.dumps(self.classification_dict(), indent=indent) 467 | 468 | 469 | class Criterion(TaxonomyNode): 470 | def __init__(self, description): 471 | self._description = description 472 | self._parent = None 473 | 474 | @property 475 | def description(self): 476 | return self._description 477 | 478 | @property 479 | def parent(self): 480 | return self._parent 481 | 482 | @parent.setter 483 | def parent(self, value): 484 | if self._parent is None: 485 | self._parent = value 486 | else: 487 | raise AttributeError("Parent already assigned to this Criterion. You may not change it.") 488 | 489 | def extend_classification(self, classification): 490 | raise NotImplementedError('extend_classification() must be implemented in concrete subclass {my_class}' 491 | .format(my_class=type(self))) 492 | 493 | @property 494 | def selector(self): 495 | """ 496 | This is the string which can be used with getattr() to get a value for a criteria 497 | from a subject (a trade). The name 'selector' is taken from Smalltalk. 498 | """ 499 | raise NotImplementedError('selector() must be implemented in concrete subclass {my_class}' 500 | .format(my_class=type(self))) 501 | 502 | def subject_value(self, subject): 503 | return getattr(subject, self.selector, None) 504 | 505 | @property 506 | def criterion_number(self): 507 | return self.parent.criterion_number_for(self) 508 | 509 | @property 510 | def criterion_name(self): 511 | return 'Segmentation criterion {n}'.format(n=self.criterion_number) 512 | 513 | def display(self, prefix): 514 | return prefix + self.full_name() 515 | 516 | def full_name(self): 517 | return '{criterion_name} - {criterion_description}'.format( 518 | criterion_name=self.criterion_name, 519 | criterion_description=self.description, 520 | ) 521 | 522 | @property 523 | def children(self): 524 | return [] 525 | 526 | 527 | class CriterionOption(TaxonomyNode): 528 | def __init__(self, criterion): 529 | self._criterion = criterion 530 | 531 | @property 532 | def criterion(self): 533 | return self._criterion 534 | 535 | @property 536 | def parent(self): 537 | return self.criterion 538 | 539 | @property 540 | def children(self): 541 | return [] 542 | 543 | def full_name(self): 544 | return self.parent.display(prefix=' - ') 545 | 546 | def classification_dict(self): 547 | raise NotImplementedError('classification_dict() must be implemented in concrete subclass {my_class}' 548 | .format(my_class=type(self))) 549 | 550 | 551 | class ArbitraryValueCriterion(Criterion): 552 | def __init__(self, description): 553 | super(ArbitraryValueCriterion, self).__init__(description) 554 | self.concrete_options = dict() 555 | 556 | def extend_classification(self, classification): 557 | try: 558 | this_value = str(self.subject_value(classification.subject)) 559 | if this_value not in self.concrete_options: 560 | new_option = ValueOption(criterion=self, value=this_value) 561 | self.concrete_options[this_value] = new_option 562 | this_option= self.concrete_options[this_value] 563 | classification.options.append(this_option) 564 | except AttributeError as ex: 565 | classification.errors.append('{my_class} exception: {exception_string}'.format( 566 | my_class=type(self), 567 | exception_string=str(ex), 568 | ) 569 | ) 570 | return classification 571 | 572 | def init_sample(self, sample): 573 | sample_value = '{selector}.value'.format(selector=self.selector) 574 | setattr(sample, self.selector, sample_value) 575 | return sample 576 | 577 | 578 | class DescreteValueCriterion(Criterion): 579 | def __init__(self, description): 580 | super(DescreteValueCriterion, self).__init__(description) 581 | self._concrete_options = None 582 | 583 | @property 584 | def concrete_options(self): 585 | if self._concrete_options is None: 586 | values_dict = dict() 587 | for allowed_value in self.allowed_values(): 588 | new_option = ValueOption(criterion=self, value=allowed_value) 589 | values_dict[allowed_value] = new_option 590 | self._concrete_options = values_dict 591 | return self._concrete_options 592 | 593 | def allowed_values(self): 594 | raise NotImplementedError('allowed_values() must be implemented in concrete subclass {my_class}' 595 | .format(my_class=type(self))) 596 | 597 | def extend_classification(self, classification): 598 | try: 599 | try: 600 | this_value = self.subject_value(classification.subject) 601 | except (AttributeError): 602 | classification.errors.append('{my_class} got no value. Should be one of [{good_values}].'.format( 603 | my_class=type(self).__name__, 604 | good_values=", ".join(self.concrete_options), 605 | )) 606 | return classification 607 | this_option = self.concrete_options[this_value] 608 | classification.options.append(this_option) 609 | except (KeyError) as ex: 610 | classification.errors.append('{my_class} got bad value: {value}. Should be one of [{good_values}].'.format( 611 | my_class=type(self).__name__, 612 | value=self.subject_value(classification.subject), 613 | good_values=", ".join(self.concrete_options), 614 | )) 615 | return classification 616 | 617 | def init_sample(self, sample): 618 | sample_value = random.choice(self.allowed_values()) 619 | setattr(sample, self.selector, sample_value) 620 | return sample 621 | 622 | 623 | class ValueOption(CriterionOption): 624 | def __init__(self, criterion, value): 625 | super(ValueOption, self).__init__(criterion) 626 | self._value = value 627 | 628 | @property 629 | def value(self): 630 | return self._value 631 | 632 | def full_name(self): 633 | return '{parent}" for {value}'.format( 634 | parent=self.parent.full_name(), 635 | value=self.value) 636 | 637 | def classification_dict(self): 638 | criterion_name = self.parent.criterion_name 639 | description_name = criterion_name + " description" 640 | target_dict = collections.OrderedDict() 641 | target_dict[description_name] = self.parent.description 642 | target_dict[criterion_name] = self.value 643 | return target_dict 644 | 645 | 646 | class NotionalCurrencyCriterion(ArbitraryValueCriterion): 647 | 648 | @property 649 | def selector(self): 650 | return 'notional_currency' 651 | 652 | 653 | class UnderlyingIssuerCriterion(ArbitraryValueCriterion): 654 | 655 | @property 656 | def selector(self): 657 | return 'underlying_issuer' 658 | 659 | 660 | class UnderlyingCurrencyPairCriterion(ArbitraryValueCriterion): 661 | 662 | @property 663 | def selector(self): 664 | return 'underlying_currency_pair' 665 | 666 | 667 | class UnderlyingStockIndexCriterion(ArbitraryValueCriterion): 668 | 669 | @property 670 | def selector(self): 671 | return 'underlying_stock_index' 672 | 673 | 674 | class UnderlyingShareCriterion(ArbitraryValueCriterion): 675 | 676 | @property 677 | def selector(self): 678 | return 'underlying_share' 679 | 680 | 681 | class UnderlyingShareEntitlingToDividendsCriterion(ArbitraryValueCriterion): 682 | 683 | @property 684 | def selector(self): 685 | return 'underlying_share_entitling_to_dividends' 686 | 687 | 688 | class UnderlyingDividendIndexCriterion(ArbitraryValueCriterion): 689 | 690 | @property 691 | def selector(self): 692 | return 'underlying_dividend_index' 693 | 694 | 695 | class UnderlyingVolatilityIndexCriterion(ArbitraryValueCriterion): 696 | 697 | @property 698 | def selector(self): 699 | return 'underlying_volatility_index' 700 | 701 | 702 | class UnderlyingETFCriterion(ArbitraryValueCriterion): 703 | 704 | @property 705 | def selector(self): 706 | return 'underlying_etf' 707 | 708 | 709 | class UnderlyingEquityTypeCriterion(DescreteValueCriterion): 710 | 711 | @property 712 | def selector(self): 713 | return 'underlying_type' 714 | 715 | def allowed_values(self): 716 | return['single name', 'index', 'basket'] 717 | 718 | 719 | class UnderlyingEquityCriterion(ArbitraryValueCriterion): 720 | 721 | @property 722 | def selector(self): 723 | return 'underlying_equity' 724 | 725 | 726 | class UnderlyingCommodityCriterion(ArbitraryValueCriterion): 727 | 728 | @property 729 | def selector(self): 730 | return 'underlying_commodity' 731 | 732 | 733 | class UnderlyingBondCriterion(ArbitraryValueCriterion): 734 | 735 | @property 736 | def selector(self): 737 | return 'underlying_bond' 738 | 739 | 740 | class UnderlyingFutureForwardCriterion(ArbitraryValueCriterion): 741 | 742 | @property 743 | def selector(self): 744 | return 'underlying_future_forward' 745 | 746 | 747 | class UnderlyingEquityOptionCriterion(ArbitraryValueCriterion): 748 | 749 | @property 750 | def selector(self): 751 | return 'underlying_equity_option' 752 | 753 | 754 | class EquityParameterCriterion(DescreteValueCriterion): 755 | 756 | @property 757 | def selector(self): 758 | return 'equity_parameter' 759 | 760 | def allowed_values(self): 761 | return['price', 'dividend', 'variance'] 762 | 763 | 764 | class EnergyTypeCriterion(DescreteValueCriterion): 765 | 766 | @property 767 | def selector(self): 768 | return 'energy_type' 769 | 770 | @staticmethod 771 | def rts23_nrgy_nodes(): 772 | return rts23_table2.root.child_with_code('NRGY').children 773 | 774 | def allowed_values(self): 775 | """ 776 | I am **assuming** that all possible values here are the children of 'NRGY' in 777 | the RTS 23 taxonomy. 778 | """ 779 | return [node.code for node in self.rts23_nrgy_nodes()] 780 | 781 | 782 | class MetalTypeCriterion(DescreteValueCriterion): 783 | 784 | @property 785 | def selector(self): 786 | return 'metal_type' 787 | 788 | @staticmethod 789 | def rts23_metl_nodes(): 790 | return rts23_table2.root.child_with_code('METL').children 791 | 792 | def allowed_values(self): 793 | """ 794 | I am **assuming** that all possible values here are the children of 'METL' in 795 | the RTS 23 taxonomy. 796 | """ 797 | return [node.code for node in self.rts23_metl_nodes()] 798 | 799 | 800 | class UnderlyingEnergyCriterion(ArbitraryValueCriterion): 801 | 802 | @property 803 | def selector(self): 804 | return 'underlying_energy' 805 | 806 | 807 | class SettlementTypeCriterion(ArbitraryValueCriterion): 808 | 809 | @property 810 | def selector(self): 811 | return 'settlement_type' 812 | 813 | 814 | class LoadTypeCriterion(ArbitraryValueCriterion): 815 | 816 | @property 817 | def selector(self): 818 | return 'load_type' 819 | 820 | 821 | class DeliveryCriterion(ArbitraryValueCriterion): 822 | 823 | @property 824 | def selector(self): 825 | return 'delivery' 826 | 827 | 828 | class UnderlyingInstrumentCriterion(ArbitraryValueCriterion): 829 | 830 | @property 831 | def selector(self): 832 | return 'underlying_instrument' 833 | 834 | 835 | class UnderlyingInterestRateCriterion(ArbitraryValueCriterion): 836 | 837 | @property 838 | def selector(self): 839 | return 'underlying_interest_rate' 840 | 841 | 842 | class TermOfUnderlyingInterestRateCriterion(ArbitraryValueCriterion): 843 | 844 | @property 845 | def selector(self): 846 | return 'term_of_underlying_interest_rate' 847 | 848 | 849 | class UnderlyingSwapTypeCriterion(ArbitraryValueCriterion): 850 | 851 | @property 852 | def selector(self): 853 | return 'underlying_swap_type' 854 | 855 | 856 | class InflationIndexCriterion(ArbitraryValueCriterion): 857 | 858 | @property 859 | def selector(self): 860 | return 'inflation_index' 861 | 862 | 863 | class NotionalCurrencyPairCriterion(ArbitraryValueCriterion): 864 | 865 | @property 866 | def selector(self): 867 | return 'notional_currency_pair' 868 | 869 | 870 | class UnderlyingMetalCriterion(ArbitraryValueCriterion): 871 | 872 | @property 873 | def selector(self): 874 | return 'underlying_metal' 875 | 876 | 877 | class UnderlyingAgriculturalCriterion(ArbitraryValueCriterion): 878 | 879 | @property 880 | def selector(self): 881 | return 'underlying_agricultural' 882 | 883 | 884 | class UnderlyingIndexCriterion(ArbitraryValueCriterion): 885 | 886 | @property 887 | def selector(self): 888 | return 'underlying_index' 889 | 890 | 891 | class UnderlyingReferenceEntityCriterion(ArbitraryValueCriterion): 892 | 893 | @property 894 | def selector(self): 895 | return 'underlying_ref_entity' 896 | 897 | 898 | class UnderlyingReferenceEntityTypeCriterion(ArbitraryValueCriterion): 899 | 900 | @property 901 | def selector(self): 902 | return 'ref_entity_type' 903 | 904 | 905 | class CDSIndexSubClassCriterion(ArbitraryValueCriterion): 906 | 907 | @property 908 | def selector(self): 909 | return 'cds_index_sub_class' 910 | 911 | 912 | class CDSSubClassCriterion(ArbitraryValueCriterion): 913 | 914 | @property 915 | def selector(self): 916 | return 'cds_sub_class' 917 | 918 | 919 | class ContractTypeCriterion(ArbitraryValueCriterion): 920 | 921 | @property 922 | def selector(self): 923 | return 'contract_type' 924 | 925 | 926 | class FreightTypeCriterion(ArbitraryValueCriterion): 927 | 928 | @property 929 | def selector(self): 930 | return 'freight_type' 931 | 932 | 933 | class FreightSubTypeCriterion(ArbitraryValueCriterion): 934 | 935 | @property 936 | def selector(self): 937 | return 'freight_sub_type' 938 | 939 | 940 | class FreightSizeCriterion(ArbitraryValueCriterion): 941 | 942 | @property 943 | def selector(self): 944 | return 'freight_size' 945 | 946 | 947 | class FreightRouteOrTimeCriterion(ArbitraryValueCriterion): 948 | 949 | @property 950 | def selector(self): 951 | return 'freight_route_or_time' 952 | 953 | 954 | class BucketedTermOfUnderlyingCriterion(Criterion): 955 | def __init__(self, description, bucket_ceilings): 956 | super(BucketedTermOfUnderlyingCriterion, self).__init__(description) 957 | self._bucket_ceilings = bucket_ceilings 958 | self._root_option = None 959 | 960 | @property 961 | def bucket_ceilings(self): 962 | return self._bucket_ceilings 963 | 964 | @property 965 | def root_option(self): 966 | if self._root_option is None: 967 | new_root = DateBucketOption(parent=self, bucket_ceilings=self.bucket_ceilings) 968 | self._root_option = new_root 969 | return self._root_option 970 | 971 | def extend_classification(self, classification): 972 | try: 973 | option = self.root_option.option_for_dates( 974 | classification.subject.term_from_date, 975 | classification.subject.term_to_date) 976 | if option: 977 | classification.options.append(option) 978 | else: 979 | raise KeyError 980 | except KeyError as _: 981 | classification.errors.append( 982 | 'Bad term bucket. ' 983 | 'Dates: from_date={from_date}, to_date={to_date}.'.format( 984 | from_date=classification.subject.term_from_date, 985 | to_date=classification.subject.term_to_date, 986 | ) 987 | ) 988 | return classification 989 | 990 | def init_sample(self, sample): 991 | sample.term_from_date = datetime.date.today() 992 | delta_days = random.choice([30, 90, 180, 365, 1000]) 993 | sample.term_to_date = sample.term_from_date + datetime.timedelta(delta_days) 994 | 995 | 996 | class SwapMaturityBucketCriterion(Criterion): 997 | def __init__(self, description, bucket_ceilings): 998 | super(SwapMaturityBucketCriterion, self).__init__(description) 999 | self._bucket_ceilings = bucket_ceilings 1000 | self._root_option = None 1001 | 1002 | @property 1003 | def bucket_ceilings(self): 1004 | return self._bucket_ceilings 1005 | 1006 | @property 1007 | def root_option(self): 1008 | if self._root_option is None: 1009 | new_root = DateBucketOption(parent=self, bucket_ceilings=self.bucket_ceilings) 1010 | self._root_option = new_root 1011 | return self._root_option 1012 | 1013 | def extend_classification(self, classification): 1014 | try: 1015 | option = self.root_option.option_for_dates( 1016 | classification.subject.swap_from_date, 1017 | classification.subject.swap_to_date) 1018 | if option: 1019 | classification.options.append(option) 1020 | else: 1021 | raise KeyError 1022 | except KeyError as _: 1023 | classification.errors.append( 1024 | 'Bad swap maturity bucket. ' 1025 | 'Dates: from_date={from_date}, to_date={to_date}.'.format( 1026 | from_date=classification.subject.swap_from_date, 1027 | to_date=classification.subject.swap_to_date, 1028 | ) 1029 | ) 1030 | return classification 1031 | 1032 | def init_sample(self, sample): 1033 | sample.swap_from_date = datetime.date.today() 1034 | delta_days = random.choice([30, 90, 180, 365, 1000]) 1035 | sample.swap_to_date = sample.swap_from_date + datetime.timedelta(delta_days) 1036 | 1037 | 1038 | class OptionMaturityBucketCriterion(Criterion): 1039 | def __init__(self, description, bucket_ceilings): 1040 | super(OptionMaturityBucketCriterion, self).__init__(description) 1041 | self._bucket_ceilings = bucket_ceilings 1042 | self._root_option = None 1043 | 1044 | @property 1045 | def bucket_ceilings(self): 1046 | return self._bucket_ceilings 1047 | 1048 | @property 1049 | def root_option(self): 1050 | if self._root_option is None: 1051 | new_root = DateBucketOption(parent=self, bucket_ceilings=self.bucket_ceilings) 1052 | self._root_option = new_root 1053 | return self._root_option 1054 | 1055 | def extend_classification(self, classification): 1056 | try: 1057 | option = self.root_option.option_for_dates( 1058 | classification.subject.option_from_date, 1059 | classification.subject.option_to_date) 1060 | if option: 1061 | classification.options.append(option) 1062 | else: 1063 | raise KeyError 1064 | except KeyError: 1065 | classification.errors.append( 1066 | 'Bad option maturity bucket. ' 1067 | 'Dates: from_date={from_date}, to_date={to_date}.'.format( 1068 | from_date=classification.subject.from_date, 1069 | to_date=classification.subject.to_date, 1070 | ) 1071 | ) 1072 | return classification 1073 | 1074 | def init_sample(self, sample): 1075 | sample.option_from_date = datetime.date.today() 1076 | delta_days = random.choice([30, 90, 180, 365, 1000]) 1077 | sample.option_to_date = sample.option_from_date + datetime.timedelta(delta_days) 1078 | 1079 | 1080 | class MetalsMaturityBucketCriterion(Criterion): 1081 | def __init__(self, description, options): 1082 | super(MetalsMaturityBucketCriterion, self).__init__(description) 1083 | self._options = options 1084 | # Claim ownership of the criteria, then assign them as my children 1085 | for criterion in options.values(): 1086 | criterion.parent = self 1087 | 1088 | @property 1089 | def description(self): 1090 | """ 1091 | I need to return a blend of my description and that of my parenr 1092 | """ 1093 | return self._description 1094 | 1095 | def extend_classification(self, classification): 1096 | """ 1097 | Here I simply delegate to the maturity bucket Criterion for the parameter of my subject 1098 | """ 1099 | metal_type = 'Unknown' 1100 | try: 1101 | metal_type = classification.subject.metal_type 1102 | bucket_criterion = self._options[metal_type] 1103 | bucket_criterion.extend_classification(classification) 1104 | except KeyError: 1105 | classification.errors.append( 1106 | 'Bad metals maturity bucket. Metal type: {metal_type}. ' 1107 | 'Dates: from_date={from_date}, to_date={to_date}.'.format( 1108 | metal_type=metal_type, 1109 | from_date=classification.subject.from_date, 1110 | to_date=classification.subject.to_date, 1111 | ) 1112 | ) 1113 | return classification 1114 | 1115 | def init_sample(self, sample): 1116 | random.choice(list(self._options.values())).init_sample(sample) 1117 | 1118 | def criterion_number_for(self, criterion): 1119 | """ 1120 | I delegate this to my parent since I represent the Segmentation number here, not criterion. 1121 | """ 1122 | if criterion.parent == self: 1123 | return self.parent.criterion_number_for(self) 1124 | 1125 | def display(self, prefix): 1126 | target = super(MetalsMaturityBucketCriterion, self).display(prefix=prefix) 1127 | segment_prefix = prefix + '- ' 1128 | for (bucket_name, bucket_criterion) in self._options.items(): 1129 | target += '\n' + segment_prefix + 'Maturity buckets for "' + bucket_name + '"\n' 1130 | target += bucket_criterion.root_option.display(prefix=segment_prefix + '- ') 1131 | return target 1132 | 1133 | 1134 | class EnergyMaturityBucketCriterion(Criterion): 1135 | def __init__(self, description, options): 1136 | super(EnergyMaturityBucketCriterion, self).__init__(description) 1137 | self._bucket_map = None 1138 | self._options = options 1139 | # Claim ownership of the criteria, then assign them as my children 1140 | for criterion in options.values(): 1141 | criterion.parent = self 1142 | 1143 | @property 1144 | def bucket_map(self): 1145 | """ 1146 | Really this is a guess. These mappings are to Sub Products in RTS 23. 1147 | I'm guessing 'RNNG' goes in gas_electricty. 1148 | """ 1149 | if self._bucket_map is None: 1150 | product_map = dict( 1151 | oil=['OILP', 'DIST', 'LGHT'], 1152 | coal=['COAL'], 1153 | gas_electricity=['ELEC', 'NGAS', 'INRG', 'RNNG'], 1154 | ) 1155 | reversed_map = {} 1156 | for bucket_name, sub_product_names in product_map.items(): 1157 | for sub_product_name in sub_product_names: 1158 | reversed_map[sub_product_name] = bucket_name 1159 | self._bucket_map = reversed_map 1160 | return self._bucket_map 1161 | 1162 | def extend_classification(self, classification): 1163 | """ 1164 | Here I simply delegate to the maturity bucket Criterion for the parameter of my subject 1165 | Energy type must be one of the Subproduct vales of NRGY in RTS 23 1166 | """ 1167 | try: 1168 | try: 1169 | # TODO: Use the energy type criteria to try to get the value ... and return None 1170 | energy_type = classification.subject.energy_type 1171 | except (AttributeError): 1172 | classification.errors.append( 1173 | 'Bad energy maturity bucket. Subject has no energy type.') 1174 | return classification 1175 | bucket_key = self.bucket_map[energy_type] 1176 | bucket_criterion = self._options[bucket_key] 1177 | bucket_criterion.extend_classification(classification) 1178 | except KeyError: 1179 | classification.errors.append( 1180 | 'Bad energy maturity bucket. Energy type: {energy_type}. ' 1181 | 'Dates: from_date={from_date}, to_date={to_date}.'.format( 1182 | energy_type=classification.subject.energy_type, 1183 | from_date=classification.subject.from_date, 1184 | to_date=classification.subject.to_date, 1185 | ) 1186 | ) 1187 | return classification 1188 | 1189 | def init_sample(self, sample): 1190 | sample.from_date = datetime.date.today() 1191 | delta_days = random.choice([30, 90, 180, 365, 1000]) 1192 | sample.to_date = sample.from_date + datetime.timedelta(delta_days) 1193 | 1194 | def criterion_number_for(self, criterion): 1195 | """ 1196 | I delegate this to my parent since I represent the Segmentation number here, not criterion. 1197 | """ 1198 | if criterion.parent == self: 1199 | return self.parent.criterion_number_for(self) 1200 | 1201 | def display(self, prefix): 1202 | target = super(EnergyMaturityBucketCriterion, self).display(prefix=prefix) 1203 | segment_prefix = prefix + '- ' 1204 | for (bucket_name, bucket_criterion) in self._options.items(): 1205 | target += '\n' + segment_prefix + 'Maturity buckets for "' + bucket_name + '"\n' 1206 | target += bucket_criterion.root_option.display(prefix=segment_prefix + '- ') 1207 | return target 1208 | 1209 | 1210 | class EquityParameterMaturityBucketCriterion(Criterion): 1211 | def __init__(self, description, options): 1212 | super(EquityParameterMaturityBucketCriterion, self).__init__(description) 1213 | self._options = options 1214 | # Claim ownership of the criteria, then assign them as my children 1215 | for criterion in options.values(): 1216 | criterion.parent = self 1217 | 1218 | def extend_classification(self, classification): 1219 | bucket_criterion = self._options.get(classification.subject.equity_parameter, None) 1220 | if bucket_criterion: 1221 | bucket_criterion.extend_classification(classification) 1222 | else: 1223 | classification.errors.append("{my_class} error - no bucketing for parameter: {parameter}. " 1224 | "Must be one of: [{good_values}].".format( 1225 | my_class=type(self).__name__, 1226 | parameter=classification.subject.equity_parameter, 1227 | good_values=", ".join(self._options.keys()), 1228 | )) 1229 | return classification 1230 | 1231 | def init_sample(self, sample): 1232 | random.choice(list(self._options.values())).init_sample(sample) 1233 | 1234 | def criterion_number_for(self, criterion=None): 1235 | """ 1236 | I delegate this to my parent since I represent the Segmentation number here, not criterion. 1237 | """ 1238 | if criterion.parent == self: 1239 | return self.parent.criterion_number_for(self) 1240 | 1241 | def display(self, prefix): 1242 | target = super(EquityParameterMaturityBucketCriterion, self).display(prefix=prefix) 1243 | segment_prefix = prefix + '- ' 1244 | for (bucket_name, bucket_criterion) in self._options.items(): 1245 | target += '\n' + segment_prefix + 'Maturity buckets for "' + bucket_name + '"\n' 1246 | target += bucket_criterion.root_option.display(prefix=segment_prefix + '- ') 1247 | return target 1248 | 1249 | 1250 | class MaturityBucketCriterion(Criterion): 1251 | def __init__(self, description, bucket_ceilings): 1252 | super(MaturityBucketCriterion, self).__init__(description) 1253 | self._bucket_ceilings = bucket_ceilings 1254 | self._root_option = None 1255 | 1256 | @property 1257 | def bucket_ceilings(self): 1258 | return self._bucket_ceilings 1259 | 1260 | @property 1261 | def root_option(self): 1262 | if self._root_option is None: 1263 | new_root = DateBucketOption(parent=self, bucket_ceilings=self.bucket_ceilings) 1264 | self._root_option = new_root 1265 | return self._root_option 1266 | 1267 | def display(self, prefix): 1268 | target = super(MaturityBucketCriterion, self).display(prefix=prefix) 1269 | target += self.description + "\n" 1270 | target += self.root_option.display(prefix=prefix+'- ') 1271 | return target 1272 | 1273 | def extend_classification(self, classification): 1274 | option = self.root_option.option_for(classification.subject) 1275 | if option: 1276 | classification.options.append(option) 1277 | else: 1278 | classification.errors.append('Bad maturity bucket dates: from_date={from_date}, to_date={to_date}.'.format( 1279 | from_date=classification.subject.from_date, 1280 | to_date=classification.subject.to_date, 1281 | )) 1282 | return classification 1283 | 1284 | def init_sample(self, sample): 1285 | sample.from_date = datetime.date.today() 1286 | delta_days = random.choice([30, 90, 180, 365, 1000]) 1287 | sample.to_date = sample.from_date + datetime.timedelta(delta_days) 1288 | 1289 | 1290 | class MaturityBucketCeiling(object): 1291 | 1292 | def __init__(self, periods, description=None): 1293 | self._periods = periods 1294 | self._description = description 1295 | 1296 | @property 1297 | def periods(self): 1298 | return self._periods 1299 | 1300 | @property 1301 | def period_name(self): 1302 | raise NotImplementedError('period_name() must be implemented in concrete subclass {my_class}' 1303 | .format(my_class=type(self))) 1304 | 1305 | def ceiling_string(self): 1306 | """ 1307 | A string which describes my ceiling, e.g. 1 month or 5 years 1308 | """ 1309 | target = "{n} {name}".format( 1310 | n=self.periods, 1311 | name=self.period_name) 1312 | return target 1313 | 1314 | def display(self, prefix=""): 1315 | target = "time to maturity <= {ceiling}".format( 1316 | ceiling=self.ceiling_string, 1317 | ) 1318 | return target 1319 | 1320 | def end_date_from(self, base_date): 1321 | raise NotImplementedError('end_date_from() must be implemented in concrete subclass {my_class}' 1322 | .format(my_class=type(self))) 1323 | 1324 | @staticmethod 1325 | def date_on_or_before(year, month, day): 1326 | """ 1327 | I return a date which is ideally year/month/day but if I can't 1328 | make a date with those values I return the closest date prior. 1329 | e.g. if you ask for 2016/02/31 you'll get 2016/02/29 1330 | """ 1331 | sane_day = max(1, min(day, 31)) 1332 | sane_month = max(1, min(month, 12)) 1333 | sane_year = max(1900, min(year, 9999)) # though 1900 or 9999 may not be very sane years 1334 | best_date = None 1335 | while best_date is None: 1336 | try: 1337 | best_date = datetime.date(sane_year, sane_month, sane_day) 1338 | except ValueError: 1339 | if sane_day > 1: 1340 | sane_day -= 1 1341 | pass 1342 | return best_date 1343 | 1344 | def next_step(self, bucket_option): 1345 | """ 1346 | I return a new instance of my class which takes the next step into the future by 1347 | exactly the same amount of time I represent from the preceding step of option. 1348 | I'll only do this if my class and the one before me is of the same type ... for now, 1349 | anyway. 1350 | """ 1351 | previous_bucket_ceiling = bucket_option.previous_bucket_option.ceiling 1352 | my_type = type(self) 1353 | if type(previous_bucket_ceiling) == my_type: 1354 | difference_since_last = self.periods - previous_bucket_ceiling.periods 1355 | next_number_of_periods = self.periods + difference_since_last 1356 | return my_type(next_number_of_periods) 1357 | else: 1358 | error_message = "Can't work out the next step. I am a {my_type}, the preceding is {preceding}".format( 1359 | my_type=my_type, 1360 | preceding=type(previous_bucket_ceiling) 1361 | ) 1362 | raise ValueError(error_message) 1363 | 1364 | 1365 | class WeekBucketCeiling(MaturityBucketCeiling): 1366 | 1367 | @property 1368 | def period_name(self): 1369 | if self.periods == 1: 1370 | return "week" 1371 | else: 1372 | return "weeks" 1373 | 1374 | def end_date_from(self, base_date): 1375 | """ 1376 | I work out the end date by simply adding 6 days to the start date. 1377 | """ 1378 | delta = datetime.timedelta(days=6) 1379 | return base_date + delta 1380 | 1381 | 1382 | class MonthBucketCeiling(MaturityBucketCeiling): 1383 | 1384 | @property 1385 | def period_name(self): 1386 | if self.periods == 1: 1387 | return "month" 1388 | else: 1389 | return "months" 1390 | 1391 | @staticmethod 1392 | def add_months(base_date, months): 1393 | """ 1394 | From http://stackoverflow.com/questions/4130922/ 1395 | how-to-increment-datetime-by-custom-months-in-python-without-using-library 1396 | I would have preferred using relativedelta as explained here: 1397 | http://stackoverflow.com/questions/546321/ 1398 | how-do-i-calculate-the-date-six-months-from-the-current-date-using-the-datetime 1399 | ... but we don't have dateutil.relative delta :-( 1400 | 1401 | """ 1402 | month = base_date.month - 1 + months 1403 | year = int(base_date.year + month / 12) 1404 | month = month % 12 + 1 1405 | day = min(base_date.day, calendar.monthrange(year, month)[1]) 1406 | return datetime.date(year, month, day) 1407 | 1408 | def end_date_from(self, base_date): 1409 | return self.add_months(base_date, self.periods) 1410 | 1411 | 1412 | class YearBucketCeiling(MaturityBucketCeiling): 1413 | 1414 | @property 1415 | def period_name(self): 1416 | if self.periods == 1: 1417 | return "year" 1418 | else: 1419 | return "years" 1420 | 1421 | def end_date_from(self, base_date): 1422 | """ 1423 | I work out the end date by dumbly adding the number of years to the start date, then 1424 | if this is not a valid date I try day -1. So if start date is the 29th Feb on a leap 1425 | year and the dumb addition ends up in February in a non-leap year then I'll try 29th Feb 1426 | & fail and then try 28th Feb which will work. 28th Feb would be the end date. 1427 | """ 1428 | return self.date_on_or_before( 1429 | year=base_date.year + self.periods, 1430 | month=base_date.month, 1431 | day=base_date.day, 1432 | ) 1433 | 1434 | 1435 | class UnboundedBucketCeiling(MaturityBucketCeiling): 1436 | """ 1437 | This ceiling would used as a catch all. 1438 | For example, Interest Rate Derivatives / Swaptions have a time to maturuty 1439 | bucket for the option which is "over 10 years". 1440 | ... so this class will be developed when we need to implement Interest Rate Derivatives / Swaptions 1441 | """ 1442 | def __init__(self, description=None): 1443 | super(UnboundedBucketCeiling, self).__init__( 1444 | periods=1, 1445 | description=description) 1446 | 1447 | def ceiling_string(self): 1448 | """ 1449 | A string which describes my ceiling, e.g. 1 month or 5 years 1450 | """ 1451 | return "unbounded" 1452 | 1453 | def display(self, prefix=""): 1454 | return prefix + "time to maturity unbounded" 1455 | 1456 | @property 1457 | def period_name(self): 1458 | return "unbounded" 1459 | 1460 | def end_date_from(self, base_date): 1461 | """ 1462 | I'm unbounded so I have no end date. 1463 | """ 1464 | return None 1465 | 1466 | 1467 | class DateBucketOption(object): 1468 | def __init__(self, parent, bucket_ceilings, previous_bucket_option=None): 1469 | """ 1470 | I represent a range of dates which are compared against various dates of 1471 | subjects (trades). The most common dates are maturity dates. 1472 | Typically I will form a part of a linked list of buckets reaching into the future. If 1473 | I am created with a previous bucket I link to that and take it's last date + 1 day 1474 | as my starting day. If I have no previous bucket then I start 'today'. 1475 | """ 1476 | self._parent = parent 1477 | self._ceiling = bucket_ceilings[0] # There must be one so failing if this is none is good 1478 | self._previous_bucket_option = previous_bucket_option 1479 | remaining_bucket_ceilings = bucket_ceilings[1:] 1480 | if remaining_bucket_ceilings: # If there are more ceilings create the next option in the list 1481 | self._next_bucket_option = type(self)( 1482 | parent=parent, 1483 | bucket_ceilings=remaining_bucket_ceilings, 1484 | previous_bucket_option=self) 1485 | else: 1486 | self._next_bucket_option = None 1487 | self._end_date = None 1488 | 1489 | @property 1490 | def parent(self): 1491 | return self._parent 1492 | 1493 | @property 1494 | def ceiling(self): 1495 | """ 1496 | My ceiling is a MaturityBucketCeiling which defines a duration such as 1 week, 1497 | 3 months or 2 years. 1498 | """ 1499 | return self._ceiling 1500 | 1501 | @property 1502 | def next_bucket_option(self): 1503 | return self._next_bucket_option 1504 | 1505 | @property 1506 | def previous_bucket_option(self): 1507 | return self._previous_bucket_option 1508 | 1509 | @property 1510 | def bucket_number(self): 1511 | if self.previous_bucket_option is None: 1512 | return 1 1513 | else: 1514 | return self.previous_bucket_option.bucket_number + 1 1515 | 1516 | @property 1517 | def start_date(self): 1518 | """ 1519 | If I'm the first bucket I start on the base date (the date of my context). If I'm 1520 | subsequent bucket I start on the day after the end of the previous bucket. 1521 | """ 1522 | if self.previous_bucket_option is None: 1523 | return self.parent.base_date 1524 | else: 1525 | return self.previous_bucket_option.end_date + datetime.timedelta(days=1) 1526 | 1527 | @property 1528 | def end_date(self): 1529 | if self._end_date is None: 1530 | self._end_date = self.ceiling.end_date_from(base_date=self.parent.base_date) 1531 | return self._end_date 1532 | 1533 | def name(self): 1534 | if self.previous_bucket_option: 1535 | floor_string = self.previous_bucket_option.ceiling.ceiling_string() + " to " 1536 | else: 1537 | floor_string = "Zero to " 1538 | return 'Maturity bucket {bucket_number}: {floor}{ceiling}'.format( 1539 | bucket_number=self.bucket_number, 1540 | floor=floor_string, 1541 | ceiling=self.ceiling.ceiling_string(), 1542 | ) 1543 | 1544 | def display(self, prefix=""): 1545 | target = prefix + self.name() 1546 | if self.next_bucket_option: 1547 | target += "\n" + self.next_bucket_option.display(prefix=prefix) 1548 | return target 1549 | 1550 | def classification_dict(self): 1551 | """ 1552 | I return the a dictionary containing my attributes of the sub class. 1553 | """ 1554 | criterion_name = self.parent.criterion_name 1555 | description_name = criterion_name + " description" 1556 | target_dict = collections.OrderedDict() 1557 | target_dict[description_name] = self.parent.description 1558 | target_dict[criterion_name] = self.name() 1559 | return target_dict 1560 | 1561 | def option_for_dates(self, from_date, to_date): 1562 | # print(str(self.full_name())) 1563 | # print(">>From" + str(from_date) + ", to: " + str(to_date)) 1564 | # sanity checks ... 1565 | if from_date is None or to_date is None or to_date < from_date: 1566 | return None 1567 | bucket_end_date = self.ceiling.end_date_from(base_date=from_date) 1568 | if bucket_end_date is None or to_date <= bucket_end_date: 1569 | return self 1570 | else: 1571 | return self.get_next_bucket_option().option_for_dates(from_date=from_date, to_date=to_date) 1572 | 1573 | def option_for(self, subject): 1574 | return self.option_for_deal_lifetime(subject) 1575 | # return self.option_for_maturity_date(subject) 1576 | 1577 | def option_for_deal_lifetime(self, subject): 1578 | """ 1579 | I return myself if the, given a base of subject.from_date, the subject.to_date 1580 | is beneath my ceiling. Otherwise I delegate to the next bucket. 1581 | """ 1582 | return self.option_for_dates( 1583 | from_date=subject.from_date, 1584 | to_date=subject.to_date, 1585 | ) 1586 | 1587 | def get_next_bucket_option(self): 1588 | """ 1589 | This is the more assertive way of asking for the next bucket. If I currently 1590 | have no next bucket then I make one based on me. 1591 | """ 1592 | if not self.next_bucket_option: 1593 | my_class = type(self) 1594 | new_bucket = my_class( 1595 | parent=self.parent, 1596 | bucket_ceilings=[self.ceiling.next_step(bucket_option=self)], 1597 | previous_bucket_option=self) 1598 | self._next_bucket_option = new_bucket 1599 | return self.next_bucket_option 1600 | 1601 | def full_name(self): 1602 | target = self.parent.full_name() + ' ' 1603 | target += self.name() 1604 | return target 1605 | 1606 | 1607 | class ThresholdSpecification(object): 1608 | """ 1609 | The rules and thresholds which determine if a sub-class is reportable. 1610 | """ 1611 | def __init__( 1612 | self, 1613 | liquidity_criteria=None, 1614 | liquid_thresholds=None, 1615 | non_liquid_thresholds=None, 1616 | ): 1617 | self.liquidity_criteria = liquidity_criteria 1618 | self.liquid_thresholds = liquid_thresholds or [] 1619 | self.non_liquid_thresholds = non_liquid_thresholds 1620 | 1621 | def summary_string(self): 1622 | target_string = "ThresholdSpecification:" 1623 | if self.liquidity_criteria: 1624 | target_string += "\n >" + self.liquidity_criteria.summary_string() 1625 | else: 1626 | target_string += "\n >" + "No liquidity criteria" 1627 | target_string += "\n-Liquid:" 1628 | if self.liquid_thresholds: 1629 | for liquid_threshold in self.liquid_thresholds: 1630 | target_string += "\n>" + liquid_threshold.summary_string() 1631 | else: 1632 | target_string += "\n >" + "NONE" 1633 | target_string += "\n-Non-liquid:" 1634 | if self.non_liquid_thresholds: 1635 | target_string += "\n >" + self.non_liquid_thresholds.summary_string() 1636 | else: 1637 | target_string += "\n >" + "NONE" 1638 | return target_string 1639 | 1640 | 1641 | class LiquidityCriteria(object): 1642 | """ 1643 | The rules which determine if a sub-class is deemed to be liquid or not. 1644 | From the document (e.g. Table 5.1): 1645 | "Each sub-class shall be determined not to have a liquid market as per 1646 | Articles 6 and 8(1)(b) if it does not meet one or all of the following 1647 | thresholds of the quantitative liquidity criteria. For sub-classes 1648 | determined to have a liquid market the additional qualitative liquidity 1649 | criterion, where applicable, shall be applied" 1650 | """ 1651 | 1652 | def __init__( 1653 | self, 1654 | average_daily_notional_amount=None, 1655 | average_daily_number_of_trades=None, 1656 | qualitative_liquidity_criterion=None): 1657 | self.average_daily_notional_amount = average_daily_notional_amount 1658 | self.average_daily_number_of_trades = average_daily_number_of_trades 1659 | self.qualitative_liquidity_criterion = qualitative_liquidity_criterion 1660 | 1661 | def summary_string(self): 1662 | target_string = 'LiquidityCriteria: ADNA={adna}, No.ofTrades={number}, Qualitative="{qualitative}".'.format( 1663 | adna=self.average_daily_notional_amount or "None", 1664 | number=self.average_daily_number_of_trades or "None", 1665 | qualitative=self.qualitative_liquidity_criterion or "None", 1666 | ) 1667 | return target_string 1668 | 1669 | 1670 | class ThresholdTable(object): 1671 | """ 1672 | Text taken from RTS 2 (c.f Table 5.2): 1673 | 1674 | [Table for] pre-trade and post-trade SSTI and LIS thresholds for 1675 | sub-classes determined to have a liquid market 1676 | 1677 | Transactions to be considered for the calculations of the thresholds 1678 | ... calculation of thresholds should be performed for each sub-class of 1679 | the sub-asset class considering the transactions executed on financial 1680 | instruments belonging to the sub-class 1681 | """ 1682 | 1683 | def __init__( 1684 | self, 1685 | ssti_pre_trade, 1686 | lis_pre_trade, 1687 | ssti_post_trade, 1688 | lis_post_trade, 1689 | adna_floor=None): 1690 | self.adna_floor = adna_floor 1691 | self.ssti_pre_trade = ssti_pre_trade 1692 | self.lis_pre_trade = lis_pre_trade 1693 | self.ssti_post_trade = ssti_post_trade 1694 | self.lis_post_trade = lis_post_trade 1695 | 1696 | def summary_string(self): 1697 | target_string = \ 1698 | "ThresholdTable: " \ 1699 | "\n adna_floor={adna_floor}, " \ 1700 | "\n ssti_pre_trade={ssti_pre_trade}, " \ 1701 | "\n lis_pre_trade={lis_pre_trade}, " \ 1702 | "\n ssti_post_trade={ssti_post_trade}, " \ 1703 | "\n lis_post_trade={lis_post_trade}".format( 1704 | adna_floor=self.adna_floor, 1705 | ssti_pre_trade=self.ssti_pre_trade, 1706 | lis_pre_trade=self.lis_pre_trade, 1707 | ssti_post_trade=self.ssti_post_trade, 1708 | lis_post_trade=self.lis_post_trade, 1709 | ) 1710 | return target_string 1711 | 1712 | 1713 | class SumOfMoney(object): 1714 | def __init__(self, currency, amount): 1715 | self.currency = currency 1716 | self.amount = locale.atoi(amount) 1717 | 1718 | def __repr__(self): 1719 | return 'SumOfMoney("{currency}", {amount})'.format( 1720 | currency=self.currency, 1721 | amount=locale.format("%d", self.amount, grouping=True), 1722 | ) 1723 | 1724 | 1725 | class PreTrade(object): 1726 | def __init__(self, trade_percentile=None, threshold_floor=None): 1727 | self.trade_percentile = trade_percentile 1728 | self.threshold_floor = threshold_floor 1729 | 1730 | def __repr__(self): 1731 | return 'PreTrade(trade_percentile={percentile}, threshold_floor={floor})'.format( 1732 | percentile=self.trade_percentile, 1733 | floor=self.threshold_floor, 1734 | ) 1735 | 1736 | 1737 | class PostTrade(object): 1738 | def __init__(self, trade_percentile=None, volume_percentile=None, threshold_floor=None): 1739 | self.trade_percentile = trade_percentile 1740 | self.volume_percentile = volume_percentile 1741 | self.threshold_floor = threshold_floor 1742 | 1743 | def __repr__(self): 1744 | return 'PostTrade(trade_percentile={trade_percentile}, ' \ 1745 | 'volume_percentile={volume_percentile}, ' \ 1746 | 'threshold_floor={floor})'.format( 1747 | trade_percentile=self.trade_percentile, 1748 | volume_percentile=self.volume_percentile, 1749 | floor=self.threshold_floor, 1750 | ) 1751 | --------------------------------------------------------------------------------