├── .gitignore ├── .vscode ├── launch.json ├── microstructure-plotter.code-workspace └── settings.json ├── LICENSE ├── README.md ├── assets ├── didnt_send_1.png ├── didnt_send_2.png ├── didnt_send_3.png ├── erratic_valuation_1.png ├── erratic_valuation_2.png ├── erratic_valuation_3.png ├── erratic_valuation_4.png ├── legend.png ├── order_book.png ├── plotter_video.gif ├── tts.png └── zoomed_out_events.png ├── docs ├── legend │ └── README.md ├── micro_primer │ └── README.md └── schema │ └── README.md ├── examples ├── .DS_Store ├── README.md └── example_1 │ ├── .DS_Store │ ├── fake_data │ ├── fill_data.csv │ ├── order_data.csv │ ├── quote_data.csv │ ├── trade_data.csv │ └── val_data.csv │ ├── plot_example.py │ └── plot_example.sh ├── microplot ├── __init__.py ├── data.py ├── plotter.py ├── schema.py └── scripts │ ├── __init__.py │ └── plot_csv.py ├── requirements.txt └── setup.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 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Python: Current File", 5 | "type": "python", 6 | "request": "launch", 7 | "program": "${file}", 8 | "console": "integratedTerminal", 9 | "cwd": "${fileDirname}", 10 | "env": {"PYTHONPATH": "${workspaceFolder}${pathSeparator}${env:PYTHONPATH}"} 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /.vscode/microstructure-plotter.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": ".." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "terminal.integrated.env.osx": {"PYTHONPATH": "${workspaceFolder}"} 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Will Thompson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # microstructure-plotter 2 | 3 | **For those of you in the trenches, I offer ye this tool ⚔️** 4 | 5 | This is a plotting tool designed to help visualize financial market data 📈 and trading system behavior 🤖 on the smallest timescales 🔎. 6 | 7 | As all things in life, when it comes to financial microstructure: _the devil 😈 is in the details_ 8 | 9 | Built using [Chaco](https://docs.enthought.com/chaco/index.html) 🌮 10 | 11 | See the [illustrated example](#illustrated-example) for a deep dive into what this plotting tool can uncover 🔎. 12 | 13 | ![zoooooom](/assets/plotter_video.gif) 14 | 15 | ![tts](/assets/tts.png) 16 | 17 | ![orders](/assets/erratic_valuation_3.png) 18 | 19 | ## Contents 20 | 21 | - [⚠️Disclaimer](#%EF%B8%8Fdisclaimer) 22 | - [✨Features](#features) 23 | - [🧠Illustrated Example](#illustrated-example) 24 | - [🧪Test It Out Right Now](#test-it-out-right-now) 25 | - [🗺️Plots Legend](#%EF%B8%8Fplots-legend) 26 | - [💸Free Advice](#free-advice) 27 | - [✏️Data Schema](#%EF%B8%8Fdata-schema) 28 | - [🤔What is "Market Microstructure"?](#what-is-market-microstructure) 29 | - [🛞Installation](#installation) 30 | - [ 📜Reqs](#reqs) 31 | - [𓊍To Do](#𓊍to-do) 32 | - [Citation](#citation) 33 | - [License](#license) 34 | 35 | ## ⚠️Disclaimer 36 | 37 | There is no α here. This is just a fancy wrapper on top of an open-source plotting package. 38 | 39 | You should **B.Y.O.A** (_Bring Your Own Alpha_). 40 | 41 | **However**...for the trained eye 👁️, this simple type of visualization can be immensely helpful. 42 | 43 | ## ✨Features 44 | 45 | Here are some useful features to take note of 🥁: 46 | 47 | 1. ✨Plot **multiple** 🤹, asynchronous (unsampled) microstructure elements together on **the sample plot**: order book quotes, trades, orders, order acks, fills etc etc 48 | 2. ✨Analyze events on seconds, milliseconds, microseconds, _nanoseconds_ 🕳️ : zoom 🔎 in on the **most granular time unit** available to microstructure behavior 49 | 3. ✨All axes are 🔗**linked** : 🔎 _zoom in on one product, see what's happening in all others at that same timestamp_ 50 | 51 | ## 🧠Illustrated Example 52 | 53 | To show why a tool like this might be useful, [here](/examples/README.md) is an example motivated from things seen in the wild. 54 | 55 | Note: This data was painstakingly created _by hand_ 🤌 to appear quasi-realistic. I am not an artist 🧑‍🎨 nor is this real 🌎 data. 56 | 57 | See the [legend](#%EF%B8%8Fplots-legend) 👇 to understand the plots further. 58 | 59 | ## 🧪Test It Out Right Now 60 | 61 | After installing the package either via [pip](#installation) or via [setup.py](#reqs), run this quick line to see this in action right now: 62 | 63 | ``` bash examples/example_1/plot_example.sh ``` 64 | 65 | or 66 | 67 | ``` python examples/example_1/plot_example.py ``` 68 | 69 | **Right-click** 🖱️ to zoom in on different parts of the plot to see what is happening on **smaller** and **_smaller_** timescales 🔎. Consult the [legend](#plots-legend) or [illustrated example](#illustrated-example) for more clarity on how to interpret the plots. 70 | 71 | ## 🗺️Plots Legend 72 | 73 | [Here](/docs/legend/README.md) is a complete legend of everything the plotter can visualize. 74 | 75 | ## 💸Free Advice 76 | 77 | 1. _Don't log in prod_ ✏️ (unless logging isn't occuring on the hot path). 78 | 2. Make sure the timestamps are from ⏰ _synchronized clocks_ ⏰ (better if geosync'd/GPS) with enough precision, otherwise these plots will be uninformative or misleading. If you don't trust others' timestamps, do your own capture. 79 | 3. This plotter is _memory intensive_ 🧠. Don't try to plot too much at once. 80 | 81 | ## ✏️Data Schema 82 | 83 | This plotting module expects a specific [data schema](/docs/schema/README.md). 84 | 85 | ## 🤔What is "Market Microstructure"? 86 | 87 | _For the uninitiated, here is a [⏰ 30 second primer ⏰ on market microstructure](/docs/micro_primer/README.md)_. 88 | 89 | ## 🛞Installation 90 | 91 | You can install the repo using pip directly: 92 | 93 | ```pip install git+https://github.com/will-thompson-k/microstructure-plotter``` 94 | 95 | Alternatively you can use the setup.py. 96 | 97 | ## 📜Reqs 98 | 99 | Python 3.7+ recommended. 100 | 101 | Here are the package requirements (found in requirements.txt) 102 | 103 | - [ ] chaco==5.1.0 104 | - [ ] enable==5.3.1 105 | - [ ] traits==6.4.1 106 | - [ ] traitsui==7.4.2 107 | - [ ] pandas==1.3.5 108 | - [ ] pyqt 109 | 110 | ## 𓊍To Do 111 | 112 | - [ ] add more order types: modifies, equity order types, etc. 113 | - [ ] add "decision boundary" of model: bid/ask edges, etc. 114 | - [ ] unit tests 115 | 116 | ## Citation 117 | 118 | ```python 119 | @misc{microstructure-plotter, 120 | author = {Thompson, Will}, 121 | url = {https://github.com/will-thompson-k/microstructure-plotter}, 122 | year = {2023} 123 | } 124 | ``` 125 | ## License 126 | 127 | MIT -------------------------------------------------------------------------------- /assets/didnt_send_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/will-thompson-k/microstructure-plotter/ce5a1c5326c5709523758c8942a25fd8db45c8ee/assets/didnt_send_1.png -------------------------------------------------------------------------------- /assets/didnt_send_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/will-thompson-k/microstructure-plotter/ce5a1c5326c5709523758c8942a25fd8db45c8ee/assets/didnt_send_2.png -------------------------------------------------------------------------------- /assets/didnt_send_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/will-thompson-k/microstructure-plotter/ce5a1c5326c5709523758c8942a25fd8db45c8ee/assets/didnt_send_3.png -------------------------------------------------------------------------------- /assets/erratic_valuation_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/will-thompson-k/microstructure-plotter/ce5a1c5326c5709523758c8942a25fd8db45c8ee/assets/erratic_valuation_1.png -------------------------------------------------------------------------------- /assets/erratic_valuation_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/will-thompson-k/microstructure-plotter/ce5a1c5326c5709523758c8942a25fd8db45c8ee/assets/erratic_valuation_2.png -------------------------------------------------------------------------------- /assets/erratic_valuation_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/will-thompson-k/microstructure-plotter/ce5a1c5326c5709523758c8942a25fd8db45c8ee/assets/erratic_valuation_3.png -------------------------------------------------------------------------------- /assets/erratic_valuation_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/will-thompson-k/microstructure-plotter/ce5a1c5326c5709523758c8942a25fd8db45c8ee/assets/erratic_valuation_4.png -------------------------------------------------------------------------------- /assets/legend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/will-thompson-k/microstructure-plotter/ce5a1c5326c5709523758c8942a25fd8db45c8ee/assets/legend.png -------------------------------------------------------------------------------- /assets/order_book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/will-thompson-k/microstructure-plotter/ce5a1c5326c5709523758c8942a25fd8db45c8ee/assets/order_book.png -------------------------------------------------------------------------------- /assets/plotter_video.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/will-thompson-k/microstructure-plotter/ce5a1c5326c5709523758c8942a25fd8db45c8ee/assets/plotter_video.gif -------------------------------------------------------------------------------- /assets/tts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/will-thompson-k/microstructure-plotter/ce5a1c5326c5709523758c8942a25fd8db45c8ee/assets/tts.png -------------------------------------------------------------------------------- /assets/zoomed_out_events.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/will-thompson-k/microstructure-plotter/ce5a1c5326c5709523758c8942a25fd8db45c8ee/assets/zoomed_out_events.png -------------------------------------------------------------------------------- /docs/legend/README.md: -------------------------------------------------------------------------------- 1 | # Legend 2 | 3 | Here is a legend of the plot points to expect within a plot: 4 | 5 | ![legend](../../assets/legend.png) 6 | 7 | Note that the plotter can plot both "sim" and "prod" fills on the same plot. The only difference is the color of the plot points. 8 | 9 | To enable the legend in the plotter, set the flag in the constructor. Default is turned off. 10 | 11 | ## Interpretation 12 | 13 | Here are what these plot points mean: 14 | 15 | ### Quote & Trade Data 16 | 17 | - **ask price**: Top-of-book (TOB), "level 1" price of the best offer in the order book. 18 | - **bid price**: Top-of-book (TOB), "level 1" price of the best bid in the order book. 19 | - **micro price**: The "instanteous" price of the asset. Depends on your philosophy: is this "inverse weighted average"? is this "mid price"? Dealer's choice. 20 | - **trade price**: The price of a market trade event or "match" between a market maker and taker. 21 | 22 | ### System Data 23 | 24 | - **val price**: The "valuation" or fair value or predicted future value of the asset at any given time. Subjective. 25 | - **cancel order price**: The price of a cancellation order sent to the exchange. 26 | - **cancel order ack**: The acknowledgement or "ack" from the exchange of a cancellation request. 27 | - **new order price**: The price of a new order sent to the exchange. 28 | - **new order ack**: The acknowledgement or "ack" from the exchange of a new order request. 29 | - **order reject**: The rejection of a request (new, cancel, modify) of an order request. You probably got filled when you didn't want to. 30 | - **aggressive buy price**: The price at which the system was filled for an "aggressive" buy order (i.e. you are the taker). 31 | - **aggressive sell price**: The price at which the system was filled for an "aggressive" sell order (i.e. you are the taker). 32 | - **passive buy price**: The price at which the system was filled for a "passive" buy order (i.e. you are the market maker). 33 | - **passive sell price**: The price at which the system was filled for a "passive" sell order (i.e. you are the market maker). -------------------------------------------------------------------------------- /docs/micro_primer/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Microstructure Primer 3 | 4 | Markets can be decomposed into exchanges and the participants that trade on those exchanges 5 | 6 | ![order book](/assets/order_book.png) 7 | 8 | ## Market Makers: 9 | 10 | **"market makers" or "scalpers" or "liquidity providers":** those offering to provide a quantity needed for a theoretical premium over what they think the security is worth. 11 | 12 | The market makers compete with one another either by providing **better prices**, or **offering more size**, or having **waited around in a queue longer** (time priority), or **paying for the right to match first** (think "free" brokerage companies selling your orders). Their price and quantities are **aggregated** by the exchange to form a **"central limit order book"** (CLOB), which reflect the current overall "best market price" to buy or sell a security at any time. 13 | 14 | ## Takers: 15 | 16 | **"takers"**: those who want a certain amount of a security and are willing to "take" the market makers' price. They could want to get into that position for a number of reasons: 17 | 18 | - they think the _market is mispriced_ (relative to their own view on their longer time horizon) 19 | - they are trying to _hedge a risk_ in their portfolio 20 | - they are executing on behalf of a client 21 | 22 | Of course, most sophisticated players don't fall cleanly into either of these categories. 23 | 24 | Generally, there are _more market makers_ than _takers_ at any given time. 25 | 26 | ## Exchange Software 27 | 28 | The exchange provides the platform (i.e. software) for the makers and takers to meet and trade. In the event that a taker wants to initiate a trade (be the "aggressor"), the exchange has a **matching engine** with a series of rules to determine how much of the taker's order to allocate to each of the market makers' orders (the "passive parties"). _Different matching engines rules incentivize different types of competition among market makers._ 29 | 30 | The exchange software has a number of connected nodes, chief among them: 31 | 32 | - **an order book**: a data structure aggregating the market makers' orders into "the market" for a given security 33 | - **a matching engine**: adds orders to the order book and executes trades between parties, allocating a given taker's order among the market makers 34 | - **a pricefeed**: disseminates quotes, trades, etc - changes in public market state (unless it's a dark pool!) 35 | - **an order gateway**: an API for participants to communicate their price and quantity preferences to the exchange - depending on the exchange, there might be a multitude of order types (particularly if there are restrictions like REG NMS) 36 | 37 | How these participants interact, their impact on market prices, and the way exchange software behave are ✨**market microstructure**✨. 38 | 39 | ## Latency 40 | 41 | Since most of these participants are actually **trading algos** 🤖, the timescales at which all these interactions occur is _quite small_. In many web-based AI applications, **25 milliseconds (ms)** is considered a bleeding edge response time (also accounting for the fact that internet is a very unpredictable and slow medium). For most of these market participants to compete, they need response times of **<1 microsecond (us)** ⏰. 42 | 43 | The pace at which market prices change depends on the regime. If it's 7pm on a Thursday, there may be barely any updates within an hour. If it's [**non-farm payrolls**](https://www.cnbc.com/nonfarm-payrolls/), then there is likely a _ton_ of activity and the exchange is likely overloaded/queuing. 44 | 45 | This means that raw market data is fundamentally **_asynchronous_, _non-uniform_, _event-level_ time series**📈. 46 | 47 | ## Resources 48 | 49 | For a very confusing, vague explanation, read the Wikipedia article [here](https://en.wikipedia.org/wiki/Market_microstructure). -------------------------------------------------------------------------------- /docs/schema/README.md: -------------------------------------------------------------------------------- 1 | # Schema 2 | 3 | Here are the fields and data types expected for each data input. Only the quote data is a required input. All others are optional. 4 | 5 | ## Quote Data 6 | 7 | - **timestamp:** Nanosecond precision EPOCH timestamp INT 8 | - **symbol:** STR 9 | - **bid_price:** FLOAT 10 | - **ask_price:** FLOAT 11 | - **micro_price:** FLOAT 12 | 13 | ## Trade Data 14 | 15 | - **timestamp:** Nanosecond precision EPOCH timestamp INT 16 | - **symbol:** STR 17 | - **price:** FLOAT 18 | 19 | ## Fill Data 20 | 21 | - **timestamp:** Nanosecond precision EPOCH timestamp INT 22 | - **symbol:** STR 23 | - **price:** FLOAT 24 | - **is_buy:** BOOL 25 | - **is_aggressive:** BOOL 26 | 27 | ## Orders Data 28 | 29 | - **timestamp:** Nanosecond precision EPOCH timestamp INT 30 | - **symbol:** STR 31 | - **price:** FLOAT 32 | - **is_new:** BOOL 33 | - **is_cancel:** BOOL 34 | - **is_reject:** BOOL 35 | - **is_ack:** BOOL 36 | 37 | ## Valuation Data 38 | 39 | - **timestamp:** Nanosecond precision EPOCH timestamp INT 40 | - **symbol:** STR 41 | - **theo_price:** FLOAT 42 | 43 | ## Code 44 | 45 | Reference [microplot::schema](/microplot/schema.py). -------------------------------------------------------------------------------- /examples/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/will-thompson-k/microstructure-plotter/ce5a1c5326c5709523758c8942a25fd8db45c8ee/examples/.DS_Store -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Here is a gallery of hand-crafted examples to illustrate the value of such a tool. 4 | 5 | Disclaimer: This data was painstakingly created _by hand_ 🤌 to appear quasi-realistic. I am not an artist 🧑‍🎨 nor is this real 🌎 data. 6 | 7 | You may want to first check out [the legend](/docs/legend/README.md) and perhaps [the 30 second primer on microstructure](/docs/micro_primer/README.md) prior. 8 | 9 | ## Example 10 | 11 | This example contains 3 different "events" of interest commonly found in the wild: 12 | 13 | 1. Event 1: [💸 Erratic valuation](#event-1--erratic-valuation) 14 | 2. Event 2: [😔 Didn't send order](#event-2--didnt-send-order) 15 | 3. Event 3: [🚀 Trade-through-the-stack event](#event-3--trade-through-the-stack-event) 16 | 17 | ![examples](/assets/zoomed_out_events.png) 18 | 19 | 20 | ### Event 1: 💸 Erratic valuation 21 | 22 | Here is an example of a valuation gone amuck and leading to bad trades. 23 | 24 | ![erratic valuation 1](/assets/erratic_valuation_1.png) 25 | 26 | The valuation jumped through the best offer and bid and sent crossing/taking/aggressive orders that were filled. This all happened in <2 seconds. 27 | 28 | **⚠️ IMPORTANT Note ⚠️:** You won't get filled at those prices in reality. The matchine engine will provide "price improvement". I didn't feel like re-working the example. Let us both pretend those fills happened at the best bid and offer instead. 29 | 30 | Zoom in further via right clicking 🖱️ to see further details 🔎. 31 | 32 | ![erratic valuation 2](/assets/erratic_valuation_2.png) 33 | 34 | Here we zoom in further to the milliseconds timescale to see what is happening. 35 | 36 | Zoom in further via right clicking 🖱️ to see further details 🔎. 37 | 38 | ![erratic valuation 3](/assets/erratic_valuation_3.png) 39 | 40 | On the ⏰ _microsecond_ ⏰ timescale, we can start to appreciate what happened.... 41 | 42 | 1. Our 🤖 trading algo's theoretical value spiked through the offer 3 ticks/levels, causing the algo to send cancels for those 3 levels. 43 | 44 | 2. There clearly is also logic for aggressive/taking orders based on this theoretical price. The system sent such a new order and got immediately filled. (Note again it's pratically impossible to get filled at that level and you would have bought a level below.) 45 | 46 | 3. The exchange acks from these requests came back serially, probably due to being sent to the same exchange order router or having an order gateway priority queue internally (this varies by exchange architecture). 47 | 48 | Now looking at the other blip, we see that the same thing happened where the trading algo sold due to a spike down in valuation. 49 | 50 | ![erratic valuation 4](/assets/erratic_valuation_4.png) 51 | 52 | For a refresher on plot items, check out [the legend](/docs/legend/README.md). 53 | 54 | ### Event 2: 😔 Didn't send order 55 | 56 | ![didn't send 1](/assets/didnt_send_1.png) 57 | 58 | In the second event, we see that the trading algo 🤖 was "run over" during a series of trades where both correlated products ticked down. In this situation, a market trade pushed the market down by making the "bid turn offer". This is a common occurence in price point transitions. 59 | 60 | The question is: could have the trading algo 🤖 avoided the fill? Did it try to? 61 | 62 | Zoom in further via right clicking 🖱️ to see further details 🔎. 63 | 64 | ![didn't send 2](/assets/didnt_send_2.png) 65 | 66 | We can also see that the 🤖 trading algo's theoretical valuation for Coin B is responding to _something_ and seems informative (most likely Coin A's pricepoint changes). 67 | 68 | Zoom in further via right clicking 🖱️ to see further details 🔎. 69 | 70 | ![didn't send 3](/assets/didnt_send_3.png) 71 | 72 | Finally, the picture becomes a little clearer. Coin A's market ticked down ("bid turned offer"). We see that the trading algo's valuation in Coin B did in fact reflect this information, yet... the system did not even attempt to send a cancel/modify request (let alone attempt to tag along and cross Coin B). 73 | 74 | This is suspicious, and needs to be investigated further to see if it is a bug 🐛. 75 | 76 | ### Event 3: 🚀 Trade-through-the-stack event 77 | 78 | Finally, we examine the 3rd event, where a very large trade occurs in the market. The trade is so large, it exhausts multiple levels in the order book. This is known as a "trade through the stack" or "sweep" depending on who you ask. 79 | 80 | ![trade through the stack](/assets/tts.png) 81 | 82 | We see that the TTS was first reported in Coin A. The 🤖 trading algo's valuation immediately reflects this information in Coin B's theoretical valuation. 83 | 84 | Responding to this change in valuation, the system attempts to cancel it's offer at the Best Offer and send a New Order several levels through the book (likely, an aggressive order). Notice that the exchange rejected both of these requests and later showed that the system was filled in Coin B. We can see later that a TTS also happened in Coin B, likely by the same aggressor as Coin A. 85 | 86 | Reporting of events can sometimes come in an on odd order depending on the exchange design. However, it looks like the trading algo's behavior was optimal in this situation - it just wasn't able to successfully act. This is just the cost of doing business. 87 | -------------------------------------------------------------------------------- /examples/example_1/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/will-thompson-k/microstructure-plotter/ce5a1c5326c5709523758c8942a25fd8db45c8ee/examples/example_1/.DS_Store -------------------------------------------------------------------------------- /examples/example_1/fake_data/fill_data.csv: -------------------------------------------------------------------------------- 1 | timestamp,symbol,price,is_buy,is_aggressive 2 | 1672543072001007000,COIN_B,10.0,True,False 3 | 1672542962000015000,COIN_B,12.0,True,True 4 | 1672543099002670000,COIN_B,10.0,False,False 5 | 1672542963000005000,COIN_B,9.0,False,True 6 | 7 | -------------------------------------------------------------------------------- /examples/example_1/fake_data/order_data.csv: -------------------------------------------------------------------------------- 1 | timestamp,symbol,price,is_new,is_cancel,is_reject,is_ack 2 | 1672542962000000000,COIN_B,11.0,False,True,False,False 3 | 1672542962000000000,COIN_B,12.0,False,True,False,False 4 | 1672542962000000000,COIN_B,13.0,False,True,False,False 5 | 1672542962000010000,COIN_B,12.0,True,False,False,False 6 | 1672542962000500000,COIN_B,11.0,False,True,False,True 7 | 1672542962000600000,COIN_B,12.0,False,True,False,True 8 | 1672542962000800000,COIN_B,13.0,False,True,False,True 9 | 1672542962000900000,COIN_B,12.0,True,False,False,True 10 | 1672542963000000000,COIN_B,10.0,False,True,False,False 11 | 1672542963000000000,COIN_B,9.0,False,True,False,False 12 | 1672542963000000000,COIN_B,8.0,False,True,False,False 13 | 1672542963000001000,COIN_B,9.0,True,False,False,False 14 | 1672542963000050000,COIN_B,10.0,False,True,False,True 15 | 1672542963000060000,COIN_B,9.0,False,True,False,True 16 | 1672542963000070000,COIN_B,8.0,False,True,False,True 17 | 1672542963000080000,COIN_B,8.0,True,False,False,True 18 | 1672543099002650000,COIN_B,10.0,False,True,False,False 19 | 1672543099002651000,COIN_B,24.0,True,False,False,False 20 | 1672543099002660000,COIN_B,10.0,False,False,True,True 21 | 1672543099002661000,COIN_B,24.0,False,False,True,True 22 | -------------------------------------------------------------------------------- /examples/example_1/fake_data/quote_data.csv: -------------------------------------------------------------------------------- 1 | timestamp,symbol,bid_price,ask_price,micro_price 2 | 1672542960000000000,COIN_A,4.0,5.0,4.5 3 | 1672542961000000000,COIN_B,10.0,11.0,10.5 4 | 1672542971000000000,COIN_A,4.0,5.0,4.5 5 | 1672542972000000000,COIN_A,4.0,5.0,4.5 6 | 1672543072000000000,COIN_A,4.0,5.0,4.5 7 | 1672543072001000000,COIN_A,3.0,4.0,3.5 8 | 1672543072001010000,COIN_B,9.0,10.0,9.5 9 | 1672543072001020000,COIN_B,9.0,10.0,9.5 10 | 1672543072001070000,COIN_B,9.0,10.0,9.5 11 | 1672543072002070000,COIN_A,3.0,4.0,3.5 12 | 1672543072002370000,COIN_A,4.0,5.0,4.5 13 | 1672543072002380000,COIN_B,10.0,11.0,10.5 14 | 1672543072002480000,COIN_A,4.0,5.0,4.5 15 | 1672543072002580000,COIN_A,3.0,4.0,3.5 16 | 1672543072002680000,COIN_B,9.0,10.0,9.5 17 | 1672543082002680000,COIN_A,3.0,4.0,3.5 18 | 1672543087002680000,COIN_B,9.0,10.0,9.5 19 | 1672543099002680000,COIN_A,15.0,16.0,15.5 20 | 1672543099002690000,COIN_B,25.0,26.0,25.5 21 | 1672543099002700000,COIN_A,15.0,16.0,15.5 22 | 1672543099002710000,COIN_B,25.0,26.0,25.5 23 | 1672543099003210000,COIN_A,15.0,16.0,15.5 24 | 1672543099063210000,COIN_B,25.0,26.0,25.5 25 | 1672543099064210000,COIN_A,15.0,16.0,15.5 -------------------------------------------------------------------------------- /examples/example_1/fake_data/trade_data.csv: -------------------------------------------------------------------------------- 1 | timestamp,symbol,price 2 | 1672543071000000000,COIN_A,4.0 3 | 1672543071100000000,COIN_A,4.0 4 | 1672543071200000000,COIN_A,4.0 5 | 1672543071500000000,COIN_A,4.0 6 | 1672543071900000000,COIN_A,4.0 7 | 1672543071910000000,COIN_A,4.0 8 | 1672543071950000000,COIN_A,4.0 9 | 1672543071970000000,COIN_A,4.0 10 | 1672543071990000000,COIN_A,4.0 11 | 1672543072001000000,COIN_A,4.0 12 | 1672543072001001000,COIN_B,10.0 13 | 1672543072001005000,COIN_B,10.0 14 | 1672543072001009000,COIN_B,10.0 15 | 1672543072002170000,COIN_A,4.0 16 | 1672543072002270000,COIN_A,4.0 17 | 1672543072002280000,COIN_B,10.0 18 | 1672543099002650000,COIN_A,4.0 19 | 1672543099002650000,COIN_A,5.0 20 | 1672543099002650000,COIN_A,6.0 21 | 1672543099002650000,COIN_A,7.0 22 | 1672543099002650000,COIN_A,8.0 23 | 1672543099002650000,COIN_A,9.0 24 | 1672543099002650000,COIN_A,10.0 25 | 1672543099002650000,COIN_A,11.0 26 | 1672543099002650000,COIN_A,12.0 27 | 1672543099002650000,COIN_A,13.0 28 | 1672543099002650000,COIN_A,14.0 29 | 1672543099002650000,COIN_A,15.0 30 | 1672543099002670000,COIN_B,10.0 31 | 1672543099002670000,COIN_B,11.0 32 | 1672543099002670000,COIN_B,12.0 33 | 1672543099002670000,COIN_B,13.0 34 | 1672543099002670000,COIN_B,14.0 35 | 1672543099002670000,COIN_B,15.0 36 | 1672543099002670000,COIN_B,16.0 37 | 1672543099002670000,COIN_B,17.0 38 | 1672543099002670000,COIN_B,18.0 39 | 1672543099002670000,COIN_B,19.0 40 | 1672543099002670000,COIN_B,20.0 41 | 1672543099002670000,COIN_B,21.0 42 | 1672543099002670000,COIN_B,22.0 43 | 1672543099002670000,COIN_B,23.0 44 | 1672543099002670000,COIN_B,24.0 45 | 1672543099002670000,COIN_B,25.0 -------------------------------------------------------------------------------- /examples/example_1/fake_data/val_data.csv: -------------------------------------------------------------------------------- 1 | timestamp,symbol,theo_price 2 | 1672542960000000000,COIN_B,10.4 3 | 1672542960010000000,COIN_B,10.25 4 | 1672542961000000000,COIN_B,10.55 5 | 1672542962000000000,COIN_B,13.0 6 | 1672542963000000000,COIN_B,8.0 7 | 1672542964000000000,COIN_B,10.9 8 | 1672542965000000000,COIN_B,10.25 9 | 1672542967000000000,COIN_B,10.6 10 | 1672542968000000000,COIN_B,10.75 11 | 1672542969000000000,COIN_B,10.4 12 | 1672542970000000000,COIN_B,10.6 13 | 1672542971000000000,COIN_B,10.3 14 | 1672542972000000000,COIN_B,10.9 15 | 1672542973000000000,COIN_B,10.3 16 | 1672542974000000000,COIN_B,10.4 17 | 1672542975000000000,COIN_B,10.2 18 | 1672542977000000000,COIN_B,10.9 19 | 1672542978000000000,COIN_B,10.1 20 | 1672542979000000000,COIN_B,10.2 21 | 1672542980000000000,COIN_B,10.6 22 | 1672543030000000000,COIN_B,10.1 23 | 1672543050000000000,COIN_B,10.6 24 | 1672543072000000000,COIN_B,10.1 25 | 1672543072001000000,COIN_B,9.2 26 | 1672543072001010000,COIN_B,9.8 27 | 1672543072001020000,COIN_B,9.1 28 | 1672543072001070000,COIN_B,9.9 29 | 1672543072002070000,COIN_B,9.2 30 | 1672543072002370000,COIN_B,10.5 31 | 1672543072002380000,COIN_B,10.3 32 | 1672543072002480000,COIN_B,10.8 33 | 1672543072002580000,COIN_B,9.5 34 | 1672543072002680000,COIN_B,9.3 35 | 1672543082002680000,COIN_B,9.2 36 | 1672543087002680000,COIN_B,9.1 37 | 1672543099002650000,COIN_B,25.0 38 | 1672543099002670000,COIN_B,25.5 39 | 1672543099002690000,COIN_B,25.5 40 | 1672543099002700000,COIN_B,25.5 41 | 1672543099002710000,COIN_B,25.5 42 | 1672543099003210000,COIN_B,25.5 43 | 1672543099063210000,COIN_B,25.5 44 | 1672543099064210000,COIN_B,25.5 -------------------------------------------------------------------------------- /examples/example_1/plot_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | Interactive microstructure plot example using fake data. Python function call. 3 | """ 4 | 5 | from microplot.scripts.plot_csv import run_plotter_csv 6 | 7 | fake_quote_data_path = "examples/example_1/fake_data/quote_data.csv" 8 | fake_trade_data_path = "examples/example_1/fake_data/trade_data.csv" 9 | fake_fill_data_path = "examples/example_1/fake_data/fill_data.csv" 10 | fake_orders_data_path = "examples/example_1/fake_data/order_data.csv" 11 | fake_valuation_data_path = "examples/example_1/fake_data/val_data.csv" 12 | 13 | args = ["-quote_data_file",fake_quote_data_path,"-trade_data_file",fake_trade_data_path,"-fill_data_sim_file",fake_fill_data_path,"-orders_data_file",fake_orders_data_path,"-valuation_data_file",fake_valuation_data_path] 14 | 15 | if __name__ == "__main__": 16 | run_plotter_csv(args) -------------------------------------------------------------------------------- /examples/example_1/plot_example.sh: -------------------------------------------------------------------------------- 1 | # Interactive microstructure plot example using fake data. 2 | # Bash script. 3 | python microplot/scripts/plot_csv.py -quote_data_file examples/example_1/fake_data/quote_data.csv -trade_data_file examples/example_1/fake_data/trade_data.csv -fill_data_sim_file examples/example_1/fake_data/fill_data.csv -orders_data_file examples/example_1/fake_data/order_data.csv -valuation_data_file examples/example_1/fake_data/val_data.csv -------------------------------------------------------------------------------- /microplot/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=C0111 2 | 3 | __all__ = ["scripts","data","plotter","schema"] 4 | -------------------------------------------------------------------------------- /microplot/data.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the PlotterDataClass for the plotting object. 3 | """ 4 | 5 | from dataclasses import dataclass 6 | import pandas as pd 7 | from typing import Tuple, List 8 | from microplot.schema import ( 9 | QUOTE_DATA_COLUMNS, 10 | TRADE_DATA_COLUMNS, 11 | FILL_DATA_COLUMNS, 12 | ORDERS_DATA_COLUMNS, 13 | VAL_DATA_COLUMNS, 14 | ) 15 | 16 | 17 | @dataclass 18 | class PlotterDataClass: 19 | """ 20 | A dataclass to pass data to the microstructure plotter. 21 | 22 | NOTE: See required fields in each property setter function or the data schema. 23 | 24 | _quote_data (pd.DataFrame): time series of best bid and offer prices and microprice ("weight ave") 25 | _trade_data (pd.DataFrame): time series of trade prices. 26 | _fill_data_sim (pd.DataFrame): time series of system fill data (but for simulations). 27 | _fill_data_prod (pd.DataFrame): time series of system fill data (but for prod). 28 | _orders (pd.DataFrame): time series of exchange order activity. Currently only news, cancels, rejects. 29 | _val_data (pd.DataFrame): time series of system valuation data-> theoretical price, "bid edge", "ask edge" (if market-making). 30 | """ 31 | 32 | _quote_data: pd.DataFrame = None 33 | _trade_data: pd.DataFrame = None 34 | _fill_data_sim: pd.DataFrame = None 35 | _fill_data_prod: pd.DataFrame = None 36 | _orders: pd.DataFrame = None 37 | _val_data: pd.DataFrame = None 38 | 39 | def get_symbols(self) -> List[str]: 40 | """ 41 | Method for grabbing all symbols in dataclass. 42 | 43 | Returns: 44 | List[str]: list of symbols found across all data series. 45 | """ 46 | symbol_set = set() 47 | time_series = [ 48 | self._quote_data, 49 | self.trade_data, 50 | self._fill_data_prod, 51 | self._fill_data_sim, 52 | self._orders, 53 | self._val_data, 54 | ] 55 | for series in time_series: 56 | series_symbols = series["symbol"].ravel() if series is not None else None 57 | if series_symbols is not None: 58 | for symbol in series_symbols: 59 | symbol_set.add(symbol) 60 | 61 | return list(symbol_set) 62 | 63 | @staticmethod 64 | def _validate_timestamps(timeseries: pd.Series) -> bool: 65 | """ 66 | Timestamps should be EPOCH nanosecond precision, integer timestamps. 67 | 68 | This equates to 19 digit ints. 69 | 70 | Args: 71 | ts (pd.Series): pandas Series object containing timestamps. 72 | 73 | Returns: 74 | bool: true/false indicating whether timestamps are valid. 75 | """ 76 | return all(timeseries.apply(lambda x: len(list(str(x))) == 19)) == True 77 | 78 | @staticmethod 79 | def _convert_timestamps(timeseries: pd.Series) -> pd.Series: 80 | """ 81 | Convert timestamps from nanosecond ints -> second-level floats. 82 | 83 | Args: 84 | ts (pd.Series): pandas Series object containing timestamps. 85 | 86 | Returns: 87 | pd.Series: converted timeseries. 88 | """ 89 | return timeseries.apply(lambda x: x * 1e-9) 90 | 91 | @classmethod 92 | def _check_columns(cls, columns: List[str], data: pd.DataFrame, name: str): 93 | """ 94 | Checks a pandas dataframe timeseries that it contains the required fields 95 | prior to setting the value. Also sanity-checks the timestamps are valid. 96 | 97 | Args: 98 | columns (List[str]): Names of required columns for data schema. 99 | data (pd.DataFrame): Pandas dataframe containing time series. 100 | name (str): Name of time series to be set. 101 | 102 | Raises: 103 | Exception: Required column not found. 104 | """ 105 | 106 | assert type(data) is pd.DataFrame 107 | 108 | for column in columns: 109 | if column not in data.columns: 110 | raise Exception(f"column:{column} not in {name}") 111 | 112 | assert "timestamp" in data.columns 113 | 114 | assert cls._validate_timestamps(data["timestamp"]) 115 | 116 | data["timestamp"] = cls._convert_timestamps(data["timestamp"]) 117 | 118 | @property 119 | def quote_data(self) -> pd.DataFrame: 120 | """ 121 | Quote data property getter. 122 | 123 | Returns: 124 | pd.DataFrame: Quote data. 125 | """ 126 | return self._quote_data 127 | 128 | @quote_data.setter 129 | def quote_data(self, data: pd.DataFrame): 130 | """ 131 | Quote data property setter. 132 | 133 | Args: 134 | data (pd.DataFrame): Quote data. 135 | """ 136 | # timestamp, symbol, bid_price, ask_price, micro_price 137 | 138 | self._check_columns(QUOTE_DATA_COLUMNS, data, "quote_data") 139 | self._quote_data = data 140 | 141 | @property 142 | def trade_data(self) -> pd.DataFrame: 143 | """ 144 | Trade data property getter. 145 | 146 | Returns: 147 | pd.DataFrame: Trade data. 148 | """ 149 | return self._trade_data 150 | 151 | @trade_data.setter 152 | def trade_data(self, data: pd.DataFrame): 153 | """ 154 | Trade data property setter. 155 | 156 | Args: 157 | data (pd.DataFrame): Trade data. 158 | """ 159 | # timestamp, symbol, price 160 | 161 | self._check_columns(TRADE_DATA_COLUMNS, data, "trade_data") 162 | self._trade_data = data 163 | 164 | @property 165 | def fill_data_sim( 166 | self, 167 | ) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame]: 168 | """ 169 | Fill data sim property getter. 170 | 171 | Returns: 172 | Tuple[pd.DataFrame,pd.DataFrame,pd.DataFrame,pd.DataFrame]: 173 | [buy_aggressive_fills, buy_passive_fills, sell_aggressive_fills, sell_passive_fills] 174 | """ 175 | 176 | if self._fill_data_sim is None: 177 | return None, None, None, None 178 | 179 | buy_aggressive_fills = self._fill_data_sim[ 180 | self._fill_data_sim.is_buy & self._fill_data_sim.is_aggressive 181 | ] 182 | buy_passive_fills = self._fill_data_sim[ 183 | self._fill_data_sim.is_buy & ~self._fill_data_sim.is_aggressive 184 | ] 185 | sell_aggressive_fills = self._fill_data_sim[ 186 | ~self._fill_data_sim.is_buy & self._fill_data_sim.is_aggressive 187 | ] 188 | sell_passive_fills = self._fill_data_sim[ 189 | ~self._fill_data_sim.is_buy & ~self._fill_data_sim.is_aggressive 190 | ] 191 | 192 | return ( 193 | buy_aggressive_fills, 194 | buy_passive_fills, 195 | sell_aggressive_fills, 196 | sell_passive_fills, 197 | ) 198 | 199 | @fill_data_sim.setter 200 | def fill_data_sim(self, data: pd.DataFrame): 201 | """ 202 | Fill data property setter. 203 | 204 | Args: 205 | data (pd.DataFrame): Fill data. 206 | """ 207 | # timestamp, symbol, price, is_buy, is_aggressive 208 | 209 | self._check_columns(FILL_DATA_COLUMNS, data, "fill_data_sim") 210 | self._fill_data_sim = data 211 | 212 | @property 213 | def fill_data_prod( 214 | self, 215 | ) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame]: 216 | """ 217 | Fill data prod property getter. 218 | 219 | Returns: 220 | Tuple[pd.DataFrame,pd.DataFrame,pd.DataFrame,pd.DataFrame]: 221 | [buy_aggressive_fills, buy_passive_fills, sell_aggressive_fills, sell_passive_fills] 222 | """ 223 | if self._fill_data_prod is None: 224 | return None, None, None, None 225 | 226 | buy_aggressive_fills = self._fill_data_prod[ 227 | self._fill_data_prod.is_buy & self._fill_data_prod.is_aggressive 228 | ] 229 | buy_passive_fills = self._fill_data_prod[ 230 | self._fill_data_prod.is_buy & ~self._fill_data_prod.is_aggressive 231 | ] 232 | sell_aggressive_fills = self._fill_data_prod[ 233 | ~self._fill_data_prod.is_buy & self._fill_data_prod.is_aggressive 234 | ] 235 | sell_passive_fills = self._fill_data_prod[ 236 | ~self._fill_data_prod.is_buy & ~self._fill_data_prod.is_aggressive 237 | ] 238 | 239 | return ( 240 | buy_aggressive_fills, 241 | buy_passive_fills, 242 | sell_aggressive_fills, 243 | sell_passive_fills, 244 | ) 245 | 246 | 247 | @fill_data_prod.setter 248 | def fill_data_prod(self, data: pd.DataFrame): 249 | """ 250 | Fill data property setter. 251 | 252 | Args: 253 | data (pd.DataFrame): Fill data. 254 | """ 255 | # timestamp, symbol, price, is_buy, is_aggressive 256 | 257 | self._check_columns(FILL_DATA_COLUMNS, data, "fill_data_prod") 258 | self._fill_data_prod = data 259 | 260 | @property 261 | def orders( 262 | self, 263 | ) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame]: 264 | """ 265 | Order data property getter. 266 | 267 | Returns: 268 | Tuple[pd.DataFrame,pd.DataFrame,pd.DataFrame,pd.DataFrame,pd.DataFrame]: 269 | [new_orders,new_order_acks,cancel_orders,cancel_order_acks, rejects] 270 | """ 271 | # new, new_ack, cxl, cxl_ack, rej 272 | # TODO: add modifies and all the other types 273 | 274 | if self._orders is None: 275 | return None, None, None, None, None 276 | 277 | new_orders = self._orders[self._orders.is_new & ~self._orders.is_ack] 278 | new_order_acks = self._orders[self._orders.is_new & self._orders.is_ack] 279 | cancel_orders = self._orders[self._orders.is_cancel & ~self._orders.is_ack] 280 | cancel_orders_acks = self._orders[self._orders.is_cancel & self._orders.is_ack] 281 | rejects = self._orders[self._orders.is_reject] 282 | 283 | return new_orders, new_order_acks, cancel_orders, cancel_orders_acks, rejects 284 | 285 | @orders.setter 286 | def orders(self, data: pd.DataFrame): 287 | """ 288 | Order data property setter. 289 | 290 | Args: 291 | data (pd.DataFrame): Order data. 292 | """ 293 | # timestamp, symbol, price, is_new, is_cancel, is_reject, is_ack 294 | 295 | self._check_columns(ORDERS_DATA_COLUMNS, data, "orders") 296 | self._orders = data 297 | 298 | @property 299 | def val_data(self) -> pd.DataFrame: 300 | """ 301 | Valuation data property getter. 302 | 303 | Returns: 304 | pd.DataFrame: valuation data. 305 | """ 306 | return self._val_data 307 | 308 | 309 | @val_data.setter 310 | def val_data(self, data: pd.DataFrame): 311 | """ 312 | Valuation data property setter. 313 | 314 | Args: 315 | data (pd.DataFrame): Val data. 316 | """ 317 | # timestamp, symbol, theo_price 318 | 319 | self._check_columns(VAL_DATA_COLUMNS, data, "val_data") 320 | self._val_data = data -------------------------------------------------------------------------------- /microplot/plotter.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the microstructure plotter. 3 | """ 4 | 5 | from enable.api import ComponentEditor 6 | from traits.api import HasTraits, Instance 7 | from traitsui.api import Handler, Item, View 8 | 9 | from chaco.scales.api import CalendarScaleSystem 10 | from chaco.scales_tick_generator import ScalesTickGenerator 11 | from chaco.tools.api import PanTool, ZoomTool 12 | from chaco.api import ArrayPlotData, Plot, PlotAxis, PlotGrid, VPlotContainer 13 | 14 | from microplot.data import PlotterDataClass 15 | 16 | 17 | class DummyPlotterHandler(Handler): 18 | """ 19 | This is a "dummy" plotter handler class. 20 | This forces a silent close of the plotter. 21 | """ 22 | 23 | def closed(self, info, is_ok): 24 | return 25 | 26 | 27 | class MicroPlotter(HasTraits): 28 | """ 29 | This is the microstructure plotting class. 30 | """ 31 | 32 | container = Instance(VPlotContainer) 33 | 34 | traits_view = View( 35 | Item("container", editor=ComponentEditor(), show_label=False), 36 | width=900, 37 | height=500, 38 | resizable=True, 39 | title="Microstructure Plotter", 40 | handler=DummyPlotterHandler, 41 | ) 42 | 43 | def __init__(self, data: PlotterDataClass, show_legend: bool = False): 44 | """ 45 | Args: 46 | data (PlotterDataClass): dataclass for plotting object. 47 | show_legend (bool): flag to show legend on plots. Crowded image. Default = False. 48 | """ 49 | 50 | # plotterdataclass ingested from datasource 51 | self._data = data 52 | # flag to show legend in plots (warning: crowds screen) 53 | self._show_legend = show_legend 54 | # find union of symbols across time series 55 | self._symbols = data.get_symbols() 56 | 57 | # to cache Plot objects 58 | self._subplots = [] 59 | # to be used later for connecting plots 60 | self._top_plot_indexer = None 61 | self._top_plot_index_range = None 62 | self._top_plot_bottom_axis = None 63 | 64 | super().__init__() 65 | 66 | def plot(self, *args, **kws): 67 | """ 68 | One stop "public" method for rendering all plots. 69 | 70 | Call this after instantiating object to render plots. 71 | 72 | Raises: 73 | Exception: HasTraits:: unable to configure. 74 | """ 75 | 76 | try: 77 | super().configure_traits(*args, **kws) 78 | except: 79 | raise Exception("Unable to configure traits.") 80 | 81 | def _container_default(self) -> VPlotContainer: 82 | """ 83 | Required method for HasTraits class to return a 84 | "Vertical Plot Container" object containing 85 | the plots of interest. 86 | 87 | Returns: 88 | VPlotContainer: Container object with 89 | the subplots of interest. 90 | """ 91 | 92 | return self._generate_subplots() 93 | 94 | def _generate_subplots(self) -> VPlotContainer: 95 | """ 96 | Method for instantiating the VPlotContainer. 97 | Creates subplot Plot objects for each symbol. 98 | 99 | Returns: 100 | VPlotContainer: Container object with 101 | the subplots of interest. 102 | """ 103 | 104 | # run through each symbol, instantiate subplots 105 | for symbol in self._symbols: 106 | plot = self._generate_subplot(symbol) 107 | self._link_subplot(plot) 108 | 109 | # instantiate a container to hold all the plots 110 | container = VPlotContainer(bgcolor="transparent") 111 | 112 | # add all the linked sub-plots to the VPlotContainer 113 | for plot in self._subplots: 114 | container.add(plot) 115 | # clear cache 116 | self._subplots = [] 117 | 118 | return container 119 | 120 | def _generate_subplot(self, symbol: str) -> Plot: 121 | """ 122 | For a given symbol, instantiate a subplot 123 | timeseries with all the relevant plot datapoints. 124 | 125 | Args: 126 | symbol (str): The symbol of interest. 127 | 128 | Returns: 129 | Plot: The subplot object for a given symbol. 130 | """ 131 | # set plot data 132 | data_array = self._set_plot_data_array(symbol) 133 | plot_attributes = set(data_array.list_data()) 134 | 135 | # instantiate plot object 136 | plot = Plot(data_array, auto_grid=False, auto_axis=False) 137 | 138 | # render plots 139 | self._render_plots(plot, plot_attributes) 140 | 141 | # setup plot stuff 142 | self._setup_plot(plot, symbol) 143 | 144 | return plot 145 | 146 | def _setup_plot(self, plot: Plot, symbol: str): 147 | """ 148 | Setup Plot attributes (zooming, etc). 149 | 150 | Args: 151 | plot (Plot): The Plot class (for a given symbol). 152 | symbol (str): The symbol of interest. 153 | """ 154 | 155 | # setup plot details 156 | plot.title = "Symbol: %s " % (symbol) 157 | plot.legend.visible = self._show_legend 158 | 159 | # create pan tool 160 | plot.tools.append(PanTool(plot)) 161 | # create zooming ability 162 | zoom_controller = ZoomTool( 163 | component=plot, tool_mode="box", drag_button="right", always_on=True 164 | ) 165 | plot.overlays.append(zoom_controller) 166 | # setup plot axis 167 | left_axis = PlotAxis(plot, orientation="left") 168 | plot.overlays.append(left_axis) 169 | # setup plot grid 170 | h_mapper = plot.value_mapper 171 | h_grid = PlotGrid( 172 | mapper=h_mapper, 173 | orientation="horizontal", 174 | component=plot, 175 | line_color="lightgray", 176 | line_style="dot", 177 | ) 178 | plot.underlays.append(h_grid) 179 | 180 | def _render_plots(self, plot: Plot, plot_attributes: set): 181 | """ 182 | Render Plot elements within instantiated class. Set title and other properties. 183 | 184 | Args: 185 | plot (Plot): The Plot class (for a given symbol). 186 | plot_attributes (set): plot attributes set in data_array object. 187 | """ 188 | # quote_data 189 | if "quote_timestamp" in plot_attributes: 190 | plot.plot( 191 | ("quote_timestamp", "quote_bid_price"), 192 | line_width=4, 193 | name="bid_price", 194 | color="black", 195 | line_style="solid", 196 | render_style="connectedhold", 197 | ) 198 | plot.plot( 199 | ("quote_timestamp", "quote_ask_price"), 200 | line_width=4, 201 | name="ask_price", 202 | color="black", 203 | line_style="solid", 204 | render_style="connectedhold", 205 | ) 206 | plot.plot( 207 | ("quote_timestamp", "quote_micro_price"), 208 | line_width=1, 209 | name="micro_price", 210 | color="blue", 211 | line_style="solid", 212 | render_style="connectedhold", 213 | ) 214 | 215 | # trade_data 216 | if "trade_timestamp" in plot_attributes: 217 | plot.plot( 218 | ("trade_timestamp", "trade_price"), 219 | type="scatter", 220 | marker="circle", 221 | name="trade_price", 222 | color="red", 223 | marker_size=8, 224 | render_style="hold", 225 | ) 226 | 227 | # aggr_buy_sim 228 | if "sim_aggr_buy_timestamp" in plot_attributes: 229 | plot.plot( 230 | ("sim_aggr_buy_timestamp", "sim_aggr_buy_price"), 231 | type="scatter", 232 | marker="triangle", 233 | name="sim_aggr_buy_price", 234 | color="blue", 235 | marker_size=12, 236 | render_style="hold", 237 | ) 238 | 239 | # pass_buy_sim 240 | if "sim_pass_buy_timestamp" in plot_attributes: 241 | plot.plot( 242 | ("sim_pass_buy_timestamp", "sim_pass_buy_price"), 243 | type="scatter", 244 | marker="circle", 245 | name="sim_pass_buy_price", 246 | color="blue", 247 | marker_size=12, 248 | render_style="hold", 249 | ) 250 | 251 | # aggr_sell_sim 252 | if "sim_aggr_sell_timestamp" in plot_attributes: 253 | plot.plot( 254 | ("sim_aggr_sell_timestamp", "sim_aggr_sell_price"), 255 | type="scatter", 256 | marker="triangle", 257 | name="sim_aggr_sell_price", 258 | color="gold", 259 | marker_size=12, 260 | render_style="hold", 261 | ) 262 | 263 | # pass_sell_sim 264 | if "sim_pass_sell_timestamp" in plot_attributes: 265 | plot.plot( 266 | ("sim_pass_sell_timestamp", "sim_pass_sell_price"), 267 | type="scatter", 268 | marker="circle", 269 | name="sim_pass_sell_price", 270 | color="gold", 271 | marker_size=12, 272 | render_style="hold", 273 | ) 274 | 275 | # aggr_buy_prod 276 | if "prod_aggr_buy_timestamp" in plot_attributes: 277 | plot.plot( 278 | ("prod_aggr_buy_timestamp", "prod_aggr_buy_price"), 279 | type="scatter", 280 | marker="triangle", 281 | name="prod_aggr_buy_price", 282 | color="purple", 283 | marker_size=12, 284 | render_style="hold", 285 | ) 286 | 287 | # pass_buy_prod 288 | if "prod_pass_buy_timestamp" in plot_attributes: 289 | plot.plot( 290 | ("prod_pass_buy_timestamp", "prod_pass_buy_price"), 291 | type="scatter", 292 | marker="circle", 293 | name="prod_pass_buy_price", 294 | color="purple", 295 | marker_size=12, 296 | render_style="hold", 297 | ) 298 | 299 | # aggr_sell_prod 300 | if "prod_aggr_sell_timestamp" in plot_attributes: 301 | plot.plot( 302 | ("prod_aggr_sell_timestamp", "prod_aggr_sell_price"), 303 | type="scatter", 304 | marker="triangle", 305 | name="prod_aggr_sell_price", 306 | color="green", 307 | marker_size=12, 308 | render_style="hold", 309 | ) 310 | 311 | # pass_sell_prod 312 | if "prod_pass_sell_timestamp" in plot_attributes: 313 | plot.plot( 314 | ("prod_pass_sell_timestamp", "prod_pass_sell_price"), 315 | type="scatter", 316 | marker="circle", 317 | name="prod_pass_sell_price", 318 | color="green", 319 | marker_size=12, 320 | render_style="hold", 321 | ) 322 | 323 | # new_orders 324 | if "new_order_timestamp" in plot_attributes: 325 | plot.plot( 326 | ("new_order_timestamp", "new_order_price"), 327 | type="scatter", 328 | marker="diamond", 329 | name="new_order_price", 330 | color="green", 331 | marker_size=6, 332 | render_style="hold", 333 | ) 334 | 335 | # new_order_acks 336 | if "new_order_ack_timestamp" in plot_attributes: 337 | plot.plot( 338 | ("new_order_ack_timestamp", "new_order_ack_price"), 339 | type="scatter", 340 | marker="diamond", 341 | name="new_order_ack_price", 342 | color="lightgreen", 343 | marker_size=6, 344 | render_style="hold", 345 | ) 346 | 347 | # cancel_orders 348 | if "cancel_order_timestamp" in plot_attributes: 349 | plot.plot( 350 | ("cancel_order_timestamp", "cancel_order_price"), 351 | type="scatter", 352 | marker="diamond", 353 | name="cancel_order_price", 354 | color="red", 355 | marker_size=6, 356 | render_style="hold", 357 | ) 358 | 359 | # cancel_order_acks 360 | if "cancel_order_ack_timestamp" in plot_attributes: 361 | plot.plot( 362 | ("cancel_order_ack_timestamp", "cancel_order_ack_price"), 363 | type="scatter", 364 | marker="diamond", 365 | name="cancel_order_ack_price", 366 | color="pink", 367 | marker_size=6, 368 | render_style="hold", 369 | ) 370 | 371 | # rejects 372 | if "reject_orders_timestamp" in plot_attributes: 373 | plot.plot( 374 | ("reject_orders_timestamp", "reject_orders_price"), 375 | type="scatter", 376 | marker="diamond", 377 | name="reject_orders_price", 378 | color="black", 379 | marker_size=6, 380 | render_style="hold", 381 | ) 382 | 383 | # val_data 384 | if "val_data_timestamp" in plot_attributes: 385 | plot.plot( 386 | ("val_data_timestamp", "val_data_price"), 387 | color="green", 388 | line_style="solid", 389 | name="val_data_price", 390 | line_width=1, 391 | render_style="connectedhold", 392 | ) 393 | 394 | def _set_plot_data_array(self, symbol: str) -> ArrayPlotData: 395 | """ 396 | Instantiate ArrayPlotData class for Plot object. 397 | 398 | Args: 399 | symbol (str): The symbol of interest. 400 | 401 | Returns: 402 | ArrayPlotData: The array plot data class with required data fields set. 403 | """ 404 | 405 | array_plot_data = ArrayPlotData() 406 | 407 | # quote data 408 | quote_data = self._data.quote_data 409 | if quote_data is not None: 410 | quote_data = self._data.quote_data[ 411 | self._data.quote_data["symbol"] == symbol 412 | ] 413 | array_plot_data.set_data("quote_timestamp", quote_data.timestamp.ravel()) 414 | array_plot_data.set_data("quote_bid_price", quote_data.bid_price.ravel()) 415 | array_plot_data.set_data("quote_ask_price", quote_data.ask_price.ravel()) 416 | array_plot_data.set_data( 417 | "quote_micro_price", quote_data.micro_price.ravel() 418 | ) 419 | 420 | # trade data 421 | trade_data = self._data.trade_data 422 | if trade_data is not None: 423 | trade_data = self._data.trade_data[ 424 | self._data.trade_data["symbol"] == symbol 425 | ] 426 | array_plot_data.set_data("trade_timestamp", trade_data.timestamp.ravel()) 427 | array_plot_data.set_data("trade_price", trade_data.price.ravel()) 428 | 429 | # fill data - sim 430 | ( 431 | buy_aggressive_fills, 432 | buy_passive_fills, 433 | sell_aggressive_fills, 434 | sell_passive_fills, 435 | ) = self._data.fill_data_sim 436 | # aggr_buy 437 | if not (buy_aggressive_fills is None or buy_aggressive_fills.empty): 438 | buy_aggressive_fills = buy_aggressive_fills[ 439 | buy_aggressive_fills["symbol"] == symbol 440 | ] 441 | array_plot_data.set_data( 442 | "sim_aggr_buy_timestamp", buy_aggressive_fills.timestamp.ravel() 443 | ) 444 | array_plot_data.set_data( 445 | "sim_aggr_buy_price", buy_aggressive_fills.price.ravel() 446 | ) 447 | # pass_buy 448 | if not (buy_passive_fills is None or buy_passive_fills.empty): 449 | buy_passive_fills = buy_passive_fills[buy_passive_fills["symbol"] == symbol] 450 | array_plot_data.set_data( 451 | "sim_pass_buy_timestamp", buy_passive_fills.timestamp.ravel() 452 | ) 453 | array_plot_data.set_data( 454 | "sim_pass_buy_price", buy_passive_fills.price.ravel() 455 | ) 456 | # aggr_sell 457 | if not (sell_aggressive_fills is None or sell_aggressive_fills.empty): 458 | sell_aggressive_fills = sell_aggressive_fills[ 459 | sell_aggressive_fills["symbol"] == symbol 460 | ] 461 | array_plot_data.set_data( 462 | "sim_aggr_sell_timestamp", sell_aggressive_fills.timestamp.ravel() 463 | ) 464 | array_plot_data.set_data( 465 | "sim_aggr_sell_price", sell_aggressive_fills.price.ravel() 466 | ) 467 | # pass_sell 468 | if not (sell_passive_fills is None or sell_passive_fills.empty): 469 | sell_passive_fills = sell_passive_fills[ 470 | sell_passive_fills["symbol"] == symbol 471 | ] 472 | array_plot_data.set_data( 473 | "sim_pass_sell_timestamp", sell_passive_fills.timestamp.ravel() 474 | ) 475 | array_plot_data.set_data( 476 | "sim_pass_sell_price", sell_passive_fills.price.ravel() 477 | ) 478 | 479 | # fill data - prod 480 | ( 481 | buy_aggressive_fills, 482 | buy_passive_fills, 483 | sell_aggressive_fills, 484 | sell_passive_fills, 485 | ) = self._data.fill_data_prod 486 | # aggr_buy 487 | if not (buy_aggressive_fills is None or buy_aggressive_fills.empty): 488 | buy_aggressive_fills = buy_aggressive_fills[ 489 | buy_aggressive_fills["symbol"] == symbol 490 | ] 491 | array_plot_data.set_data( 492 | "prod_aggr_buy_timestamp", buy_aggressive_fills.timestamp.ravel() 493 | ) 494 | array_plot_data.set_data( 495 | "prod_aggr_buy_price", buy_aggressive_fills.price.ravel() 496 | ) 497 | # pass_buy 498 | if not (buy_passive_fills is None or buy_passive_fills.empty): 499 | buy_passive_fills = buy_passive_fills[buy_passive_fills["symbol"] == symbol] 500 | array_plot_data.set_data( 501 | "prod_pass_buy_timestamp", buy_passive_fills.timestamp.ravel() 502 | ) 503 | array_plot_data.set_data( 504 | "prod_pass_buy_price", buy_passive_fills.price.ravel() 505 | ) 506 | # aggr_sell 507 | if not (sell_aggressive_fills is None or sell_aggressive_fills.empty): 508 | sell_aggressive_fills = sell_aggressive_fills[ 509 | sell_aggressive_fills["symbol"] == symbol 510 | ] 511 | array_plot_data.set_data( 512 | "prod_aggr_sell_timestamp", sell_aggressive_fills.timestamp.ravel() 513 | ) 514 | array_plot_data.set_data( 515 | "prod_aggr_sell_price", sell_aggressive_fills.price.ravel() 516 | ) 517 | # pass_sell 518 | if not (sell_passive_fills is None or sell_passive_fills.empty): 519 | sell_passive_fills = sell_passive_fills[ 520 | sell_passive_fills["symbol"] == symbol 521 | ] 522 | array_plot_data.set_data( 523 | "prod_pass_sell_timestamp", sell_passive_fills.timestamp.ravel() 524 | ) 525 | array_plot_data.set_data( 526 | "prod_pass_sell_price", sell_passive_fills.price.ravel() 527 | ) 528 | 529 | # orders 530 | self._data.orders 531 | ( 532 | new_orders, 533 | new_order_acks, 534 | cancel_orders, 535 | cancel_order_acks, 536 | rejects, 537 | ) = self._data.orders 538 | # new orders 539 | if not (new_orders is None or new_orders.empty): 540 | new_orders = new_orders[new_orders["symbol"] == symbol] 541 | array_plot_data.set_data( 542 | "new_order_timestamp", new_orders.timestamp.ravel() 543 | ) 544 | array_plot_data.set_data("new_order_price", new_orders.price.ravel()) 545 | # new orders - ack 546 | if not (new_order_acks is None or new_order_acks.empty): 547 | new_order_acks = new_order_acks[new_order_acks["symbol"] == symbol] 548 | array_plot_data.set_data( 549 | "new_order_ack_timestamp", new_order_acks.timestamp.ravel() 550 | ) 551 | array_plot_data.set_data( 552 | "new_order_ack_price", new_order_acks.price.ravel() 553 | ) 554 | # cancels 555 | if not (cancel_orders is None or cancel_orders.empty): 556 | cancel_orders = cancel_orders[cancel_orders["symbol"] == symbol] 557 | array_plot_data.set_data( 558 | "cancel_order_timestamp", cancel_orders.timestamp.ravel() 559 | ) 560 | array_plot_data.set_data("cancel_order_price", cancel_orders.price.ravel()) 561 | # cancels - ack 562 | if not (cancel_order_acks is None or cancel_order_acks.empty): 563 | cancel_order_acks = cancel_order_acks[cancel_order_acks["symbol"] == symbol] 564 | array_plot_data.set_data( 565 | "cancel_order_ack_timestamp", cancel_order_acks.timestamp.ravel() 566 | ) 567 | array_plot_data.set_data( 568 | "cancel_order_ack_price", cancel_order_acks.price.ravel() 569 | ) 570 | # rejects 571 | if not (rejects is None or rejects.empty): 572 | rejects = rejects[rejects["symbol"] == symbol] 573 | array_plot_data.set_data( 574 | "reject_orders_timestamp", rejects.timestamp.ravel() 575 | ) 576 | array_plot_data.set_data("reject_orders_price", rejects.price.ravel()) 577 | 578 | # val data 579 | val_data = self._data.val_data 580 | if val_data is not None: 581 | val_data = self._data.val_data[self._data.val_data["symbol"] == symbol] 582 | array_plot_data.set_data("val_data_timestamp", val_data.timestamp.ravel()) 583 | array_plot_data.set_data("val_data_price", val_data.theo_price.ravel()) 584 | 585 | return array_plot_data 586 | 587 | def _link_subplot(self, plot: Plot): 588 | """ 589 | Link a subplot object to others via PlotAxis object. 590 | 591 | Args: 592 | plot (Plot): The subplot Plot object. 593 | """ 594 | 595 | if self._top_plot_indexer is None: 596 | # add a calendar-based timescale on the x-axis 597 | bottom_axis = PlotAxis( 598 | plot, 599 | orientation="bottom", 600 | tick_generator=ScalesTickGenerator(scale=CalendarScaleSystem()), 601 | ) 602 | plot.underlays.append(bottom_axis) 603 | # persist plot::indexer and plotaxis - need indices and tick_generator to link axes later 604 | self._top_plot_indexer = plot.index_mapper 605 | self._top_plot_index_range = plot.index_range 606 | self._top_plot_bottom_axis = bottom_axis 607 | else: 608 | bottom_axis = PlotAxis( 609 | plot, 610 | orientation="bottom", 611 | # link the first plot's index mapper 612 | mapper=self._top_plot_indexer, 613 | tick_generator=ScalesTickGenerator(scale=CalendarScaleSystem()), 614 | ) 615 | # link other attributes 616 | bottom_axis.tick_generator = self._top_plot_bottom_axis.tick_generator 617 | plot.index_range = self._top_plot_index_range 618 | plot.underlays.append(bottom_axis) 619 | 620 | # store to be added to VPlot later 621 | self._subplots.append(plot) 622 | -------------------------------------------------------------------------------- /microplot/schema.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the column names ("data schema") for the required inputs. 3 | """ 4 | 5 | QUOTE_DATA_COLUMNS = ["timestamp", "symbol", "bid_price", "ask_price", "micro_price"] 6 | TRADE_DATA_COLUMNS = ["timestamp", "symbol", "price"] 7 | FILL_DATA_COLUMNS = ["timestamp", "symbol", "price", "is_buy", "is_aggressive"] 8 | ORDERS_DATA_COLUMNS = [ 9 | "timestamp", 10 | "symbol", 11 | "price", 12 | "is_new", 13 | "is_cancel", 14 | "is_reject", 15 | "is_ack", 16 | ] 17 | VAL_DATA_COLUMNS = ["timestamp", "symbol", "theo_price"] 18 | -------------------------------------------------------------------------------- /microplot/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=C0111 2 | 3 | __all__ = ["plot_csv"] 4 | -------------------------------------------------------------------------------- /microplot/scripts/plot_csv.py: -------------------------------------------------------------------------------- 1 | """ 2 | Command-line script to load a csv and generate a microstructure plot. 3 | """ 4 | 5 | from microplot.data import PlotterDataClass 6 | from microplot.plotter import MicroPlotter 7 | from microplot.schema import ( 8 | QUOTE_DATA_COLUMNS, 9 | TRADE_DATA_COLUMNS, 10 | FILL_DATA_COLUMNS, 11 | ORDERS_DATA_COLUMNS, 12 | VAL_DATA_COLUMNS, 13 | ) 14 | import logging 15 | import argparse 16 | import os.path 17 | import pandas as pd 18 | import sys 19 | from typing import Any, List 20 | 21 | def _check_file_path(file_path: str) -> bool: 22 | """ 23 | Check the file_path before trying to open. 24 | 25 | Args: 26 | file_path (str): file path. 27 | 28 | Returns: 29 | bool: Boolean whether file path is valid. 30 | """ 31 | 32 | if not file_path.endswith(".csv"): 33 | return False 34 | 35 | return os.path.isfile(file_path) 36 | 37 | def _check(file_path: str): 38 | """ 39 | Red-face test the file. 40 | 41 | Args: 42 | file_path (str): file path. 43 | 44 | Raises: 45 | Exception: file path either does not exist or is not a csv. 46 | """ 47 | 48 | if not _check_file_path(file_path): 49 | raise Exception(f"{file_path} either does not exist or is not a csv file.") 50 | 51 | 52 | def run_plotter_csv(command_args:List[Any]): 53 | """ 54 | Main function for creating plotter via csv. 55 | 56 | Args: 57 | command_args (List[Any]): command-line args. 58 | """ 59 | # logger 60 | logger = logging.getLogger("microplotter") 61 | logger.setLevel(logging.INFO) 62 | # console handler 63 | console_handler = logging.StreamHandler() 64 | console_handler.setLevel(logging.INFO) 65 | # formatter 66 | formatter = logging.Formatter( 67 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 68 | ) 69 | # add formatter to ch 70 | console_handler.setFormatter(formatter) 71 | # add ch to logger 72 | logger.addHandler(console_handler) 73 | 74 | parser = argparse.ArgumentParser() 75 | parser.add_argument( 76 | "-quote_data_file", 77 | "--quote_data_file", 78 | help="[Required] csv containing market quotes", 79 | type=str, 80 | required=True 81 | ) 82 | parser.add_argument( 83 | "-trade_data_file", 84 | "--trade_data_file", 85 | help="csv containing market trades", 86 | type=str, 87 | required=False 88 | ) 89 | parser.add_argument( 90 | "-fill_data_sim_file", 91 | "--fill_data_sim_file", 92 | help="csv containing fill data from sims", 93 | type=str, 94 | required=False 95 | ) 96 | parser.add_argument( 97 | "-fill_data_prod_file", 98 | "--fill_data_prod_file", 99 | help="csv containing fill data from production", 100 | type=str, 101 | required=False 102 | ) 103 | parser.add_argument( 104 | "-orders_data_file", 105 | "--orders_data_file", 106 | help="csv containing order data", 107 | type=str, 108 | required=False 109 | ) 110 | parser.add_argument( 111 | "-valuation_data_file", 112 | "--valuation_data_file", 113 | help="csv containing valuation data", 114 | type=str, 115 | required=False 116 | ) 117 | # read in command-line args 118 | args = parser.parse_args(command_args) 119 | 120 | # data class 121 | data = PlotterDataClass() 122 | 123 | # read in quote data 124 | if args.quote_data_file is not None: 125 | _check(args.quote_data_file) 126 | logger.info("Reading in quote data....") 127 | with open(args.quote_data_file) as file: 128 | data.quote_data = pd.read_csv(file, usecols=QUOTE_DATA_COLUMNS) 129 | logger.info("Done") 130 | 131 | # read in trade data 132 | if args.trade_data_file is not None: 133 | _check(args.trade_data_file) 134 | logger.info("Reading in trade data....") 135 | with open(args.trade_data_file) as file: 136 | data.trade_data = pd.read_csv(file, usecols=TRADE_DATA_COLUMNS) 137 | logger.info("Done") 138 | 139 | # read in fill data -- sim 140 | if args.fill_data_sim_file is not None: 141 | _check(args.fill_data_sim_file) 142 | logger.info("Reading in fill data sim....") 143 | with open(args.fill_data_sim_file) as file: 144 | data.fill_data_sim = pd.read_csv(file, usecols=FILL_DATA_COLUMNS) 145 | logger.info("Done") 146 | 147 | # read in fill data -- prod 148 | if args.fill_data_prod_file is not None: 149 | _check(args.fill_data_prod_file) 150 | logger.info("Reading in fill data prod....") 151 | with open(args.fill_data_prod_file) as file: 152 | data.fill_data_prod = pd.read_csv(file, usecols=FILL_DATA_COLUMNS) 153 | logger.info("Done") 154 | 155 | # read in orders 156 | if args.orders_data_file is not None: 157 | _check(args.orders_data_file) 158 | logger.info("Reading in orders data....") 159 | with open(args.orders_data_file) as file: 160 | data.orders = pd.read_csv(file, usecols=ORDERS_DATA_COLUMNS) 161 | logger.info("Done") 162 | 163 | # read in valuation data 164 | if args.valuation_data_file is not None: 165 | _check(args.valuation_data_file) 166 | logger.info("Reading in valuation data....") 167 | with open(args.valuation_data_file) as file: 168 | data.val_data = pd.read_csv(file, usecols=VAL_DATA_COLUMNS) 169 | logger.info("Done") 170 | 171 | # plotter 172 | logger.info("Creating plotter....") 173 | plotter = MicroPlotter(data) 174 | logger.info("Done") 175 | # call plot() method 176 | logger.info("Rendering plots....") 177 | plotter.plot() 178 | logger.info("Done") 179 | 180 | 181 | if __name__ == "__main__": 182 | # set up this way to be able to import function 183 | run_plotter_csv(sys.argv[1:]) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | chaco==5.1.0 2 | enable==5.3.1 3 | pandas==1.3.5 4 | setuptools==61.2.0 5 | traits==6.4.1 6 | traitsui==7.4.2 7 | pyqt -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | setup.py 3 | """ 4 | from setuptools import setup 5 | 6 | setup( 7 | name='microstructure-plotter', 8 | version='0.1', 9 | packages=['microplot', 'microplots.scripts'], 10 | setup_requires=['chaco==5.1.0','enable==5.3.1','traits==6.4.1','traitsui==7.4.2','pandas==1.3.5','pyqt'], 11 | url='https://github.com/will-thompson-k/microstructure-plotter', 12 | license='MIT', 13 | author='Will Thompson', 14 | author_email='', 15 | description='Visualize financial market data 📈 on infinitesimal timescales 🔎', 16 | ) --------------------------------------------------------------------------------