├── .gitignore ├── README.md └── notebooks ├── Hasbrouck_Market_Microstructure ├── bivariate_normal_projection.ipynb ├── market_makers_game_theory.ipynb ├── order_book_simulations │ ├── classes │ │ ├── market_manager.py │ │ ├── order.py │ │ ├── order_book.py │ │ ├── trade.py │ │ └── trader.py │ ├── experiments │ │ ├── experiment_0_order_flow_vs_price.ipynb │ │ ├── experiment_1_CMSW_framework.ipynb │ │ ├── experiment_2_informed_traders_and_depth.ipynb │ │ ├── experiment_3_risk_and_depth.ipynb │ │ ├── experiment_4_market_making.ipynb │ │ ├── order_flow_representation.ipynb │ │ └── order_flow_simulation.ipynb │ ├── tests │ │ └── tests1.ipynb │ └── utilities.py ├── price_impact.ipynb ├── roll_model_relaxing_of_assumptions.ipynb ├── roll_model_serial_dependence.ipynb ├── sequential_trade_model.ipynb ├── sequential_trade_model_part_2.ipynb └── sequential_trade_model_part_3.ipynb ├── finance_notebooks ├── bybit_flow_analysis │ ├── .gitignore │ └── format_flow_from_bybit.ipynb └── temperature_analysis │ ├── temperature_dataset_deseasoning.ipynb │ └── temperature_dataset_exploration.ipynb ├── general_python_tutorials ├── dangers_of_hidden_parameters.ipynb └── multiindexing_tutorial.ipynb ├── simple_vectorial_backtest ├── backtest.py └── simple_vectorial_backtest.ipynb └── statistics ├── interpretation_of_logistic_regression_estimations.ipynb └── wu_for_better_hypothesis_testing.ipynb /.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 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 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 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | /notebooks/finance_notebooks/temperature_analysis/data 156 | /notebooks/finance_notebooks/temperature_analysis/*.parquet 157 | /notebooks/finance_notebooks/temperature_analysis/*.csv -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to my Quantitative Finance Playground! 2 | 3 | ### Overview 4 | This repository serves as a hub for listing my articles and experiments. I primarly focus on Quantitative Finance and Machine Learning. 5 | 6 | I am currently studying Hasbrouck’s Empirical Market Microstructure, an interesting technical introduction on market making. 7 | 8 | 9 | ### About Me 10 | Starting with a background in Physics and spending six years in finance, I've currently found my groove as a Quant. 11 | 12 | When I'm not crunching numbers, I'm absorbed in the pages of a book or tending to my garden. 13 | 14 | 15 | ### Feedback and Contacts 16 | Your feedback is important! If you have any questions, suggestions, or feedback, please don't hesitate to reach out to me via [**LinkedIn**](https://www.linkedin.com/in/luigi-battistoni/). 17 | 18 | 19 | ## Articles 20 | Below are my articles, organized in topics and sorted by their most recent publication date. Where applicable, I will provide links to Python notebooks containing the plots and computations used in each piece. 21 | 22 | - [Finance](#finance️) 23 | - [Python](#python) 24 | - [Statistics](#statistics) 25 | - [Philosophy](#philosophy) 26 | 27 | ## 28 | ## 29 | 30 | ### **Finance** 31 | [⬆️ Return to index](#articles) 32 | 33 | 34 | - 📄 **[Modeling Competition between Market Makers](https://medium.com/@lu.battistoni/modeling-competition-between-market-makers-89bec121fedb)** 35 | - 📓 [Notebook](https://github.com/Peropero0/quantitative_finance_playground/blob/main/notebooks/Hasbrouck_Market_Microstructure/market_makers_game_theory.ipynb) 36 | - 📅 January 2025 37 | 38 | - 📄 **[Modeling Market Making and Price Impact](https://medium.com/@lu.battistoni/modeling-market-making-and-price-impact-e3fbdbaef30a)** 39 | - 📓 [Notebook](https://github.com/Peropero0/quantitative_finance_playground/blob/main/notebooks/Hasbrouck_Market_Microstructure/price_impact.ipynb) 40 | - 📅 January 2025 41 | 42 | - 📄 **[Using the Order Book depth to unveil informed trading](https://medium.com/@lu.battistoni/using-the-order-book-depth-to-unveil-informed-trading-bc92b5288d94)** 43 | - 📓 [Notebook 1](https://github.com/Peropero0/quantitative_finance_playground/blob/main/notebooks/Hasbrouck_Market_Microstructure/order_book_simulations/experiment_2_informed_traders_and_depth.ipynb) 44 | - 📙 [Notebook 2](https://github.com/Peropero0/quantitative_finance_playground/blob/main/notebooks/Hasbrouck_Market_Microstructure/order_book_simulations/experiment_3_risk_and_depth.ipynb) 45 | - 📅 August 2024 46 | 47 | - 📄 **[When should an investor prefer a Market Order over a Limit Order?](https://medium.com/@lu.battistoni/when-should-an-investor-prefer-a-market-order-over-a-limit-order-593bc0fd6dd9)** 48 | - 📓 [Notebook](https://github.com/Peropero0/quantitative_finance_playground/blob/main/notebooks/Hasbrouck_Market_Microstructure/order_book_simulations/experiment_1_CMSW_framework.ipynb) 49 | - 📅 August 2024 50 | 51 | - 📄 [**An Order Book simulator in Python**](https://medium.com/@lu.battistoni/an-order-book-simulator-in-python-b7b59ec82258) 52 | - 📁 [Folder](https://github.com/Peropero0/quantitative_finance_playground/tree/main/notebooks/Hasbrouck_Market_Microstructure/order_book_simulations) 53 | - 📅 July 2024 54 | 55 | 56 | - 📄 [**A brilliant way to represent the Order Flow in Python**](https://medium.com/@lu.battistoni/a-brilliant-way-to-represent-the-order-flow-in-python-fb96318e1070) 57 | - 📓 [Notebook](https://github.com/Peropero0/quantitative_finance_playground/blob/main/notebooks/Hasbrouck_Market_Microstructure/order_book_simulations/order_flow_representation.ipynb) 58 | - 📅 July 2024 59 | 60 | - 📄 [**Understanding Futures contract specifications**](https://medium.com/@lu.battistoni/understanding-futures-contract-specifications-c8be50844acd) 61 | - 📅 June 2024 62 | 63 | 64 | 65 | 66 | - 📄 [**8 Options Trading rules to be successful**](https://medium.com/@lu.battistoni/8-options-trading-rules-to-be-successful-5418f469137f) 67 | - 📅 May 2024 68 | 69 | 70 | 71 | - 📄 [**Distribution of the Order Flow in Python**](https://medium.com/technological-singularity/distribution-of-the-order-flow-in-python-d7ba059dbf13) 72 | - 📓 [Notebook](https://github.com/Peropero0/quantitative_finance_playground/blob/main/notebooks/Hasbrouck_Market_Microstructure/sequential_trade_model_part_3.ipynb) 73 | - 📅 Apr 2024 74 | 75 | 76 | - 📄 [**Sequential Trade Model for Asymmetrical Information — Part 2**](https://medium.com/technological-singularity/sequential-trade-model-for-asymmetrical-information-part-2-74ce13070bdd) 77 | - 📅 Apr 2024 78 | 79 | 80 | 81 | - 📄 [**Sequential Trade Model for Asymmetrical Information**](https://medium.com/@lu.battistoni/sequential-trade-model-for-asymmetrical-information-54245268f802) 82 | - 📓 [Notebook 1](https://github.com/Peropero0/quantitative_finance_playground/blob/main/notebooks/Hasbrouck_Market_Microstructure/sequential_trade_model.ipynb) 83 | 84 | - 📙 [Notebook 2](https://github.com/Peropero0/quantitative_finance_playground/blob/main/notebooks/Hasbrouck_Market_Microstructure/sequential_trade_model_part_2.ipynb) 85 | - 📅 Apr 2024 86 | 87 | 88 | - 📄 [**Relaxing Linear Regression assumptions — A Roll model application**](https://medium.com/@lu.battistoni/relaxing-linear-regression-assumptions-a-roll-model-application-59e310dde6ce) 89 | - 📓 [Notebook](https://github.com/Peropero0/quantitative_finance_playground/blob/main/notebooks/Hasbrouck_Market_Microstructure/roll_model_relaxing_of_assumptions.ipynb) 90 | - 📅 Mar 2024 91 | 92 | 93 | - 📄 [**The Roll Model Under Serial Dependence**](https://python.plainenglish.io/roll-model-under-serial-dependence-f9ba693446f9) 94 | - 📓 [Notebook](https://github.com/Peropero0/quantitative_finance_playground/blob/main/notebooks/Hasbrouck_Market_Microstructure/roll_model_serial_dependence.ipynb) 95 | - 📅 Feb 2024 96 | 97 | 98 | ## 99 | ### **Python** 100 | [⬆️ Return to index](#articles) 101 | 102 | - 📄 [**The Dangers of Pandas Hidden Parameters**](https://medium.com/@lu.battistoni/the-dangers-of-pandas-hidden-parameters-1e6a013345e0) 103 | - 📓 [Notebook](https://github.com/Peropero0/quantitative_finance_playground/blob/main/notebooks/general_python_tutorials/dangers_of_hidden_parameters.ipynb) 104 | - 📅 December 2024 105 | 106 | - 📄 [**How to download and format free historical order book dataset**](https://medium.com/@lu.battistoni/how-to-download-and-format-free-historical-order-book-dataset-16b3a84a8e0e) 107 | - 📓 [Notebook](https://github.com/Peropero0/quantitative_finance_playground/blob/main/notebooks/finance_notebooks/bybit_flow_analysis/format_flow_from_bybit.ipynb) 108 | - 📅 September 2024 109 | 110 | - 📄 [**Exploratory Data Analysis in Python**](https://medium.com/@lu.battistoni/exploratory-data-analysis-in-python-6a41a7505f5b) 111 | - 📓📙 [Notebooks](https://github.com/Peropero0/quantitative_finance_playground/tree/main/notebooks/finance_notebooks/temperature_analysis) 112 | - 📅 June 2024 113 | 114 | - 📄 [**Understanding Pandas MultiIndex in Finance**](https://medium.com/@lu.battistoni/understanding-pandas-multiindex-in-finance-cdfdda16f792) 115 | - 📓 [Notebook](https://github.com/Peropero0/quantitative_finance_playground/blob/main/notebooks/general_python_tutorials/multiindexing_tutorial.ipynb) 116 | - 📅 May 2024 117 | 118 | 119 | - 📄 [**Backtesting a systematic trading strategy in Python**](https://medium.com/@lu.battistoni/backtesting-a-systematic-trading-strategy-in-python-e08263e888ab) 120 | - 📓 [Notebook](https://github.com/Peropero0/quantitative_finance_playground/blob/main/notebooks/simple_vectorial_backtest/simple_vectorial_backtest.ipynb) 121 | - 📅 May 2024 122 | 123 | ## 124 | ### Statistics 125 | [⬆️ Return to index](#articles) 126 | 127 | - 📄 **[Must-Know in Statistics: The Bivariate Normal Projection Explained](https://medium.com/@lu.battistoni/must-know-in-statistics-the-bivariate-normal-projection-explained-ace7b2f70b5b)** 128 | - 📓 [Notebook](https://github.com/Peropero0/quantitative_finance_playground/blob/main/notebooks/Hasbrouck_Market_Microstructure/bivariate_normal_projection.ipynb) 129 | - 📅 August 2024 130 | 131 | - 📄 **[Using the concept of Wú (無) for better hypothesis testing](https://medium.com/@lu.battistoni/using-the-concept-of-w%C3%BA-%E7%84%A1-for-better-hypothesis-testing-689fdbafaaf6)** 132 | - 📓 [Notebook](https://github.com/Peropero0/quantitative_finance_playground/blob/main/notebooks/statistics/wu_for_better_hypothesis_testing.ipynb) 133 | - 📅 August 2024 134 | 135 | - 📄 **[Must-Know in Statistics: interpretation of Logistic Regression coefficients](https://medium.com/@lu.battistoni/must-know-in-statistics-interpretation-of-logistic-regression-coefficients-a332f74305fd)** 136 | - 📓 [Notebook](https://github.com/Peropero0/quantitative_finance_playground/blob/main/notebooks/statistics/interpretation_of_logistic_regression_estimations.ipynb) 137 | - 📅 October 2024 138 | 139 | 140 | ## 141 | ### Philosophy 142 | [⬆️ Return to index](#articles) 143 | 144 | - 📄 **[Do you have what it takes to become a trader?](https://medium.com/@lu.battistoni/do-you-have-what-it-takes-to-become-a-trader-cf3909e0f5da)** 145 | - 📅 August 2024 146 | 147 | 148 | 149 | ## -------------------------------------------------------------------------------- /notebooks/Hasbrouck_Market_Microstructure/order_book_simulations/classes/market_manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class contains the logic of the simulation. You can run the simulations using the method run_market_manager. 3 | Write custom logic in the method simulate_market. 4 | """ 5 | from classes.trader import Trader 6 | from classes.order_book import OrderBook 7 | from abc import abstractmethod 8 | import numpy as np 9 | 10 | class MarketManager(): 11 | 12 | def __init__(self, simulation_length, traders_dict, book: OrderBook): 13 | self.simulation_length = simulation_length 14 | self.traders = self.generate_traders(traders_dict) 15 | self.book = book 16 | 17 | 18 | def generate_traders(self, traders_dict): 19 | """ Method useful to generate traders. The traders dict is a dictionary that has: 20 | - key -> is an int representing the trader_id, you should have a key for each trader 21 | - values: 22 | - value[0] -> initial_cash : initial cash of the trader, can be a float 23 | - value[1] -> number_units_stock_in_inventory : inital number of units of stock of the trader, can be a float 24 | - value[2] -> check_order_feasibility : do I have to check if a trader has enough cash/units to trade? 25 | """ 26 | 27 | traders_list = [] 28 | for key, value in traders_dict.items(): 29 | traders_list.append( 30 | Trader( 31 | initial_cash=value[0], 32 | number_units_stock_in_inventory=value[1], 33 | check_order_feasibility=value[2], 34 | trader_id=key 35 | ) 36 | ) 37 | 38 | return traders_list 39 | 40 | def run_market_manager(self, *args): 41 | """This is the main engine that you should run. This takes care of running the simulation 42 | that is defined under self.simulate_market() . 43 | At each timestep of the simulation, run the actual logic of the market and then 44 | update the trader's quantities, like the cash and the number of units. 45 | """ 46 | 47 | # update the traders' sequences with initial values 48 | self.update_traders_cash(simulation_step=0) 49 | self.update_traders_number_of_units_of_stock(simulation_step=0) 50 | 51 | for simulation_step in range(1, self.simulation_length + 1): 52 | self.simulate_market(simulation_step, *args) 53 | 54 | self.update_current_cash_margin_and_units(simulation_step) 55 | 56 | self.update_traders_cash(simulation_step) 57 | self.update_traders_number_of_units_of_stock(simulation_step) 58 | self.update_traders_total_wealth(simulation_step) 59 | self.update_traders_active_orders() 60 | 61 | @abstractmethod 62 | def simulate_market(self, simulation_step, *args): 63 | """ 64 | Here you can add a custom logic of how the traders should behave 65 | """ 66 | pass 67 | 68 | 69 | def update_traders_cash(self, simulation_step): 70 | for trader in self.traders: 71 | trader.cash_sequence.append((simulation_step, trader.cash)) 72 | 73 | 74 | def update_traders_number_of_units_of_stock(self, simulation_step): 75 | for trader in self.traders: 76 | trader.number_units_stock_in_inventory_sequence.append((simulation_step, trader.number_units_stock_in_inventory)) 77 | trader.number_units_stock_in_market_sequence.append((simulation_step, trader.number_units_stock_in_market)) 78 | 79 | def update_traders_total_wealth(self, simulation_step): 80 | for trader in self.traders: 81 | if np.isnan(self.book.price_sequence[-1]) or (self.book.price_sequence[-1] == False): 82 | price = self.book.mid_price_sequence[-1] 83 | else: 84 | price = self.book.price_sequence[-1] 85 | 86 | # total wealth = 87 | # cash + (stocks in my inventory + stocks in limit sells) * last price) 88 | total_wealth = trader.cash + ((trader.number_units_stock_in_inventory + trader.number_units_stock_in_market) * price) 89 | 90 | trader.total_wealth_sequence.append((simulation_step, total_wealth)) 91 | 92 | def update_current_cash_margin_and_units(self, simulation_step): 93 | """ 94 | This function is useful to update the current cash, margin and units of each trader. 95 | 96 | It uses the trades list of the book to update the quantities. 97 | We don't update some quantities because we already did that in the order book class 98 | """ 99 | for trade in self.book.trades[simulation_step]: 100 | trader_already_in_book = [ 101 | trader for trader in self.traders if trader.trader_id == trade.trader_id_already_in_book 102 | ][0] 103 | 104 | trader_coming_in_book = [ 105 | trader for trader in self.traders if trader.trader_id == trade.trader_id_coming_in_book 106 | ][0] 107 | 108 | 109 | if trade.direction == 'buy': 110 | trader_coming_in_book.cash = round( 111 | trader_coming_in_book.cash - (trade.price * trade.volume), 5) 112 | 113 | trader_coming_in_book.margin = round( 114 | trader_coming_in_book.margin - (trade.price * trade.volume), 5) 115 | 116 | trader_coming_in_book.number_units_stock_in_inventory = round( 117 | trader_coming_in_book.number_units_stock_in_inventory + trade.volume, 5) 118 | 119 | trader_already_in_book.cash = round( 120 | trader_already_in_book.cash + (trade.price * trade.volume), 5) 121 | trader_already_in_book.margin = round( 122 | trader_already_in_book.margin + (trade.price * trade.volume), 5) 123 | 124 | trader_already_in_book.number_units_stock_in_market = round( 125 | trader_already_in_book.number_units_stock_in_market - trade.volume,5) 126 | 127 | elif trade.direction == 'sell': 128 | trader_coming_in_book.cash = round( 129 | trader_coming_in_book.cash + (trade.price * trade.volume), 5) 130 | trader_coming_in_book.margin = round( 131 | trader_coming_in_book.margin + (trade.price * trade.volume), 5) 132 | 133 | trader_coming_in_book.number_units_stock_in_inventory = round( 134 | trader_coming_in_book.number_units_stock_in_inventory - trade.volume, 5) 135 | 136 | 137 | trader_already_in_book.cash = round( 138 | trader_already_in_book.cash - (trade.price * trade.volume), 5) 139 | 140 | trader_already_in_book.number_units_stock_in_inventory = round( 141 | trader_already_in_book.number_units_stock_in_inventory + trade.volume, 5) 142 | 143 | 144 | 145 | 146 | 147 | def update_traders_active_orders(self): 148 | """ 149 | Keep track of active orders issued by each trader 150 | """ 151 | for trader in self.traders: 152 | active_limit_buys = [(bid[0], bid[1], bid[2], 'limit_buy') for bid in self.book.bids if bid[3] == trader.trader_id] 153 | active_limit_sells = [(ask[0], ask[1], ask[2], 'limit_sell') for ask in self.book.asks if ask[3] == trader.trader_id] 154 | 155 | trader.active_orders = active_limit_buys + active_limit_sells 156 | 157 | -------------------------------------------------------------------------------- /notebooks/Hasbrouck_Market_Microstructure/order_book_simulations/classes/order.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class contains the orders details, like the order type, the price and the quantity. 3 | """ 4 | class Order(): 5 | 6 | supported_orders = ( 7 | 'market_buy', 8 | 'market_sell', 9 | 'limit_buy', 10 | 'limit_sell', 11 | 'modify_limit_buy', 12 | 'modify_limit_sell', 13 | 'do_nothing') 14 | 15 | def __init__(self, order_type, price, quantity, trader_id): 16 | 17 | if order_type not in self.supported_orders: 18 | raise ValueError(f'valid values for order_type are {self.supported_orders}.\nYou passed {order_type}') 19 | 20 | self.order_type = order_type 21 | 22 | # other checks: 23 | # price > 0, quantity > 0, if market_order you should pass no prices, if limit you should pass it 24 | self.price = price 25 | self.quantity = quantity 26 | self.trader_id = trader_id 27 | # add the attribute order id if you want to implement the cancel order feature 28 | 29 | def print_order(self): 30 | print(f"{self.order_type} - price: {self.price} - quantity: {self.quantity}") 31 | 32 | -------------------------------------------------------------------------------- /notebooks/Hasbrouck_Market_Microstructure/order_book_simulations/classes/order_book.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class is the core of the order book simulator. This class allows a Trader object to place some 3 | Order objects as orders in the book. If the demand and the offer match, a Trade object is generated. 4 | 5 | This class allows to: 6 | - place limit orders 7 | - execute market orders 8 | - modify orders 9 | - print the state of the order book 10 | - return various quantities (mid price, micro price, bid ask spread, traded price, traded volumes) 11 | 12 | Additional features that can be implemented in this simulator are the following: 13 | - stop loss / take profit 14 | 15 | """ 16 | 17 | from classes.order import Order 18 | from prettytable import PrettyTable 19 | import numpy as np 20 | from classes.trade import Trade 21 | 22 | 23 | class OrderBook(): 24 | 25 | def __init__(self): 26 | self.bids = [] # list of (price, quantity, order_id, trader_id) 27 | self.asks = [] # list of (price, quantity, order_id, trader_id) 28 | 29 | self.trades = {} # dictionary where the key is the time (you can see this as a snapshot number) and the value is the Trade object 30 | self.time = 0 # time of the simulation, you can see this as an order book snapshot number 31 | 32 | self.price_sequence = [] # contains the sequence of executed prices 33 | self.mid_price_sequence = [] # sequence of mid prices 34 | self.micro_price_sequence = [] # sequence of micro prices 35 | self.volumes_sequence = [] # sequence of volumes of the executed prices 36 | self.buy_sequence = [] # 1 if the trade was a buy, 0 otherwise 37 | self.sell_sequence = [] # 1 if the trade was a sell, 0 otherwise 38 | self.book_state_sequence = [] # wrapper for the book state 39 | self.bid_ask_spread_sequence = [] # sequence of bid ask spreads 40 | self.volume_imbalance_sequence = [] # sequence of volume imbalances 41 | self.order_flow_imbalance_sequence = [] # sequence of order flow imbalances 42 | self.last_best_bid_price = np.nan 43 | self.last_best_ask_price = np.nan 44 | self.last_best_bid_volume = np.nan 45 | self.last_best_ask_volume = np.nan 46 | self.depth_sequence_size = [] # sequence of depth of the book 47 | self.depth_sequence_volumes = [] # sequence of depth of the book 48 | 49 | 50 | def execute_market_order(self, quantity, order_type, order_id, trader_id): 51 | # execute a market order, getting the first available ask if buying 52 | # and the first available bid if selling 53 | # if the first available book level is not sufficient to execute the 54 | # whole trade, the next level is used 55 | 56 | if order_type == 'market_buy': 57 | # market buy 58 | try: 59 | # pop the best ask 60 | best_available_ask_price, best_available_ask_quantity, bb_order_id, bb_trader_id = self.asks.pop(0) 61 | except Exception: 62 | # ask is empty 63 | return 64 | 65 | # if traded quantity > available quantity... 66 | if quantity >= best_available_ask_quantity: 67 | # ...the trade happens at the ask and all the available volumes at ask are traded 68 | self.trades[self.time].append( 69 | Trade( 70 | price=best_available_ask_price, 71 | volume=best_available_ask_quantity, 72 | direction='buy', 73 | trader_id_already_in_book=bb_trader_id, 74 | trader_id_coming_in_book=trader_id, 75 | order_id_already_in_book=bb_order_id, 76 | order_id_coming_in_book=order_id 77 | ) 78 | ) 79 | 80 | # add another market order for the remaining quantity. 81 | # this will call the function again and execute it on the new best ask 82 | # since we called the Trade class, we don't have to take care of margin and units 83 | self.execute_market_order(round(quantity - best_available_ask_quantity, 5), 'market_buy', order_id, trader_id) 84 | else: 85 | # if the quantity is less than the available quantity... 86 | if quantity != 0: # <- this is useful to stop the recursion if the market order executes exactly the ask volumes 87 | # ... then trade 88 | self.trades[self.time].append( 89 | Trade( 90 | price=best_available_ask_price, 91 | volume=quantity, 92 | direction='buy', 93 | trader_id_already_in_book=bb_trader_id, 94 | trader_id_coming_in_book=trader_id, 95 | order_id_already_in_book=bb_order_id, 96 | order_id_coming_in_book=order_id) 97 | ) 98 | # since we popped the best ask, now we want to put it again in the asks sequence, 99 | # with the updated volume 100 | # since we called the Trade class, we don't have to take care of margin and units 101 | self.asks.append((best_available_ask_price, round(best_available_ask_quantity - quantity, 5), bb_order_id, bb_trader_id)) 102 | self.asks = sorted(self.asks, key=lambda x: (x[0], x[2])) 103 | 104 | elif order_type == 'market_sell': 105 | # market sell 106 | # the code is similar to market buy, but with the bids 107 | try: 108 | best_available_bid_price, best_available_bid_quantity, bb_order_id, bb_trader_id = self.bids.pop(0) 109 | except Exception: 110 | # bid is empty 111 | return 112 | if quantity >= best_available_bid_quantity: 113 | self.trades[self.time].append( 114 | Trade( 115 | price=best_available_bid_price, 116 | volume=best_available_bid_quantity, 117 | direction='sell', 118 | trader_id_already_in_book=bb_trader_id, 119 | trader_id_coming_in_book=trader_id, 120 | order_id_already_in_book=bb_order_id, 121 | order_id_coming_in_book=order_id) 122 | ) 123 | # since we called the Trade class, we don't have to take care of margin and units 124 | 125 | self.execute_market_order(round(quantity - best_available_bid_quantity, 5), 'market_sell', order_id, trader_id) 126 | else: 127 | if quantity != 0: 128 | self.trades[self.time].append( 129 | Trade( 130 | price=best_available_bid_price, 131 | volume=quantity, 132 | direction='sell', 133 | trader_id_already_in_book=bb_trader_id, 134 | trader_id_coming_in_book=trader_id, 135 | order_id_already_in_book=bb_order_id, 136 | order_id_coming_in_book=order_id) 137 | ) 138 | # since we called the Trade class, we don't have to take care of margin and units 139 | 140 | self.bids.append((best_available_bid_price, round(best_available_bid_quantity - quantity, 5), bb_order_id, bb_trader_id)) 141 | self.bids = sorted(self.bids, key=lambda x: (-x[0], x[2])) 142 | 143 | 144 | @staticmethod 145 | def find_order_with_certain_price(order_book, price, order_id=None): 146 | # this method finds the orders with a certain price 147 | # this is useful to update the volumes of the orders when dealing with a limit order 148 | orders = [] 149 | for index, (p, q, o_id, t_id) in enumerate(order_book): 150 | if p == price: 151 | orders.append((index, (p, q, o_id, t_id))) 152 | 153 | if not orders: 154 | return None 155 | else: 156 | return orders 157 | 158 | def modify_order_of_the_order_book(self, trader, price, quantity, order_type, trader_id): 159 | if order_type == 'modify_limit_buy': 160 | where_to_look = self.bids 161 | rev = True 162 | 163 | # I already checked that this is feasible 164 | trader.margin += (price * quantity) 165 | 166 | elif order_type == 'modify_limit_sell': 167 | where_to_look = self.asks 168 | rev = False 169 | 170 | # I already checked that this is feasible 171 | trader.number_units_stock_in_inventory += quantity 172 | trader.number_units_stock_in_market = round(trader.number_units_stock_in_market - quantity, 5) 173 | else: 174 | raise ValueError('Order type not supported') 175 | 176 | 177 | orders = OrderBook.find_order_with_certain_price(where_to_look, price) 178 | 179 | if orders is not None: 180 | while True: 181 | # a trader could have many orders with that price 182 | (index, q_in_order, id) = [(t[0], t[1][1], t[1][2]) for t in orders if t[1][3] == trader_id][0] 183 | 184 | where_to_look.pop(index) 185 | quantity = round(q_in_order - quantity, 5) 186 | 187 | if quantity > 0: 188 | # if something remains, then add it again to the book 189 | where_to_look.append((price, quantity, id, trader_id)) 190 | break 191 | elif quantity == 0: 192 | break 193 | else: 194 | quantity = abs(quantity) 195 | orders = OrderBook.find_order_with_certain_price(where_to_look, price) 196 | 197 | 198 | if rev: 199 | self.bids = sorted(self.bids, key=lambda x: (-x[0], x[2])) 200 | else: 201 | self.asks = sorted(self.asks, key=lambda x: (x[0], x[2])) 202 | 203 | 204 | 205 | def add_limit_order(self, trader, price, quantity, order_type, order_id, trader_id): 206 | # add a limit order to the order book 207 | # the rules are the following: 208 | # - buy limit order with price >= best ask -> you are executed at the best ask 209 | # - sell limit toder with price <= best bid -> you are executed at the best bid 210 | # - otherwise the order goes into the order book 211 | 212 | if order_type == 'limit_buy': 213 | # you want to add a limit buy 214 | # this means that you go into the bid part of the book and place an order. 215 | # if you place an order with a price >= than the best ask, then you are executed 216 | 217 | try: 218 | # get the best ask 219 | best_available_ask_price, best_available_ask_quantity, bb_order_id, bb_trader_id = self.asks[0] 220 | except Exception: 221 | # if there is no ask add a fake one, in reality probably a dealer would execute your trade 222 | best_available_ask_price = price + 1 223 | best_available_ask_quantity = 0 224 | 225 | # if your limit buy has a price greater than the best ask, you are executed at the best ask 226 | if price >= best_available_ask_price: 227 | # you are executed at the best ask for the volumes in the best ask 228 | if quantity > best_available_ask_quantity: 229 | if best_available_ask_quantity != 0: 230 | self.execute_market_order(best_available_ask_quantity, 'market_buy', order_id, trader_id) 231 | 232 | # then you are either executed at the next best ask or a limit buy is added 233 | # this does this logic recursively 234 | self.add_limit_order(trader, price, round(quantity - best_available_ask_quantity, 5), 'limit_buy', order_id, trader_id) 235 | trader.margin = round(trader.margin - (quantity - best_available_ask_quantity), 5) * price 236 | 237 | # if the quantity is less than the available quantity, you are executed on the best ask 238 | elif quantity <= best_available_ask_quantity: 239 | self.execute_market_order(quantity, 'market_buy', order_id, trader_id) 240 | 241 | # if your price is less than the best ask, then your order goes in the book 242 | elif price < best_available_ask_price: 243 | # now check for the best bid 244 | try: 245 | best_available_bid_price, _, _, _ = self.bids[0] 246 | except Exception: 247 | best_available_bid_price = -1 248 | 249 | 250 | self.bids.append((price, quantity, order_id, trader_id)) 251 | self.bids = sorted(self.bids, key=lambda x: (-x[0], x[2])) 252 | 253 | trader.margin = round(trader.margin - (quantity * price), 5) 254 | 255 | elif order_type == 'limit_sell': 256 | # this is similar to the limit buy situation 257 | try: 258 | best_available_bid_price, best_available_bid_quantity, bb_order_id, bb_trader_id = self.bids[0] 259 | except Exception: 260 | best_available_bid_price = -1 261 | best_available_bid_quantity = 0 262 | 263 | if price <= best_available_bid_price: 264 | if quantity > best_available_bid_quantity: 265 | if best_available_bid_quantity != 0: 266 | self.execute_market_order(best_available_bid_quantity, 'market_sell', order_id, trader_id) 267 | self.add_limit_order(trader, price, round(quantity - best_available_bid_quantity, 5), 'limit_sell', order_id, trader_id) 268 | 269 | 270 | 271 | elif quantity <= best_available_bid_quantity: 272 | self.execute_market_order(quantity, 'market_sell', order_id, trader_id) 273 | 274 | 275 | elif price > best_available_bid_price: 276 | try: 277 | best_available_ask_price, _, _, _ = self.asks[0] 278 | except Exception: 279 | best_available_ask_price = price + 1 280 | 281 | self.asks.append((price, quantity, order_id, trader_id)) 282 | self.asks = sorted(self.asks, key=lambda x: (x[0], x[2])) 283 | 284 | trader.number_units_stock_in_inventory = round(trader.number_units_stock_in_inventory - quantity, 5) 285 | trader.number_units_stock_in_market = quantity 286 | 287 | def order_manager(self, order: Order, trader, time=None, update_lists=True): 288 | # method used to add, execute or modify an order of the order book 289 | if time is None: 290 | self.time += 1 291 | else: 292 | self.time = time 293 | 294 | self.trades[self.time] = [] 295 | 296 | if order.order_type in ('market_buy', 'market_sell'): 297 | self.execute_market_order(order.quantity, order.order_type, self.time, order.trader_id) 298 | elif order.order_type in ('limit_buy', 'limit_sell'): 299 | self.add_limit_order(trader, order.price, order.quantity, order.order_type, self.time, order.trader_id) 300 | elif order.order_type in ('modify_limit_buy', 'modify_limit_sell'): 301 | self.modify_order_of_the_order_book(trader, order.price, order.quantity, order.order_type, order.trader_id) 302 | 303 | # if no orders we want to update the book anyway 304 | 305 | # update the lists useful to track various quantities 306 | 307 | if update_lists: 308 | self.update_mid_price_sequence() 309 | self.update_micro_price_sequence() 310 | 311 | self.update_bid_ask_spread_sequence() 312 | self.update_price_volume_sequences() 313 | self.update_volume_imbalance_sequence() 314 | self.update_order_flow_imbalance_sequence() 315 | 316 | self.update_book_state_sequence() 317 | self.update_depth_sequence() 318 | 319 | 320 | 321 | def print_order_book_state(self): 322 | # print the bid and the asks, with prices and volumess 323 | print(f"\nOrder book at time {self.time}") 324 | 325 | table = PrettyTable() 326 | table.field_names = ['price', 'quantity', 'side'] 327 | 328 | asks = self.asks[::-1] 329 | 330 | sums = {} 331 | for p, v, _, _ in asks: 332 | if p in sums: 333 | sums[p] += v 334 | else: 335 | sums[p] = v 336 | 337 | for p, v in list(sums.items()): 338 | table.add_row((p, v, 'ask')) 339 | 340 | sums = {} 341 | for p, v, _, _ in self.bids: 342 | if p in sums: 343 | sums[p] += v 344 | else: 345 | sums[p] = v 346 | 347 | for p, v in list(sums.items()): 348 | table.add_row((p, v, 'bid')) 349 | 350 | print(table) 351 | print("") 352 | 353 | def return_mid_price(self): 354 | # return the mid price, that is in the middle of the bid ask spread 355 | try: 356 | return (self.asks[0][0] + self.bids[0][0]) / 2 357 | except Exception: 358 | return np.nan 359 | 360 | def return_micro_price(self): 361 | # return the microprice 362 | try: 363 | price_ask = self.asks[0][0] 364 | asks_orders = self.find_order_with_certain_price(self.asks, price_ask) 365 | volume_ask = sum([v[1][1] for v in asks_orders]) 366 | 367 | price_bid = self.bids[0][0] 368 | bids_orders = self.find_order_with_certain_price(self.bids, price_bid) 369 | volume_bid = sum([v[1][1] for v in bids_orders]) 370 | 371 | return ((volume_bid * price_ask) + (volume_ask * price_bid)) / (volume_ask + volume_bid) 372 | except Exception: 373 | return np.nan 374 | 375 | def return_bid_ask_spread(self): 376 | # return the bid ask spread 377 | try: 378 | price_ask = self.asks[0][0] 379 | price_bid = self.bids[0][0] 380 | 381 | return round(price_ask - price_bid, 5) 382 | except Exception: 383 | return np.nan 384 | 385 | 386 | def update_price_volume_sequences(self): 387 | trades = self.trades[self.time] 388 | 389 | if trades: 390 | sum_of_volume = 0 391 | price_executed = 0 392 | direction = '' 393 | for trade in trades: 394 | sum_of_volume += trade.volume 395 | price_executed = trade.price 396 | direction = trade.direction 397 | 398 | self.price_sequence.append(price_executed) 399 | self.volumes_sequence.append(sum_of_volume) 400 | 401 | if direction == 'buy': 402 | self.buy_sequence.append(1) 403 | self.sell_sequence.append(0) 404 | elif direction == 'sell': 405 | self.buy_sequence.append(0) 406 | self.sell_sequence.append(1) 407 | 408 | else: 409 | if self.price_sequence: 410 | self.price_sequence.append(self.price_sequence[-1]) 411 | else: 412 | self.price_sequence.append(self.return_mid_price()) 413 | 414 | self.volumes_sequence.append(0) 415 | self.buy_sequence.append(0) 416 | self.sell_sequence.append(0) 417 | 418 | 419 | def update_book_state_sequence(self): 420 | 421 | ask_list = [] 422 | bid_list = [] 423 | 424 | if self.asks: 425 | sums = {} 426 | first_ask = True 427 | for p, v, _, _ in self.asks: 428 | if first_ask: 429 | self.last_best_ask_price = p # quantity useful to compute the order flow imbalance 430 | first_ask = False 431 | if p in sums: 432 | sums[p] += v 433 | else: 434 | sums[p] = v 435 | 436 | 437 | ask_list = [[self.time, p, v, 'ask'] for p, v in sums.items()] 438 | 439 | # quantity useful to compute the order flow imbalance 440 | self.last_best_ask_volume = sums[self.last_best_ask_price] 441 | 442 | self.book_state_sequence.append(ask_list) 443 | 444 | if self.bids: 445 | sums = {} 446 | first_bid = True 447 | for p, v, _, _ in self.bids: 448 | if first_bid: 449 | self.last_best_bid_price = p # quantity useful to compute the order flow imbalance 450 | first_bid = False 451 | if p in sums: 452 | sums[p] += v 453 | else: 454 | sums[p] = v 455 | 456 | 457 | bid_list = [[self.time, p, v, 'bid'] for p, v in sums.items()] 458 | 459 | # quantity useful to compute the order flow imbalance 460 | self.last_best_bid_volume = sums[self.last_best_bid_price] 461 | 462 | self.book_state_sequence.append(bid_list) 463 | 464 | 465 | def update_mid_price_sequence(self): 466 | self.mid_price_sequence.append(self.return_mid_price()) 467 | 468 | def update_micro_price_sequence(self): 469 | self.micro_price_sequence.append(self.return_micro_price()) 470 | 471 | def update_bid_ask_spread_sequence(self): 472 | self.bid_ask_spread_sequence.append(self.return_bid_ask_spread()) 473 | 474 | 475 | def return_volume_imbalance(self): 476 | try: 477 | asks_orders = self.find_order_with_certain_price(self.asks, self.asks[0][0]) 478 | volume_ask = sum([v[1][1] for v in asks_orders]) 479 | 480 | bids_orders = self.find_order_with_certain_price(self.bids, self.bids[0][0]) 481 | volume_bid = sum([v[1][1] for v in bids_orders]) 482 | 483 | return round(volume_bid - volume_ask, 5) / (volume_bid + volume_ask) 484 | 485 | except Exception: 486 | return np.nan 487 | 488 | 489 | def update_volume_imbalance_sequence(self): 490 | self.volume_imbalance_sequence.append(self.return_volume_imbalance()) 491 | 492 | 493 | def return_order_flow_imbalance(self): 494 | try: 495 | if self.time == 1: 496 | return 0 497 | 498 | else: 499 | # sum volumes of orders with the same price 500 | price_ask = self.asks[0][0] 501 | asks_orders = self.find_order_with_certain_price(self.asks, price_ask) 502 | volume_ask = sum([v[1][1] for v in asks_orders]) 503 | 504 | price_bid = self.bids[0][0] 505 | bids_orders = self.find_order_with_certain_price(self.bids, price_bid) 506 | volume_bid = sum([v[1][1] for v in bids_orders]) 507 | 508 | if price_bid > self.last_best_bid_price: 509 | delta_volume_bid = volume_bid 510 | elif price_bid < self.last_best_bid_price: 511 | delta_volume_bid = - self.last_best_bid_volume 512 | else: 513 | delta_volume_bid = round(volume_bid - self.last_best_bid_volume, 5) 514 | 515 | if price_ask > self.last_best_ask_price: 516 | delta_volume_ask = - self.last_best_ask_volume 517 | elif price_ask < self.last_best_ask_price: 518 | delta_volume_ask = volume_ask 519 | else: 520 | delta_volume_ask = round(volume_ask - self.last_best_ask_volume, 5) 521 | 522 | 523 | return round(delta_volume_bid - delta_volume_ask, 5) 524 | 525 | 526 | except Exception: 527 | return 0 528 | 529 | 530 | def update_order_flow_imbalance_sequence(self): 531 | self.order_flow_imbalance_sequence.append(self.return_order_flow_imbalance()) 532 | 533 | def return_order_book_depth_size(self): 534 | return (len(self.book_state_sequence[(self.time * 2) - 2]), 535 | len(self.book_state_sequence[(self.time * 2) - 1])) 536 | 537 | def return_order_book_depth_volumes(self): 538 | sum_volumes_ask = 0 539 | for a in self.asks: 540 | sum_volumes_ask += a[1] 541 | 542 | sum_volumes_bid = 0 543 | for b in self.bids: 544 | sum_volumes_bid += b[1] 545 | 546 | return (sum_volumes_ask, sum_volumes_bid) 547 | 548 | def update_depth_sequence(self): 549 | self.depth_sequence_size.append(self.return_order_book_depth_size()) 550 | self.depth_sequence_volumes.append(self.return_order_book_depth_volumes()) 551 | 552 | 553 | -------------------------------------------------------------------------------- /notebooks/Hasbrouck_Market_Microstructure/order_book_simulations/classes/trade.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class contains the trades details, like the price, the volume and the direction (buy/sell). 3 | The order becomes a trade if it is executed. 4 | """ 5 | class Trade(): 6 | def __init__(self, price, volume, direction, 7 | trader_id_already_in_book, 8 | trader_id_coming_in_book, 9 | order_id_already_in_book, 10 | order_id_coming_in_book): 11 | 12 | # price of the trade 13 | self.price = price 14 | 15 | # volume of the trade 16 | self.volume = volume 17 | 18 | # direction of the trade (buy/sell) 19 | self.direction = direction 20 | 21 | # id of trader that already has his order in the book (a limit order) 22 | self.trader_id_already_in_book = trader_id_already_in_book 23 | 24 | # id of trader that is entering the order in the book (it can be mkt or limit) 25 | self.trader_id_coming_in_book = trader_id_coming_in_book 26 | 27 | # id of order already in the book, the limit order 28 | self.order_id_already_in_book = order_id_already_in_book 29 | 30 | # id of the order that has been issued and is being matched with the order already in the book 31 | self.order_id_coming_in_book = order_id_coming_in_book 32 | -------------------------------------------------------------------------------- /notebooks/Hasbrouck_Market_Microstructure/order_book_simulations/classes/trader.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class can be used to contain various information about the traders. 3 | In this first iteration, it only contains the method used to place orders in the order book. 4 | 5 | In further iterations, it could contain 6 | - a risk appetite or utility function 7 | - a generalised way to describe a trading strategy followed by the trader 8 | """ 9 | 10 | from classes.order import Order 11 | from classes.order_book import OrderBook 12 | from prettytable import PrettyTable 13 | 14 | import numpy as np 15 | 16 | class Trader(): 17 | def __init__(self, initial_cash=100, number_units_stock_in_inventory=0, trader_id=None, check_order_feasibility=False): 18 | # here you can set different trader attributes 19 | # like the initial cash, the trading strategy type, the risk aversion, etc... 20 | self.cash = initial_cash # total cash 21 | self.margin = initial_cash # what is available to trade 22 | self.number_units_stock_in_inventory = number_units_stock_in_inventory # stocks available to sell 23 | self.number_units_stock_in_market = 0 # number of stocks currently being sold on the market 24 | 25 | self.trader_id = trader_id 26 | 27 | # do I have to check if the trader has enough cash / units to trade? 28 | self.check_order_feasibility = check_order_feasibility 29 | 30 | self.active_orders = [] # list containing the active orders of the trader (price, volume, order_id, order_type) 31 | 32 | self.cash_sequence = [] # list containing tuples with (time, cash) 33 | self.number_units_stock_in_inventory_sequence = [] # list containing tuples with (time, units stock) 34 | self.number_units_stock_in_market_sequence = [] 35 | self.total_wealth_sequence = [] # list containing tuples with (time, total wealth) 36 | 37 | 38 | def submit_order_to_order_book(self, order_type, price, quantity, book: OrderBook, time=None, verbose=True, update_lists=True): 39 | # if the order is feasible... 40 | if not self.check_if_order_is_feasible(book, order_type, price, quantity): 41 | order_type = 'do_nothing' 42 | 43 | # ...generate an Order object and add it to the order book 44 | order = Order(order_type=order_type, price=price, quantity=quantity, trader_id=self.trader_id) 45 | 46 | if verbose: 47 | order.print_order() 48 | 49 | 50 | book.order_manager(order, self, time, update_lists=update_lists) 51 | 52 | 53 | 54 | def print_active_orders(self, time=None): 55 | print(self.active_orders) 56 | if time is None: 57 | print(f"\nActive orders of trader {self.trader_id}") 58 | else: 59 | print(f"\nActive orders of trader {self.trader_id} at time {self.time}") 60 | 61 | table = PrettyTable() 62 | table.field_names = ['price', 'quantity', 'order_id', 'order_type'] 63 | 64 | for (price, quantity, order_id, order_type) in self.active_orders: 65 | table.add_row((price, quantity, order_id, order_type)) 66 | 67 | print(table) 68 | print("") 69 | 70 | 71 | def check_if_order_is_feasible(self, book, order_type, price, quantity): 72 | """ 73 | Does the trader have enough margin or units to trade? 74 | """ 75 | if self.check_order_feasibility: 76 | # logic to check 77 | if order_type in ('market_buy', 'limit_buy'): 78 | if order_type == 'market_buy': 79 | price = book.asks[0][0] 80 | 81 | if self.margin >= price * quantity: 82 | # the trader has enough money 83 | return True 84 | else: 85 | return False 86 | 87 | elif order_type in ('market_sell', 'limit_sell'): 88 | if self.number_units_stock_in_inventory >= quantity: 89 | # the trader has enough shares to sell 90 | return True 91 | else: 92 | return False 93 | 94 | elif order_type in ('modify_limit_buy', 'modify_limit_sell'): 95 | if order_type == 'modify_limit_buy': 96 | o = 'limit_buy' 97 | elif order_type == 'modify_limit_sell': 98 | o = 'limit_sell' 99 | active_limit = [order for order in self.active_orders if order[3] == o] 100 | volume_of_orders_with_right_price = [order[1] for order in active_limit if order[0] == price] 101 | 102 | if sum(volume_of_orders_with_right_price) >= quantity: 103 | return True 104 | else: 105 | return False 106 | 107 | else: 108 | return True 109 | 110 | 111 | else: 112 | # if you don't have to check for feasibility, then the order is always feasible! 113 | return True 114 | 115 | 116 | -------------------------------------------------------------------------------- /notebooks/Hasbrouck_Market_Microstructure/order_book_simulations/utilities.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from matplotlib import pyplot as plt 3 | 4 | 5 | def number_of_decimal_digits(number): 6 | # convert in str 7 | num_str = str(number) 8 | 9 | # find the 'dot' 10 | if '.' in num_str: 11 | # count the digits after the dot 12 | return len(num_str.split('.')[1]) 13 | else: 14 | # no decimals, return 0 15 | return 0 16 | 17 | def add_missing_price_levels(list_with_price_levels, ask_or_bid, ticksize=0.1): 18 | price_level_per_time = {} 19 | 20 | # price levels for each time 21 | for item in list_with_price_levels: 22 | time = item[0] 23 | price_level = item[1] 24 | if time not in price_level_per_time: 25 | price_level_per_time[time] = [] 26 | price_level_per_time[time].append(price_level) 27 | 28 | # generate and add new price levels 29 | new_price_levels = [] 30 | c = number_of_decimal_digits(ticksize) 31 | 32 | for time, price_levels in price_level_per_time.items(): 33 | min_price_level = min(price_levels) 34 | max_price_level = max(price_levels) 35 | 36 | # generate missing price levels 37 | new_price_level = round(min_price_level + ticksize, c) 38 | while new_price_level < max_price_level: 39 | if new_price_level not in price_levels: 40 | # volume is zero 41 | new_price_levels.append([time, new_price_level, 0, ask_or_bid]) 42 | new_price_level = round(new_price_level + ticksize, c) 43 | 44 | list_with_price_levels.extend(new_price_levels) 45 | return list_with_price_levels 46 | 47 | 48 | def plot_order_flow(book_state_sequence: List[List], price_sequence: List =None, volumes_sequence: List =None, buy_sequence: List =None, sell_sequence: List =None, ticksize: float = 1, y_max = None, y_min = None): 49 | """ Plot the sequence of snapshots of the order book, that is the order flow. 50 | Moreover, you can plot the executed trades, volumes and prices. 51 | 52 | inputs: 53 | - book_state_sequence (List[List]): list of lists that contains the sequence of book states. Every state is an order book snapshot. 54 | Every snapshot is made like this: [[time,price_ask,volume,'ask']],[[time,price_bid,volume,'bid']]. 55 | If you have more than one ask or bid, list the asks ascending and bids descending. 56 | Example of a book_state_sequence for 2 timesteps: 57 | [ 58 | [[1,101,2,'ask'],[1,102,7,'ask'],[1,103,2,'ask']],[[1,99,5,'bid'],[1,98,7,'bid'],[1,97,2,'bid']], 59 | [[2,101,4,'ask'],[2,102,7,'ask'],[2,103,2,'ask']],[[2,99,5,'bid'],[2,98,7,'bid'],[2,97,2,'bid']], 60 | ] 61 | 62 | - price_sequence (List): sequence of (executed) prices for the security, i.e. [100,98,98,102] 63 | - volumes_sequence (List): sequence of executed volumes. i.e [8,7,0,3]. This requires a price_sequence. 64 | - buy_sequence (List): sequence with 1 if the executed price is a buy and 0 if it is not. i.e [1,0,0,1]. This requires a price_sequence. 65 | - sell_sequence (List): sequence with 1 if the executed price is a sell and 0 if it is not. i.e [0,1,0,0]. This requires a price_sequence. 66 | Notice that a single entry isn't necessary a buy or a sell. Set it to 0 in both vectors (i.e. the number at the 67 | 3rd place in the example) if the price didn't move (notice that its volume is 0 and the price didn't change) 68 | - ticksize (float): size of minimum tick. if ask or bid are missing between ticks, an order with volume = 0 will be added in that price level 69 | """ 70 | # Step 1: plot the order book in each timestep 71 | 72 | # Get Bid and Ask data in two different lists 73 | ask_data = [item for sublist in book_state_sequence for item in sublist if item[3] == 'ask'] 74 | bid_data = [item for sublist in book_state_sequence for item in sublist if item[3] == 'bid'] 75 | 76 | # add missing price levels 77 | ask_data = add_missing_price_levels(ask_data, ticksize=ticksize, ask_or_bid='ask') 78 | bid_data = add_missing_price_levels(bid_data, ticksize=ticksize, ask_or_bid='bid') 79 | 80 | ask_data = sorted(ask_data, key=lambda x: (x[0], x[1])) 81 | bid_data = sorted(bid_data, key=lambda x: (x[0], -x[1])) 82 | 83 | # get volumes and prices 84 | ask_times, ask_prices, ask_volumes = zip(*[(d[0], d[1], d[2]) for d in ask_data]) 85 | bid_times, bid_prices, bid_volumes = zip(*[(d[0], d[1], d[2]) for d in bid_data]) 86 | 87 | # Normalise the volumes 88 | max_volume = max(max(ask_volumes), max(bid_volumes)) 89 | norm_ask_volumes = [v * ticksize / max_volume for v in ask_volumes] 90 | norm_bid_volumes = [v * ticksize / max_volume for v in bid_volumes] 91 | 92 | fig, ax = plt.subplots() 93 | 94 | # Plot the asks 95 | for t, p, v in zip(ask_times, ask_prices, norm_ask_volumes): 96 | ax.bar(t, v, width=1, bottom=p, color='blue', alpha=0.6) 97 | 98 | # plot the bids 99 | for t, p, v in zip(bid_times, bid_prices, norm_bid_volumes): 100 | ax.bar(t, -v, width=1, bottom=p, color='red', alpha=0.6) 101 | 102 | # add some gridlines 103 | prices = sorted(set(ask_prices + bid_prices)) 104 | for price in prices: 105 | ax.axhline(y=price, color='grey', linestyle='--', linewidth=0.5) 106 | 107 | # Fill the area between the highest bid and the lowest ask for each time 108 | # this is the bid ask spread 109 | unique_times = sorted(set(ask_times + bid_times)) 110 | for t in unique_times: 111 | ask_prices_at_t = [p for time, p in zip(ask_times, ask_prices) if time == t] 112 | bid_prices_at_t = [p for time, p in zip(bid_times, bid_prices) if time == t] 113 | 114 | if ask_prices_at_t and bid_prices_at_t: 115 | min_ask_price = min(ask_prices_at_t) 116 | max_bid_price = max(bid_prices_at_t) 117 | ax.fill_between([t - 1/2, t + 1/2], max_bid_price, min_ask_price, color='yellow', alpha=0.3, edgecolor='none') 118 | 119 | 120 | # axes labels 121 | ax.set_xlabel('Time') 122 | ax.set_ylabel('Price') 123 | ax.set_title('Order Flow') 124 | 125 | # set axes limits 126 | ax.set_xlim(min(ask_times + bid_times), max(ask_times + bid_times)) 127 | 128 | # Step 2: plot the prices 129 | 130 | if price_sequence: 131 | x, y = zip(*[[i + 1, v] for (i, v) in enumerate(price_sequence)]) 132 | ax.plot(x, y, color='orange', label='price') 133 | 134 | 135 | if volumes_sequence and buy_sequence and sell_sequence: 136 | ax.scatter(x, y, s=[val * 20 for val in [v * b for v,b in zip(volumes_sequence, buy_sequence)]], alpha=1, edgecolors='black', color='green') 137 | ax.scatter(x, y, s=[val * 20 for val in [v * s for v,s in zip(volumes_sequence, sell_sequence)]], alpha=1, edgecolors='black', color='red') 138 | 139 | #ax.legend(loc='upper right') 140 | 141 | if y_min and y_max: 142 | plt.ylim(y_min, y_max) 143 | 144 | plt.show() 145 | -------------------------------------------------------------------------------- /notebooks/Hasbrouck_Market_Microstructure/roll_model_relaxing_of_assumptions.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "This notebook has been used to produce plots for my article on the relaxing of linear regression assumptions under the Roll model [(find it here)](https://medium.com/@lu.battistoni/relaxing-linear-regression-assumptions-a-roll-model-application-59e310dde6ce)\n", 8 | "\n", 9 | "*I sadly forgot to set a seed when generating the plots for the article, so my plots here are slightly different*" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 73, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "# imports\n", 19 | "import pandas as pd\n", 20 | "import numpy as np\n", 21 | "\n", 22 | "import statsmodels.api as sm\n", 23 | "import matplotlib.pyplot as plt\n", 24 | "from scipy.stats import pearsonr" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 74, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "def generate_correlated_normals(mean1, std1, mean2, std2, correlation, size=1):\n", 34 | " \"\"\"\n", 35 | " Generate correlated normal random variables.\n", 36 | "\n", 37 | " Parameters:\n", 38 | " - mean1 (float): Mean of the first normal distribution.\n", 39 | " - std1 (float): Standard deviation of the first normal distribution.\n", 40 | " - mean2 (float): Mean of the second normal distribution.\n", 41 | " - std2 (float): Standard deviation of the second normal distribution.\n", 42 | " - correlation (float): Correlation coefficient between the two normal distributions.\n", 43 | " - size (int, optional): Number of samples to generate. Default is 1.\n", 44 | "\n", 45 | " Returns:\n", 46 | " - tuple: A tuple containing two arrays of correlated normal random variables.\n", 47 | " \"\"\"\n", 48 | " # Calculate the covariance matrix\n", 49 | " cov_matrix = np.array([[std1**2, correlation * std1 * std2],\n", 50 | " [correlation * std1 * std2, std2**2]])\n", 51 | " # Set the means\n", 52 | " mean = [mean1, mean2]\n", 53 | " # Generate correlated normal random variables\n", 54 | " correlated_normals = np.random.multivariate_normal(mean, cov_matrix, size=size).T\n", 55 | " # Return the arrays of correlated normal random variables\n", 56 | " return correlated_normals[0], correlated_normals[1]\n", 57 | "\n", 58 | "\n", 59 | "def AR1_process_roll_model(phi, sigma_v, T):\n", 60 | " \"\"\"\n", 61 | " Generate a path of an autoregressive process of order 1 (AR(1)).\n", 62 | " This is under the Roll Model assumptions, so the values of the\n", 63 | " process can either be -1 or 1.\n", 64 | " \n", 65 | " Parameters:\n", 66 | " - phi (float): The autoregressive parameter.\n", 67 | " - sigma_v (float): The standard deviation of the white noise.\n", 68 | " - T (int): The number of time steps.\n", 69 | " \n", 70 | " Returns:\n", 71 | " - numpy.ndarray: An array containing the generated path.\n", 72 | " \"\"\"\n", 73 | " # Initialize an array to store the path\n", 74 | " path = np.zeros(T)\n", 75 | "\n", 76 | " # Generate the first value of the path\n", 77 | " path[0] = np.sign(np.random.uniform(-1, 1))\n", 78 | " \n", 79 | " # Generate the remaining values of the path\n", 80 | " for t in range(1, T):\n", 81 | " # Generate the white noise term\n", 82 | " v_t = np.random.normal(loc=0, scale=sigma_v)\n", 83 | " \n", 84 | " # Calculate the value of the AR(1) process\n", 85 | " value = phi * path[t-1] + v_t\n", 86 | " \n", 87 | " # Take the sign of the value to get +1 or -1\n", 88 | " path[t] = np.sign(value)\n", 89 | "\n", 90 | " return path\n", 91 | "\n", 92 | "\n", 93 | "def generate_binary_correlated_variables(corr, size, err):\n", 94 | " \"\"\"\n", 95 | " Generate binary correlated variables with a specified correlation coefficient.\n", 96 | "\n", 97 | " Parameters:\n", 98 | " - corr (float): Desired correlation coefficient between the two variables.\n", 99 | " - size (int): Number of samples to generate.\n", 100 | " - err (float): Maximum allowable error in correlation coefficient.\n", 101 | "\n", 102 | " Returns:\n", 103 | " - tuple: A tuple containing two arrays of binary correlated variables.\n", 104 | " \"\"\"\n", 105 | " # Generate the first binary variable\n", 106 | " x = np.sign(np.random.normal(0, 1, size=size))\n", 107 | " \n", 108 | " # Introduce a shift in x to create correlation with y\n", 109 | " x_shifted = np.roll(x, 1)\n", 110 | " x_shifted[0] = 1\n", 111 | " \n", 112 | " # Adjust the variables until the desired correlation is achieved within the specified error\n", 113 | " # This code is not particularly efficient, but this is out of our notebook scope\n", 114 | " while abs(np.corrcoef(x, x_shifted)[0, 1] - corr) > err:\n", 115 | " # Generate a new set of random variables\n", 116 | " x = np.sign(np.random.normal(0, 1, size=size))\n", 117 | " x_shifted = np.roll(x, 1)\n", 118 | " x_shifted[0] = 1\n", 119 | " \n", 120 | " return x, x_shifted" 121 | ] 122 | }, 123 | { 124 | "cell_type": "code", 125 | "execution_count": 75, 126 | "metadata": {}, 127 | "outputs": [], 128 | "source": [ 129 | "# parameters\n", 130 | "size = 2000 # number of datapoints\n", 131 | "c = 0.8\n", 132 | "sigma_v = 1.0\n", 133 | "sigma_u = 1.0" 134 | ] 135 | }, 136 | { 137 | "cell_type": "markdown", 138 | "metadata": {}, 139 | "source": [ 140 | "OLS Regression Results of non correlated processes - Estimation of Roll model parameter" 141 | ] 142 | }, 143 | { 144 | "cell_type": "code", 145 | "execution_count": 76, 146 | "metadata": {}, 147 | "outputs": [ 148 | { 149 | "name": "stdout", 150 | "output_type": "stream", 151 | "text": [ 152 | " OLS Regression Results \n", 153 | "==============================================================================\n", 154 | "Dep. Variable: delta_p_t R-squared: 0.570\n", 155 | "Model: OLS Adj. R-squared: 0.570\n", 156 | "Method: Least Squares F-statistic: 2647.\n", 157 | "Date: Mon, 29 Apr 2024 Prob (F-statistic): 0.00\n", 158 | "Time: 22:46:52 Log-Likelihood: -2842.0\n", 159 | "No. Observations: 2000 AIC: 5688.\n", 160 | "Df Residuals: 1998 BIC: 5699.\n", 161 | "Df Model: 1 \n", 162 | "Covariance Type: nonrobust \n", 163 | "==============================================================================\n", 164 | " coef std err t P>|t| [0.025 0.975]\n", 165 | "------------------------------------------------------------------------------\n", 166 | "const 0.0179 0.022 0.796 0.426 -0.026 0.062\n", 167 | "delta_q_t 0.8156 0.016 51.450 0.000 0.785 0.847\n", 168 | "==============================================================================\n", 169 | "Omnibus: 8.251 Durbin-Watson: 1.979\n", 170 | "Prob(Omnibus): 0.016 Jarque-Bera (JB): 6.496\n", 171 | "Skew: -0.031 Prob(JB): 0.0389\n", 172 | "Kurtosis: 2.728 Cond. No. 1.41\n", 173 | "==============================================================================\n", 174 | "\n", 175 | "Notes:\n", 176 | "[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.\n" 177 | ] 178 | }, 179 | { 180 | "data": { 181 | "image/png": "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", 182 | "text/plain": [ 183 | "
" 184 | ] 185 | }, 186 | "metadata": { 187 | "needs_background": "light" 188 | }, 189 | "output_type": "display_data" 190 | } 191 | ], 192 | "source": [ 193 | "phi = 0 # AR(1) coefficient\n", 194 | "\n", 195 | "# noise term\n", 196 | "u_t = np.random.normal(loc=0, scale=sigma_u, size=size)\n", 197 | "\n", 198 | "# AR(1) process for transactions direction\n", 199 | "q_t = AR1_process_roll_model(phi, sigma_v, size)\n", 200 | "\n", 201 | "# write everything in a dataframe for better readability\n", 202 | "df = pd.DataFrame({'q_t': q_t, 'u_t': u_t})\n", 203 | "df['q_t-1'] = df['q_t'].shift()\n", 204 | "\n", 205 | "# let's assume that the first transaction is a buy\n", 206 | "df = df.fillna(1)\n", 207 | "\n", 208 | "# difference in subsequent transactions directions\n", 209 | "df['delta_q_t'] = df['q_t'] - df['q_t-1']\n", 210 | "\n", 211 | "# difference in subsequent transaction prices\n", 212 | "df['delta_p_t'] = c * df['delta_q_t'] + df['u_t']\n", 213 | "\n", 214 | "# plot\n", 215 | "fig = plt.figure()\n", 216 | "df['delta_p_t'].hist()\n", 217 | "plt.title('delta p_t histogram - not correlated processes')\n", 218 | "\n", 219 | "x = sm.add_constant(df['delta_q_t'])\n", 220 | "\n", 221 | "# Fit OLS model\n", 222 | "model = sm.OLS(df['delta_p_t'], x).fit()\n", 223 | "\n", 224 | "# Print model summary\n", 225 | "print(model.summary())" 226 | ] 227 | }, 228 | { 229 | "cell_type": "code", 230 | "execution_count": 77, 231 | "metadata": {}, 232 | "outputs": [ 233 | { 234 | "name": "stdout", 235 | "output_type": "stream", 236 | "text": [ 237 | "Simulated estimate for c = 0.8156\n", 238 | "Theoretical estimate for c = 0.8\n" 239 | ] 240 | } 241 | ], 242 | "source": [ 243 | "print(f\"Simulated estimate for c = {round(model.params['delta_q_t'], 4)}\")\n", 244 | "print(f\"Theoretical estimate for c = {c}\")" 245 | ] 246 | }, 247 | { 248 | "cell_type": "code", 249 | "execution_count": 78, 250 | "metadata": {}, 251 | "outputs": [ 252 | { 253 | "name": "stdout", 254 | "output_type": "stream", 255 | "text": [ 256 | "The variance of the simulated process is 2.3358\n", 257 | "The theoretical variance is 2.28\n" 258 | ] 259 | } 260 | ], 261 | "source": [ 262 | "print(f\"The variance of the simulated process is {round(df['delta_p_t'].var(), 4)}\")\n", 263 | "print(f\"The theoretical variance is {round(1 + 2*(c**2), 4)}\")" 264 | ] 265 | }, 266 | { 267 | "cell_type": "markdown", 268 | "metadata": {}, 269 | "source": [ 270 | "Violation of no autocorrelation assumption" 271 | ] 272 | }, 273 | { 274 | "cell_type": "code", 275 | "execution_count": 79, 276 | "metadata": {}, 277 | "outputs": [ 278 | { 279 | "name": "stdout", 280 | "output_type": "stream", 281 | "text": [ 282 | "Empirical correlation between q_t and q_t-1: 0.04995344771893839\n", 283 | " OLS Regression Results \n", 284 | "==============================================================================\n", 285 | "Dep. Variable: delta_p_t R-squared: 0.535\n", 286 | "Model: OLS Adj. R-squared: 0.535\n", 287 | "Method: Least Squares F-statistic: 2301.\n", 288 | "Date: Mon, 29 Apr 2024 Prob (F-statistic): 0.00\n", 289 | "Time: 22:46:53 Log-Likelihood: -2840.5\n", 290 | "No. Observations: 2000 AIC: 5685.\n", 291 | "Df Residuals: 1998 BIC: 5696.\n", 292 | "Df Model: 1 \n", 293 | "Covariance Type: nonrobust \n", 294 | "==============================================================================\n", 295 | " coef std err t P>|t| [0.025 0.975]\n", 296 | "------------------------------------------------------------------------------\n", 297 | "const 0.0295 0.022 1.315 0.189 -0.014 0.073\n", 298 | "delta_q_t 0.7795 0.016 47.966 0.000 0.748 0.811\n", 299 | "==============================================================================\n", 300 | "Omnibus: 3.008 Durbin-Watson: 1.956\n", 301 | "Prob(Omnibus): 0.222 Jarque-Bera (JB): 3.055\n", 302 | "Skew: 0.093 Prob(JB): 0.217\n", 303 | "Kurtosis: 2.951 Cond. No. 1.38\n", 304 | "==============================================================================\n", 305 | "\n", 306 | "Notes:\n", 307 | "[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.\n" 308 | ] 309 | }, 310 | { 311 | "data": { 312 | "image/png": "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", 313 | "text/plain": [ 314 | "
" 315 | ] 316 | }, 317 | "metadata": { 318 | "needs_background": "light" 319 | }, 320 | "output_type": "display_data" 321 | } 322 | ], 323 | "source": [ 324 | "# Wanted correlation\n", 325 | "phi = 0.05\n", 326 | "err = 0.0001 # earling stopping condition\n", 327 | "\n", 328 | "# Generate autocorrelated variables with correlation phi\n", 329 | "q_t, q_t_1 = generate_binary_correlated_variables(phi, size, err)\n", 330 | "\n", 331 | "# Empirical correlation\n", 332 | "empirical_corr = np.corrcoef(q_t, q_t_1)[0, 1]\n", 333 | "print(\"Empirical correlation between q_t and q_t-1:\", empirical_corr)\n", 334 | "\n", 335 | "# simulate the process\n", 336 | "u_t = np.random.normal(loc=0, scale=sigma_u, size=size)\n", 337 | "df = pd.DataFrame({'q_t': q_t, 'q_t-1': q_t_1, 'u_t': u_t})\n", 338 | "df['delta_q_t'] = df['q_t'] - df['q_t-1']\n", 339 | "df['delta_p_t'] = c * df['delta_q_t'] + df['u_t']\n", 340 | "\n", 341 | "# plot\n", 342 | "fig = plt.figure()\n", 343 | "df['delta_p_t'].hist()\n", 344 | "plt.title('delta p_t histogram - p_t and p_t-1 correlated')\n", 345 | "\n", 346 | "x = sm.add_constant(df['delta_q_t'])\n", 347 | "\n", 348 | "# Fit OLS model\n", 349 | "model = sm.OLS(df['delta_p_t'], x).fit()\n", 350 | "\n", 351 | "# Print model summary\n", 352 | "print(model.summary())" 353 | ] 354 | }, 355 | { 356 | "cell_type": "code", 357 | "execution_count": 80, 358 | "metadata": {}, 359 | "outputs": [ 360 | { 361 | "name": "stdout", 362 | "output_type": "stream", 363 | "text": [ 364 | "Simulated estimate for c hat = 0.7795\n", 365 | "Theoretical estimate for c hat = 0.7589\n" 366 | ] 367 | } 368 | ], 369 | "source": [ 370 | "print(f\"Simulated estimate for c hat = {round(model.params['delta_q_t'], 4)}\")\n", 371 | "print(f\"Theoretical estimate for c hat = {round(c * np.sqrt(1 - 2*phi), 4)}\")" 372 | ] 373 | }, 374 | { 375 | "cell_type": "code", 376 | "execution_count": 81, 377 | "metadata": {}, 378 | "outputs": [ 379 | { 380 | "name": "stdout", 381 | "output_type": "stream", 382 | "text": [ 383 | "The variance of the simulated process is 2.1583\n", 384 | "The theoretical variance is 2.216\n" 385 | ] 386 | } 387 | ], 388 | "source": [ 389 | "print(f\"The variance of the simulated process is {round(df['delta_p_t'].var(), 4)}\")\n", 390 | "print(f\"The theoretical variance is {round(1 + 2*c*c*(1-phi), 4)}\")" 391 | ] 392 | }, 393 | { 394 | "cell_type": "code", 395 | "execution_count": 82, 396 | "metadata": {}, 397 | "outputs": [ 398 | { 399 | "name": "stdout", 400 | "output_type": "stream", 401 | "text": [ 402 | "The Durbin Watson statistics is 1.898\n" 403 | ] 404 | } 405 | ], 406 | "source": [ 407 | "print(f\"The Durbin Watson statistics is {round(sm.stats.stattools.durbin_watson(q_t), 4)}\")" 408 | ] 409 | }, 410 | { 411 | "cell_type": "markdown", 412 | "metadata": {}, 413 | "source": [ 414 | "Violation of error and independent variable independency" 415 | ] 416 | }, 417 | { 418 | "cell_type": "code", 419 | "execution_count": 83, 420 | "metadata": {}, 421 | "outputs": [ 422 | { 423 | "name": "stdout", 424 | "output_type": "stream", 425 | "text": [ 426 | " OLS Regression Results \n", 427 | "==============================================================================\n", 428 | "Dep. Variable: delta_p_t R-squared: 0.774\n", 429 | "Model: OLS Adj. R-squared: 0.774\n", 430 | "Method: Least Squares F-statistic: 6834.\n", 431 | "Date: Mon, 29 Apr 2024 Prob (F-statistic): 0.00\n", 432 | "Time: 22:46:54 Log-Likelihood: -2661.5\n", 433 | "No. Observations: 2000 AIC: 5327.\n", 434 | "Df Residuals: 1998 BIC: 5338.\n", 435 | "Df Model: 1 \n", 436 | "Covariance Type: nonrobust \n", 437 | "==============================================================================\n", 438 | " coef std err t P>|t| [0.025 0.975]\n", 439 | "------------------------------------------------------------------------------\n", 440 | "const -0.0042 0.020 -0.203 0.839 -0.044 0.036\n", 441 | "delta_q_t 0.9947 0.012 82.670 0.000 0.971 1.018\n", 442 | "==============================================================================\n", 443 | "Omnibus: 3.664 Durbin-Watson: 1.743\n", 444 | "Prob(Omnibus): 0.160 Jarque-Bera (JB): 3.216\n", 445 | "Skew: -0.013 Prob(JB): 0.200\n", 446 | "Kurtosis: 2.805 Cond. No. 1.70\n", 447 | "==============================================================================\n", 448 | "\n", 449 | "Notes:\n", 450 | "[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.\n" 451 | ] 452 | }, 453 | { 454 | "data": { 455 | "image/png": "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", 456 | "text/plain": [ 457 | "
" 458 | ] 459 | }, 460 | "metadata": { 461 | "needs_background": "light" 462 | }, 463 | "output_type": "display_data" 464 | } 465 | ], 466 | "source": [ 467 | "# Parameters of two normals\n", 468 | "mean1 = 0\n", 469 | "std1 = 1\n", 470 | "mean2 = 0\n", 471 | "std2 = 1\n", 472 | "correlation = 0.5 # given correlation between distributions\n", 473 | "\n", 474 | "path = np.zeros(size)\n", 475 | "\n", 476 | "# Generate two correlated normals\n", 477 | "q_t, u_t = generate_correlated_normals(mean1, std1, mean2, std2, correlation, size)\n", 478 | "\n", 479 | "# generate process paths with correlated normal random variables\n", 480 | "# this is as before, but with the difference that we generated correlated random variables\n", 481 | "path[0] = np.sign(np.random.uniform(-1,1))\n", 482 | "for t in range(1, size):\n", 483 | " value = q_t[t] - path[t-1] + u_t[t]\n", 484 | " path[t] = np.sign(value)\n", 485 | " \n", 486 | "df = pd.DataFrame({'q_t': path, 'u_t': u_t})\n", 487 | "df['q_t-1'] = df['q_t'].shift()\n", 488 | "df = df.fillna(1)\n", 489 | "df['delta_q_t'] = df['q_t'] - df['q_t-1']\n", 490 | "df['delta_p_t'] = c * df['delta_q_t'] + df['u_t']\n", 491 | "\n", 492 | "# plot\n", 493 | "fig = plt.figure()\n", 494 | "df['delta_p_t'].hist()\n", 495 | "plt.title('delta p_t histogram - q_t and u_t correlated')\n", 496 | "\n", 497 | "x = sm.add_constant(df['delta_q_t'])\n", 498 | "\n", 499 | "# Fit OLS model\n", 500 | "model = sm.OLS(df['delta_p_t'], x).fit()\n", 501 | "\n", 502 | "# Print model summary\n", 503 | "print(model.summary())" 504 | ] 505 | }, 506 | { 507 | "cell_type": "code", 508 | "execution_count": 84, 509 | "metadata": {}, 510 | "outputs": [ 511 | { 512 | "name": "stdout", 513 | "output_type": "stream", 514 | "text": [ 515 | "Simulated estimate for c hat = 0.9947\n", 516 | "Theoretical estimate for c hat = 1.0198\n" 517 | ] 518 | } 519 | ], 520 | "source": [ 521 | "print(f\"Simulated estimate for c hat = {round(model.params['delta_q_t'], 4)}\")\n", 522 | "print(f\"Theoretical estimate for c hat = {round(np.sqrt(c * (correlation + c)), 4)}\")" 523 | ] 524 | }, 525 | { 526 | "cell_type": "code", 527 | "execution_count": 85, 528 | "metadata": {}, 529 | "outputs": [ 530 | { 531 | "name": "stdout", 532 | "output_type": "stream", 533 | "text": [ 534 | "The variance of the simulated process is 3.7076\n", 535 | "The theoretical variance is 3.08\n" 536 | ] 537 | } 538 | ], 539 | "source": [ 540 | "print(f\"The variance of the simulated process is {round(df['delta_p_t'].var(), 4)}\")\n", 541 | "print(f\"The theoretical variance is {round(2*c*c + 1 + 2*c*correlation*1, 4)}\")" 542 | ] 543 | }, 544 | { 545 | "cell_type": "code", 546 | "execution_count": 86, 547 | "metadata": {}, 548 | "outputs": [ 549 | { 550 | "name": "stdout", 551 | "output_type": "stream", 552 | "text": [ 553 | "Correlation between independent variable and residuals: 0.5827\n", 554 | "Correlation test p-value: 0.0\n" 555 | ] 556 | } 557 | ], 558 | "source": [ 559 | "corr, p_value = pearsonr(df['q_t'], df['u_t'])\n", 560 | "print(f\"Correlation between independent variable and residuals: {round(corr, 4)}\")\n", 561 | "print(f\"Correlation test p-value: {round(p_value, 4)}\")" 562 | ] 563 | }, 564 | { 565 | "cell_type": "code", 566 | "execution_count": null, 567 | "metadata": {}, 568 | "outputs": [], 569 | "source": [] 570 | } 571 | ], 572 | "metadata": { 573 | "kernelspec": { 574 | "display_name": "Python 3", 575 | "language": "python", 576 | "name": "python3" 577 | }, 578 | "language_info": { 579 | "codemirror_mode": { 580 | "name": "ipython", 581 | "version": 3 582 | }, 583 | "file_extension": ".py", 584 | "mimetype": "text/x-python", 585 | "name": "python", 586 | "nbconvert_exporter": "python", 587 | "pygments_lexer": "ipython3", 588 | "version": "3.9.10" 589 | } 590 | }, 591 | "nbformat": 4, 592 | "nbformat_minor": 2 593 | } 594 | -------------------------------------------------------------------------------- /notebooks/finance_notebooks/bybit_flow_analysis/.gitignore: -------------------------------------------------------------------------------- 1 | *.txt -------------------------------------------------------------------------------- /notebooks/finance_notebooks/bybit_flow_analysis/format_flow_from_bybit.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "This notebook shows how to format order book data (data category \"OB Data\") downloaded from https://www.bybit.com/derivatives/en/history-data" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 25, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "import pandas as pd\n", 17 | "import json\n", 18 | "import matplotlib.pyplot as plt\n", 19 | "from tqdm import tqdm\n", 20 | "import numpy as np\n" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 26, 26 | "metadata": {}, 27 | "outputs": [ 28 | { 29 | "name": "stdout", 30 | "output_type": "stream", 31 | "text": [ 32 | "863839\n" 33 | ] 34 | } 35 | ], 36 | "source": [ 37 | "# file name\n", 38 | "flow_file = '2024-09-03_BTCUSDT_ob500.txt'\n", 39 | "\n", 40 | "\n", 41 | "# read the number of rows in file\n", 42 | "def number_of_rows(file_path):\n", 43 | " with open(file_path, 'r') as file:\n", 44 | " rows = file.readlines()\n", 45 | " return len(rows)\n", 46 | "\n", 47 | "# maximum number of rows\n", 48 | "max_n_of_rows = number_of_rows(flow_file)\n", 49 | "print(max_n_of_rows)\n" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": 27, 55 | "metadata": {}, 56 | "outputs": [ 57 | { 58 | "data": { 59 | "text/plain": [ 60 | "864000" 61 | ] 62 | }, 63 | "execution_count": 27, 64 | "metadata": {}, 65 | "output_type": "execute_result" 66 | } 67 | ], 68 | "source": [ 69 | "# number of datapoints in a day\n", 70 | "60 * 60 * 24 * 10" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": 28, 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "# number of rows of file to process\n", 80 | "rows_to_process = 10000\n", 81 | "\n", 82 | "# maximum bid and ask depth\n", 83 | "max_depth = 20\n", 84 | "\n", 85 | "rows_to_process = min(rows_to_process, max_n_of_rows)" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": 29, 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "def read_data(flow_file):\n", 95 | " dict_a = {} # dictionary with ask data\n", 96 | " dict_b = {} # dictionary with bid data\n", 97 | "\n", 98 | " # read row by row the txt file\n", 99 | " with open(flow_file, 'r') as file:\n", 100 | " for n in tqdm(range(rows_to_process)):\n", 101 | "\n", 102 | " riga = file.readline()\n", 103 | " riga = riga.strip().strip(\"'\")\n", 104 | " obj = json.loads(riga) # it's a json object\n", 105 | "\n", 106 | " # get the data\n", 107 | " ts = obj['ts']\n", 108 | " a = obj['data']['a']\n", 109 | " b = obj['data']['b']\n", 110 | "\n", 111 | " # if data is empty continue\n", 112 | " if (not a) or (not b):\n", 113 | " continue\n", 114 | " \n", 115 | " # populate the dictionaries\n", 116 | " dict_a[ts] = a[:max_depth]\n", 117 | " dict_b[ts] = b[:max_depth]\n", 118 | "\n", 119 | " return dict_a, dict_b\n", 120 | "\n", 121 | "\n", 122 | "def build_order_flow_dataframe(dict_a, dict_b):\n", 123 | "\n", 124 | " def format_df(dictionary):\n", 125 | " # build a dataframe from dictionary\n", 126 | " df = pd.DataFrame(pd.DataFrame().from_dict(dictionary, orient='index').stack())\n", 127 | "\n", 128 | " df = pd.DataFrame(df[0].tolist(), index=df.index)\n", 129 | " df = df.rename(columns={0: 'price', 1: 'volume'})\n", 130 | "\n", 131 | " return df\n", 132 | " \n", 133 | " ask_df = format_df(dict_a)\n", 134 | " ask_df['side'] = 'ask'\n", 135 | "\n", 136 | " bid_df = format_df(dict_b)\n", 137 | " bid_df['side'] = 'bid'\n", 138 | "\n", 139 | " flow_df = pd.concat([ask_df, bid_df])\n", 140 | " flow_df = flow_df.reset_index()\n", 141 | " flow_df['level_0'] = pd.to_datetime(flow_df['level_0'], unit='ms')\n", 142 | " flow_df = flow_df.rename(columns={'level_0': 'time', 'level_1': 'level'})\n", 143 | "\n", 144 | " flow_df = flow_df.set_index(['time', 'side', 'level'])\n", 145 | " flow_df = flow_df.sort_index()\n", 146 | "\n", 147 | " flow_df['price'] = flow_df['price'].astype(float)\n", 148 | " flow_df['volume'] = flow_df['volume'].astype(float)\n", 149 | "\n", 150 | " return flow_df\n", 151 | "\n", 152 | "\n", 153 | "def plot_order_book(df):\n", 154 | " # sort to prepare to cumsum the volumes\n", 155 | " bid_data = df.loc['bid'].sort_values(by='price', ascending=False)\n", 156 | " ask_data = df.loc['ask'].sort_values(by='price', ascending=True)\n", 157 | "\n", 158 | " # cumulative volumes\n", 159 | " bid_data['cumulative_volume'] = bid_data['volume'].cumsum()\n", 160 | " ask_data['cumulative_volume'] = ask_data['volume'].cumsum()\n", 161 | "\n", 162 | " # useful to fill price gaps\n", 163 | " price_range_bid = np.linspace(bid_data['price'].max(), bid_data['price'].min(),\n", 164 | " num=int(round(bid_data['price'].max() - bid_data['price'].min(), 1) * 10) + 1)\n", 165 | "\n", 166 | " price_range_ask = np.linspace(ask_data['price'].min(), ask_data['price'].max(),\n", 167 | " num=int(round(ask_data['price'].max() - ask_data['price'].min(), 1) * 10) + 1)\n", 168 | "\n", 169 | " # sort again the bid data\n", 170 | " bid_data = bid_data.sort_index(ascending=True)\n", 171 | "\n", 172 | "\n", 173 | " fig = plt.figure()\n", 174 | "\n", 175 | " # build a dataframe for each cum volume, in order to fill the gaps\n", 176 | " cum_vol_bid = pd.DataFrame({'price': price_range_bid}).set_index('price').join(\n", 177 | " bid_data.set_index('price')['cumulative_volume'], how='left').ffill()\n", 178 | "\n", 179 | " cum_vol_ask = pd.DataFrame({'price': price_range_ask}).set_index('price').join(\n", 180 | " ask_data.set_index('price')['cumulative_volume'], how='left').ffill()\n", 181 | "\n", 182 | " # now plot\n", 183 | " plt.figure(figsize=(10, 6))\n", 184 | "\n", 185 | " # bid\n", 186 | " plt.plot(cum_vol_bid.index, cum_vol_bid['cumulative_volume'], color='green', label='Bid')\n", 187 | " plt.fill_between(cum_vol_bid.index, cum_vol_bid['cumulative_volume'], color='green', alpha=0.3)\n", 188 | "\n", 189 | " # ask\n", 190 | " plt.plot(cum_vol_ask.index, cum_vol_ask['cumulative_volume'], color='red', label='Ask')\n", 191 | " plt.fill_between(cum_vol_ask.index, cum_vol_ask['cumulative_volume'], color='red', alpha=0.3)\n", 192 | "\n", 193 | " # labels and title\n", 194 | " plt.ylabel('Cumulative Volume [BTC]')\n", 195 | " plt.xlabel('Price [USDT]')\n", 196 | " plt.title('Market Depth')\n", 197 | " plt.legend()\n", 198 | "\n", 199 | " plt.show()" 200 | ] 201 | }, 202 | { 203 | "cell_type": "code", 204 | "execution_count": 30, 205 | "metadata": {}, 206 | "outputs": [ 207 | { 208 | "name": "stderr", 209 | "output_type": "stream", 210 | "text": [ 211 | "100%|██████████| 10000/10000 [00:01<00:00, 7998.38it/s]\n" 212 | ] 213 | } 214 | ], 215 | "source": [ 216 | "dict_a, dict_b = read_data(flow_file)\n", 217 | "flow_df = build_order_flow_dataframe(dict_a, dict_b)" 218 | ] 219 | }, 220 | { 221 | "cell_type": "code", 222 | "execution_count": 31, 223 | "metadata": {}, 224 | "outputs": [ 225 | { 226 | "data": { 227 | "text/html": [ 228 | "
\n", 229 | "\n", 242 | "\n", 243 | " \n", 244 | " \n", 245 | " \n", 246 | " \n", 247 | " \n", 248 | " \n", 249 | " \n", 250 | " \n", 251 | " \n", 252 | " \n", 253 | " \n", 254 | " \n", 255 | " \n", 256 | " \n", 257 | " \n", 258 | " \n", 259 | " \n", 260 | " \n", 261 | " \n", 262 | " \n", 263 | " \n", 264 | " \n", 265 | " \n", 266 | " \n", 267 | " \n", 268 | " \n", 269 | " \n", 270 | " \n", 271 | " \n", 272 | " \n", 273 | " \n", 274 | " \n", 275 | " \n", 276 | " \n", 277 | " \n", 278 | " \n", 279 | " \n", 280 | " \n", 281 | " \n", 282 | " \n", 283 | " \n", 284 | " \n", 285 | " \n", 286 | " \n", 287 | " \n", 288 | "
pricevolume
timesidelevel
2024-09-03 00:00:01.755ask059116.86.618
159117.10.001
259117.20.017
359117.30.002
459117.60.050
\n", 289 | "
" 290 | ], 291 | "text/plain": [ 292 | " price volume\n", 293 | "time side level \n", 294 | "2024-09-03 00:00:01.755 ask 0 59116.8 6.618\n", 295 | " 1 59117.1 0.001\n", 296 | " 2 59117.2 0.017\n", 297 | " 3 59117.3 0.002\n", 298 | " 4 59117.6 0.050" 299 | ] 300 | }, 301 | "execution_count": 31, 302 | "metadata": {}, 303 | "output_type": "execute_result" 304 | } 305 | ], 306 | "source": [ 307 | "flow_df.head()" 308 | ] 309 | }, 310 | { 311 | "cell_type": "code", 312 | "execution_count": 32, 313 | "metadata": {}, 314 | "outputs": [ 315 | { 316 | "data": { 317 | "text/plain": [ 318 | "
" 319 | ] 320 | }, 321 | "metadata": {}, 322 | "output_type": "display_data" 323 | }, 324 | { 325 | "data": { 326 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAA0oAAAIjCAYAAAA9VuvLAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8WgzjOAAAACXBIWXMAAA9hAAAPYQGoP6dpAABZ6UlEQVR4nO3deVxU9cLH8e+wLwJuCKIoqLjhgksuWaa555KlaWblUtntaqa2mLdM7VbaLZcWs11zqdRSK8vtqmmWlhtamjukqWimgoACDuf5g8e5MYAxOMMZ8PN+XueVc9bvzOvcJ7+dc37HYhiGIQAAAACAjYfZAQAAAADA3VCUAAAAAMAORQkAAAAA7FCUAAAAAMAORQkAAAAA7FCUAAAAAMAORQkAAAAA7FCUAAAAAMAORQkAAAAA7FCUAABuITExURaLRa+++qrZUdxSVFSUevToYXYMALhuUJQAADZz5syRxWKRxWLRpk2b8iw3DEORkZGyWCxu/5f2jz/+WDNmzCj0+lFRUbbv7uHhobJly6phw4YaNmyYfvzxR9cF/Yu9e/dq4sSJSkxMLJbjAQAKRlECAOTh5+enjz/+OM/8DRs26Pfff5evr68JqRzjaFGSpLi4OM2bN09z587V5MmT1b59e3311Vdq1aqVxowZ45qgf7F3715NmjSJogQAbsDL7AAAAPdz2223afHixXr99dfl5fW/f1V8/PHHatasmc6cOeO0Y2VnZyszM9Np+7sWVapU0b333ptr3ssvv6x77rlH06dPV0xMjB555BGT0gEAihNXlAAAeQwYMEB//vmn1qxZY5uXmZmpzz77TPfcc0++27z66qu68cYbVaFCBfn7+6tZs2b67LPP8qxnsVg0YsQILViwQLGxsfL19dXKlSvz3adhGBo2bJh8fHy0ZMkS2/z58+erWbNm8vf3V/ny5XX33Xfr2LFjtuXt2rXT119/rd9++812O11UVFSRfgt/f3/NmzdP5cuX14svvijDMGzLsrOzNWPGDMXGxsrPz09hYWF6+OGHde7cuVz7uPJ80erVqxUXFyc/Pz/Vr18/13eaM2eO7rrrLklS+/btbbm//fbbXPvatGmTWrRoIT8/P9WoUUNz584t0vcCAFwdRQkAkEdUVJRat26tTz75xDZvxYoVSk5O1t13353vNq+99pqaNGmi559/Xi+99JK8vLx011136euvv86z7rp16zR69Gj1799fr732Wr4lxmq1avDgwZo7d66WLl2qO++8U5L04osv6v7771dMTIymTZumUaNGae3atWrbtq3Onz8vSXrmmWcUFxenihUrat68eZo3b57Dt+H9VZkyZXTHHXfo+PHj2rt3r23+ww8/rCeffFJt2rTRa6+9piFDhmjBggXq0qWLsrKycu3j4MGD6t+/v7p166bJkyfbfp8rZbRt27YaOXKkJOlf//qXLXe9evVs+zh06JD69u2rTp06aerUqSpXrpwGDx6sPXv2FPm7AQAKYAAA8P9mz55tSDK2bt1qvPnmm0ZQUJCRnp5uGIZh3HXXXUb79u0NwzCM6tWrG927d8+17ZX1rsjMzDQaNGhg3HrrrbnmSzI8PDyMPXv25JqfkJBgSDJeeeUVIysry+jfv7/h7+9vrFq1yrZOYmKi4enpabz44ou5tv35558NLy+vXPO7d+9uVK9evdDfPb/v9FfTp083JBlffPGFYRiG8d133xmSjAULFuRab+XKlXnmV69e3ZBkfP7557Z5ycnJRuXKlY0mTZrY5i1evNiQZKxfvz7ffJKMjRs32uadPn3a8PX1NR5//PFCf08AQOFwRQkAkK9+/frp4sWLWr58uS5cuKDly5cXeNudlHOL2hXnzp1TcnKybr75Zu3YsSPPurfccovq16+f734yMzN11113afny5frmm2/UuXNn27IlS5YoOztb/fr105kzZ2xTeHi4YmJitH79+mv4xldXpkwZSdKFCxckSYsXL1ZISIg6deqUK0uzZs1UpkyZPFkiIiJ0xx132D4HBwfr/vvv186dO5WUlFSoDPXr19fNN99s+xwaGqo6deroyJEj1/r1AAB2GMwBAJCv0NBQdezYUR9//LHS09NltVrVt2/fAtdfvny5XnjhBcXHxysjI8M232Kx5Fk3Ojq6wP1MnjxZqampWrFihdq1a5dr2cGDB2UYhmJiYvLd1tvb+2++VdGlpqZKkoKCgmxZkpOTValSpXzXP336dK7PtWrVyvNb1K5dW1LOO6TCw8P/NkO1atXyzCtXrlyeZ6IAANeOogQAKNA999yjhx56SElJSerWrZvKli2b73rfffedevXqpbZt2+qtt95S5cqV5e3trdmzZ+c7zPhfrz7Z69Kli1auXKn//Oc/ateunfz8/GzLsrOzZbFYtGLFCnl6eubZ9spVH1f45ZdfJOUUnitZKlWqpAULFuS7fmhoqNMz5PedJeUaYAIA4BwUJQBAge644w49/PDD2rJlixYuXFjgep9//rn8/Py0atWqXO9Ymj17tsPHbNWqlf7xj3+oR48euuuuu7R06VLbEOU1a9aUYRiKjo62XY0pSH5XsooqNTVVS5cuVWRkpG1whZo1a+q///2v2rRpc9Xid8WhQ4dkGEauXAcOHJAk22AWzswMALg2PKMEAChQmTJlNGvWLE2cOFE9e/YscD1PT09ZLBZZrVbbvMTERC1btqxIx+3YsaM+/fRTrVy5Uvfdd5+ys7MlSXfeeac8PT01adKkPFdRDMPQn3/+afscGBio5OTkIh3/ry5evKj77rtPZ8+e1TPPPGMrM/369ZPVatW///3vPNtcvnzZNgLfFSdOnNDSpUttn1NSUjR37lzFxcXZbrsLDAyUpDzbAgCKH1eUAABXNWjQoL9dp3v37po2bZq6du2qe+65R6dPn9bMmTNVq1Yt7d69u0jH7d27t2bPnq37779fwcHBeuedd1SzZk298MILGjdunBITE9W7d28FBQUpISFBS5cu1bBhw/TEE09Ikpo1a6aFCxdqzJgxuuGGG1SmTJmrlj1JOn78uObPny8p5yrS3r17tXjxYiUlJenxxx/Xww8/bFv3lltu0cMPP6zJkycrPj5enTt3lre3tw4ePKjFixfrtddey/VMV+3atfXAAw9o69atCgsL04cffqhTp07luuoWFxcnT09Pvfzyy0pOTpavr69uvfXWAp+DAgC4DkUJAHDNbr31Vn3wwQeaMmWKRo0apejoaL388stKTEwsclGSpHvvvVcXLlzQP//5TwUHB+uVV17R008/rdq1a2v69OmaNGmSJCkyMlKdO3dWr169bNv+85//VHx8vGbPnq3p06erevXqf1uU4uPjdd9998lisSgoKEiRkZHq2bOnHnzwQbVo0SLP+m+//baaNWumd955R//617/k5eWlqKgo3XvvvWrTpk2udWNiYvTGG2/oySef1P79+xUdHa2FCxeqS5cutnXCw8P19ttva/LkyXrggQdktVq1fv16ihIAmMBi8AQoAAAuFRUVpQYNGmj58uVmRwEAFBLPKAEAAACAHYoSAAAAANihKAEAAACAHZ5RAgAAAAA7XFECAAAAADsUJQAAAACw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oYcffljPPfecKXkAAAAAXB8KVZSqV6/u6hz5CgoK0owZMzRjxgxTjg8AAACUaIaRd3LVcazWgicfHyk01DXHdpEivXD28OHDmjFjhn799VdJUv369fXYY4+pZs2aTg0HAAAAoJAMQ/riC+nf/5Z27DA7TW6dO0urVpmdwiEOv3B21apVql+/vn766Sc1atRIjRo10o8//qjY2FitWbPGFRkBAAAAFMQwpK+/lpo3l+64w71KksUieXhI58+bncRhDl9RevrppzV69GhNmTIlz/yxY8eqU6dOTgsHAAAAoACGIa1eLT33nPTTTznzfH2lG2+UunaVgoJyisqVd6BeGU36L+9EdSoPD8nTM2fy8PjflJQkRUW55pgu5HBR+vXXX7Vo0aI884cOHcqzRAAAAEBhXbggrV0rJSc7vm1WljRnjvT99zmffXyk1q1zrihFRf2vFLkDb2+zExSJw0UpNDRU8fHxiomJyTU/Pj5elSpVclowAAAAoNRJS8u5TW7hQumbb6RLl65tf97eUsuWOQWpZk33KkglnMNF6aGHHtKwYcN05MgR3XjjjZKk77//Xi+//LLGjBnj9IAAAABAiXbxYk4pWrRIWr5cSk//37LQUKl8+aLtNyxM6tFDql2bguQChS5KVqtVnp6eGj9+vIKCgjR16lSNGzdOkhQREaGJEydq5MiRLgsKAAAAuJXMTGnlSmnevJxnhTIz818vKytniOwrKlSQGjXKuVWuUSMpIKB48sIhhS5KVapU0eDBg/XAAw9o9OjRGj16tC5cuCAp531HAAAAQKlnGNKWLdL8+Tm3z/35Z+G2K1dOatgwpxzFxUmBgS6NiWtX6KI0fPhwffTRR3rllVd044036oEHHlC/fv0UQAMGAABASXHkiLR+fdFevHr0qPTxx9Lhw/+bFxIiNW6cU4DCw/PfzsNDqlSJclTCFLoojR8/XuPHj9e3336r2bNna8SIEXrsscfUr18/Pfjgg2rZsqUrcwIAAADX5ptvpLvuyv2MUFH4+kqxsTmDKLRpIwUHOycf3IrDgzm0a9dO7dq108yZM/Xpp59qzpw5at26terVq6cHHniAAR0AAADgfj78UBo2LOdZoYiInFvhHOXtLdWvL91yS85ACgygUKpZDKMo1x1z+/rrr3X//ffr/Pnzsv71QTU3kJKSopCQECUnJyvY5LafZc3S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ssaMGaM777xTHh4emjhxolPDffnll2revLnuuusuVapUSU2aNNF777131W0yMjKUkpKSawIAAAAARzhclBYsWKD33ntPjz/+uLy8vDRgwAC9//77eu6557Rlyxanhjty5IhmzZqlmJgYrVq1So888ohGjhypjz76qMBtJk+erJCQENsUGRnp1EwAAAAASj+Hi1JSUpIaNmwoSSpTpozt5bM9evTQ119/7dRw2dnZatq0qV566SU1adJEw4YN00MPPaS33367wG3GjRun5ORk23Ts2DGnZgIAAABQ+jlclKpWraqTJ09KkmrWrKnVq1dLkrZu3SpfX1+nhqtcubLq16+fa169evV09OjRArfx9fVVcHBwrgkAAAAAHOFwUbrjjju0du1aSdKjjz6q8ePHKyYmRvfff7+GDh3q1HBt2rTR/v37c807cOCAqlev7tTjAAAAAMBfOTzq3ZQpU2x/7t+/v6pVq6bNmzcrJiZGPXv2dGq40aNH68Ybb9RLL72kfv366aefftK7776rd99916nHAQAAAIC/crgo2WvdurVat27tjCx53HDDDVq6dKnGjRun559/XtHR0ZoxY4YGDhzokuMBAAAAgFTIovTll18Weoe9evUqcpj89OjRQz169HDqPgEAAADgagpVlHr37l2onVksFlmt1mvJAwAAAACmK1RRys7OdnUOAAAAAHAbDo96BwAAAAClncODOTz//PNXXf7cc88VOQwAAAAAuAOHi9LSpUtzfc7KylJCQoK8vLxUs2ZNihIAAACAEs/horRz584881JSUjR48GDdcccdTgkFAAAAAGZyyjNKwcHBmjRpksaPH++M3QEAAACAqZw2mENycrKSk5OdtTsAAAAAMI3Dt969/vrruT4bhqGTJ09q3rx56tatm9OCAQAAAIBZHC5K06dPz/XZw8NDoaGhGjRokMaNG+e0YAAAAABgFoeLUkJCgityAAAAAIDb4IWzAAAAAGDH4StKly5d0htvvKH169fr9OnTys7OzrV8x44dTgsHAAAAAGZwuCg98MADWr16tfr27asWLVrIYrG4IhcAAAAAmMbhorR8+XJ98803atOmjSvyAAAAAIDpHH5GqUqVKgoKCnJFFgAAAABwCw4XpalTp2rs2LH67bffXJEHAAAAAEzn8K13zZs316VLl1SjRg0FBATI29s71/KzZ886LRwAAAAAmMHhojRgwAAdP35cL730ksLCwhjMAQAAAECp43BR+uGHH7R582Y1btzYFXkAAAAAwHQOP6NUt25dXbx40RVZAAAAAMAtOFyUpkyZoscff1zffvut/vzzT6WkpOSaAAAAAKCkc/jWu65du0qSOnTokGu+YRiyWCyyWq3OSQYAAAAAJnG4KK1fv94VOQAAAADAbThclG655RZX5AAAAAAAt+FwUdq4ceNVl7dt27bIYQAAAADAHThclNq1a5dn3l/fpcQzSgAAAABKOodHvTt37lyu6fTp01q5cqVuuOEGrV692hUZAQAAAKBYOXxFKSQkJM+8Tp06ycfHR2PGjNH27dudEgwAAAAAzOLwFaWChIWFaf/+/c7aHQAAAACYxuErSrt378712TAMnTx5UlOmTFFcXJyzcgEAAACAaRwuSnFxcbJYLDIMI9f8Vq1a6cMPP3RaMAAAAAAwi8NFKSEhIddnDw8PhYaGys/Pz2mhAAAAAMBMDhel6tWruyIHAAAAALiNQg/msG7dOtWvX18pKSl5liUnJys2NlbfffedU8MBAAAAgBkKXZRmzJihhx56SMHBwXmWhYSE6OGHH9a0adOcGg4AAAAAzFDoorRr1y517dq1wOWdO3fmHUoAAAAASoVCF6VTp07J29u7wOVeXl76448/nBIKAAAAAMxU6KJUpUoV/fLLLwUu3717typXruyUUAAAAABgpkIXpdtuu03jx4/XpUuX8iy7ePGiJkyYoB49ejg1HAAAAACYodDDgz/77LNasmSJateurREjRqhOnTqSpH379mnmzJmyWq165plnXBYUAAAAAIpLoYtSWFiYfvjhBz3yyCMaN26cDMOQJFksFnXp0kUzZ85UWFiYy4ICAAAAQHFx6IWz1atX1zfffKNz587p0KFDMgxDMTExKleunKvyAQAAAECxc6goXVGuXDndcMMNzs4CAAAAAG6h0IM5AAAAAMD1gqIEAAAAAHZKVFGaMmWKLBaLRo0aZXYUAAAAAKVYiSlKW7du1TvvvKNGjRqZHQUAAABAKVciilJqaqoGDhyo9957jxH2AAAAALhciShKw4cPV/fu3dWxY8e/XTcjI0MpKSm5JgAAAABwRJGGBy9On376qXbs2KGtW7cWav3Jkydr0qRJLk4FAAAAoDRz6ytKx44d02OPPaYFCxbIz8+vUNuMGzdOycnJtunYsWMuTgkAAACgtHHrK0rbt2/X6dOn1bRpU9s8q9WqjRs36s0331RGRoY8PT1zbePr6ytfX9/ijgoAAACgFHHrotShQwf9/PPPueYNGTJEdevW1dixY/OUJAAAAABwBrcuSkFBQWrQoEGueYGBgapQoUKe+QAAAADgLG79jBIAAAAAmMGtryjl59tvvzU7AgAAAIBSjitKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGDHrYvS5MmTdcMNNygoKEiVKlVS7969tX//frNjAQAAACjl3LoobdiwQcOHD9eWLVu0Zs0aZWVlqXPnzkpLSzM7GgAAAIBSzMvsAFezcuXKXJ/nzJmjSpUqafv27Wrbtq1JqQAAAACUdm5dlOwlJydLksqXL1/gOhkZGcrIyLB9TklJcXkuAAAAAKWLW99691fZ2dkaNWqU2rRpowYNGhS43uTJkxUSEmKbIiMjizElAAAAgNKgxBSl4cOH65dfftGnn3561fXGjRun5ORk23Ts2LFiSggAAACgtCgRt96NGDFCy5cv18aNG1W1atWrruvr6ytfX99iSgYAAACgNHLromQYhh599FEtXbpU3377raKjo82OBAAAAOA64NZFafjw4fr444/1xRdfKCgoSElJSZKkkJAQ+fv7m5wOAAAAQGnl1s8ozZo1S8nJyWrXrp0qV65smxYuXGh2NAAAAAClmFtfUTIMw+wIAAAAAK5Dbn1FCQAAAADMQFECAAAAADsUJQAAAACwQ1ECAAAAADsUJQAAAACw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f14hISF51u3Zs6d69uypF154QV26dNELL7ygTp06XfUYv/76qyQpOjraoWwAAPfBFSUAwDULDAxUrVq1VK1atTwlqTAaNWqk33//XQcOHMh3edOmTbV3717VqlUrz3S1K1X5qVOnTq7huyXJarVq165dBV7RknKuDtWtW7dQA0jMmDFDwcHB6tixo0PZAADugytKAADT3XLLLWrbtq369OmjadOmqVatWtq3b58sFou6du2qsWPHqlWrVhoxYoQefPBBBQYGau/evVqzZo3efPNNh441ZswYPfDAA6pbt646deqktLQ0vfHGGzp37pwefPBBSTm3Ak6YMEH33Xef6tevLx8fH23YsEEffvihxo4dm2t/58+fV1JSkjIyMnTgwAG98847WrZsmebOnauyZcs66ycCABQzihIAwC18/vnneuKJJzRgwAClpaWpVq1amjJliqScK04bNmzQM888o5tvvlmGYahmzZrq37+/w8cZMGCADMPQtGnT9PTTTysgIEDNmjXTxo0bFRYWJkmqWrWqoqKiNGnSJCUmJtpeDDtp0iSNHj061/6GDBkiSfLz81OVKlV000036aefflLTpk2v8RcBAJjJYlx54hQAgFJk4sSJWrZsmeLj482OUmiJiYmKjo7Wzp07FRcXZ3YcALiu8YwSAKDU+vnnn1WmTBm99dZbZkf5W926dVNsbKzZMQAA/48rSgCAUuns2bM6e/aspJwXyNqPaOdujh8/rosXL0rKGcnP0UEqAADORVECAAAAADvcegcAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGCHogQAAAAAdihKAAAAAGDn/wCouAWneTkyaQAAAABJRU5ErkJggg==", 327 | "text/plain": [ 328 | "
" 329 | ] 330 | }, 331 | "metadata": {}, 332 | "output_type": "display_data" 333 | } 334 | ], 335 | "source": [ 336 | "plot_order_book(flow_df.loc[flow_df.index.get_level_values('time')[0]])" 337 | ] 338 | }, 339 | { 340 | "cell_type": "code", 341 | "execution_count": null, 342 | "metadata": {}, 343 | "outputs": [], 344 | "source": [] 345 | } 346 | ], 347 | "metadata": { 348 | "kernelspec": { 349 | "display_name": "Python 3", 350 | "language": "python", 351 | "name": "python3" 352 | }, 353 | "language_info": { 354 | "codemirror_mode": { 355 | "name": "ipython", 356 | "version": 3 357 | }, 358 | "file_extension": ".py", 359 | "mimetype": "text/x-python", 360 | "name": "python", 361 | "nbconvert_exporter": "python", 362 | "pygments_lexer": "ipython3", 363 | "version": "3.8.6" 364 | } 365 | }, 366 | "nbformat": 4, 367 | "nbformat_minor": 2 368 | } 369 | -------------------------------------------------------------------------------- /notebooks/general_python_tutorials/dangers_of_hidden_parameters.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "Code for article https://medium.com/@lu.battistoni/the-dangers-of-pandas-hidden-parameters-1e6a013345e0" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 2, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "import pandas as pd" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 15, 22 | "metadata": {}, 23 | "outputs": [ 24 | { 25 | "data": { 26 | "text/html": [ 27 | "
\n", 28 | "\n", 41 | "\n", 42 | " \n", 43 | " \n", 44 | " \n", 45 | " \n", 46 | " \n", 47 | " \n", 48 | " \n", 49 | " \n", 50 | " \n", 51 | " \n", 52 | " \n", 53 | " \n", 54 | " \n", 55 | " \n", 56 | " \n", 57 | " \n", 58 | " \n", 59 | " \n", 60 | " \n", 61 | " \n", 62 | " \n", 63 | " \n", 64 | " \n", 65 | " \n", 66 | " \n", 67 | " \n", 68 | " \n", 69 | " \n", 70 | " \n", 71 | " \n", 72 | " \n", 73 | " \n", 74 | "
value
timestamp
2024-01-01 00:00:0010
2024-01-01 00:30:0020
2024-01-01 01:00:0030
2024-01-01 01:30:0040
2024-01-01 02:00:0050
\n", 75 | "
" 76 | ], 77 | "text/plain": [ 78 | " value\n", 79 | "timestamp \n", 80 | "2024-01-01 00:00:00 10\n", 81 | "2024-01-01 00:30:00 20\n", 82 | "2024-01-01 01:00:00 30\n", 83 | "2024-01-01 01:30:00 40\n", 84 | "2024-01-01 02:00:00 50" 85 | ] 86 | }, 87 | "execution_count": 15, 88 | "metadata": {}, 89 | "output_type": "execute_result" 90 | } 91 | ], 92 | "source": [ 93 | "# Sample time series data\n", 94 | "data = {\n", 95 | " 'timestamp': pd.date_range(start='2024-01-01', periods=5, freq='30min'),\n", 96 | " 'value': [10, 20, 30, 40, 50]\n", 97 | "}\n", 98 | "df = pd.DataFrame(data)\n", 99 | "df = df.set_index('timestamp')\n", 100 | "\n", 101 | "df" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": 16, 107 | "metadata": {}, 108 | "outputs": [ 109 | { 110 | "data": { 111 | "text/html": [ 112 | "
\n", 113 | "\n", 126 | "\n", 127 | " \n", 128 | " \n", 129 | " \n", 130 | " \n", 131 | " \n", 132 | " \n", 133 | " \n", 134 | " \n", 135 | " \n", 136 | " \n", 137 | " \n", 138 | " \n", 139 | " \n", 140 | " \n", 141 | " \n", 142 | " \n", 143 | " \n", 144 | " \n", 145 | " \n", 146 | " \n", 147 | " \n", 148 | " \n", 149 | " \n", 150 | " \n", 151 | "
value
timestamp
2024-01-01 00:00:0030
2024-01-01 01:00:0070
2024-01-01 02:00:0050
\n", 152 | "
" 153 | ], 154 | "text/plain": [ 155 | " value\n", 156 | "timestamp \n", 157 | "2024-01-01 00:00:00 30\n", 158 | "2024-01-01 01:00:00 70\n", 159 | "2024-01-01 02:00:00 50" 160 | ] 161 | }, 162 | "execution_count": 16, 163 | "metadata": {}, 164 | "output_type": "execute_result" 165 | } 166 | ], 167 | "source": [ 168 | "# Resample to 1 hour intervals, without setting the label parameter\n", 169 | "resampled_df = df.resample('1h').sum()\n", 170 | "resampled_df" 171 | ] 172 | }, 173 | { 174 | "cell_type": "code", 175 | "execution_count": 17, 176 | "metadata": {}, 177 | "outputs": [ 178 | { 179 | "data": { 180 | "text/html": [ 181 | "
\n", 182 | "\n", 195 | "\n", 196 | " \n", 197 | " \n", 198 | " \n", 199 | " \n", 200 | " \n", 201 | " \n", 202 | " \n", 203 | " \n", 204 | " \n", 205 | " \n", 206 | " \n", 207 | " \n", 208 | " \n", 209 | " \n", 210 | " \n", 211 | " \n", 212 | " \n", 213 | " \n", 214 | " \n", 215 | " \n", 216 | " \n", 217 | " \n", 218 | " \n", 219 | " \n", 220 | "
value
timestamp
2024-01-01 01:00:0030
2024-01-01 02:00:0070
2024-01-01 03:00:0050
\n", 221 | "
" 222 | ], 223 | "text/plain": [ 224 | " value\n", 225 | "timestamp \n", 226 | "2024-01-01 01:00:00 30\n", 227 | "2024-01-01 02:00:00 70\n", 228 | "2024-01-01 03:00:00 50" 229 | ] 230 | }, 231 | "execution_count": 17, 232 | "metadata": {}, 233 | "output_type": "execute_result" 234 | } 235 | ], 236 | "source": [ 237 | "# Resample to 1 hour intervals, with label='right'\n", 238 | "resampled_df = df.resample('1h', label='right').sum()\n", 239 | "resampled_df" 240 | ] 241 | }, 242 | { 243 | "cell_type": "code", 244 | "execution_count": 18, 245 | "metadata": {}, 246 | "outputs": [ 247 | { 248 | "data": { 249 | "text/html": [ 250 | "
\n", 251 | "\n", 264 | "\n", 265 | " \n", 266 | " \n", 267 | " \n", 268 | " \n", 269 | " \n", 270 | " \n", 271 | " \n", 272 | " \n", 273 | " \n", 274 | " \n", 275 | " \n", 276 | " \n", 277 | " \n", 278 | " \n", 279 | " \n", 280 | " \n", 281 | " \n", 282 | " \n", 283 | " \n", 284 | " \n", 285 | " \n", 286 | " \n", 287 | " \n", 288 | " \n", 289 | "
value
timestamp
2024-01-01 00:00:0010
2024-01-01 01:00:0050
2024-01-01 02:00:0090
\n", 290 | "
" 291 | ], 292 | "text/plain": [ 293 | " value\n", 294 | "timestamp \n", 295 | "2024-01-01 00:00:00 10\n", 296 | "2024-01-01 01:00:00 50\n", 297 | "2024-01-01 02:00:00 90" 298 | ] 299 | }, 300 | "execution_count": 18, 301 | "metadata": {}, 302 | "output_type": "execute_result" 303 | } 304 | ], 305 | "source": [ 306 | "# Resample to 1 hour intervals, with closed='right'\n", 307 | "resampled_df = df.resample('1h', label='right', closed='right').sum()\n", 308 | "resampled_df" 309 | ] 310 | }, 311 | { 312 | "cell_type": "code", 313 | "execution_count": null, 314 | "metadata": {}, 315 | "outputs": [], 316 | "source": [] 317 | } 318 | ], 319 | "metadata": { 320 | "kernelspec": { 321 | "display_name": "Python 3", 322 | "language": "python", 323 | "name": "python3" 324 | }, 325 | "language_info": { 326 | "codemirror_mode": { 327 | "name": "ipython", 328 | "version": 3 329 | }, 330 | "file_extension": ".py", 331 | "mimetype": "text/x-python", 332 | "name": "python", 333 | "nbconvert_exporter": "python", 334 | "pygments_lexer": "ipython3", 335 | "version": "3.8.6" 336 | } 337 | }, 338 | "nbformat": 4, 339 | "nbformat_minor": 2 340 | } 341 | -------------------------------------------------------------------------------- /notebooks/simple_vectorial_backtest/backtest.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file implements a simple vectorial backtest class. 3 | You can import the class in any python file and perform a simple backtest using the class method do_backtest. 4 | This method uses the prices of instruments of the investable universe and each instrument signal to perform a backtest. 5 | 6 | Signals are meant to be high numbers for assets that I want to buy and low numbers for assets that I want to sell. 7 | Find an example in the notebook 'simple_vectorial_backtest.ipynb'. 8 | 9 | The backtest is equally weighted, meaning that, for each leg, every instrument weights the same. 10 | 11 | The arguments of the constructor are: 12 | - signals: pd.DataFrame indexed by datetime and asset and with a column called 'signal' with strategy signals 13 | - prices: pd.DataFrame indexed by datetime and asset. The columns are the open and close prices of the candles. 14 | - initial_cash: a float that represents the initial value of the equity line 15 | - commissions: a float representing how many basis points a transaction costs 16 | - number_of_instruments_long_leg: int representing how many instruments to go long 17 | - number_of_instruments_short_leg: int representing how many instruments to go short. If this number is 0 then the strategy is long only. 18 | 19 | The weights of the long only strategy sum to 1, the weights of the long short strategy sum to 0 and their absolute value sums to 1. 20 | This is done in order to keep the leverage at 1. 21 | 22 | As a final note, signals are expected to be opened at candle open of the next day and closed at candle close. 23 | You can modify the code to allow for customised execution. 24 | 25 | """ 26 | 27 | import pandas as pd 28 | import numpy as np 29 | 30 | class VectorialBacktest(): 31 | 32 | # constructor 33 | def __init__( 34 | self, 35 | signals, 36 | prices, 37 | initial_cash, 38 | commissions, 39 | number_of_instruments_long_leg, 40 | number_of_instruments_short_leg) -> None: 41 | 42 | self.signals = signals 43 | self.prices = prices 44 | self.initial_cash = initial_cash 45 | self.commissions = commissions 46 | self.number_of_instruments_long_leg = number_of_instruments_long_leg 47 | self.number_of_instruments_short_leg = number_of_instruments_short_leg 48 | 49 | # define if the strategy is long short or long only 50 | def is_longshort(self): 51 | if self.number_of_instruments_short_leg > 0: 52 | return True # the strategy is long short, i.e. we go long some instruments and short some others 53 | else: 54 | return False # the strategy is long only, i.e. I can only have positive weights allocated 55 | 56 | # get the instruments belonging to the long leg and compute their weights. 57 | # the strategy assigns equal weights to each instrument 58 | def get_long_leg_instruments_weights(self): 59 | long_leg = pd.DataFrame(self.signals['signal'].groupby(level='datetime').nlargest(self.number_of_instruments_long_leg).droplevel(0)) 60 | 61 | if self.is_longshort(): 62 | multiplier = 2 # done to keep the leverage to 1 in long short 63 | else: 64 | multiplier = 1 65 | 66 | # assign equal weights 67 | long_leg['signal'] = 1 / (multiplier * self.number_of_instruments_long_leg) 68 | return long_leg 69 | 70 | # get the instruments belonging to the short leg and compute their weights. 71 | # the strategy assigns equal weights to each instrument 72 | def get_short_leg_instruments_weights(self): 73 | short_leg = pd.DataFrame(self.signals['signal'].groupby(level='datetime').nsmallest(self.number_of_instruments_short_leg).droplevel(0)) 74 | 75 | if self.is_longshort(): 76 | # assign equal weights 77 | short_leg['signal'] = - 1 / (2 * self.number_of_instruments_short_leg) 78 | else: 79 | short_leg['signal'] = 0 80 | 81 | return short_leg 82 | 83 | # compute the metrics maximum drawdown, that is the portfolio maximum loss from a peak before a new peak happens. 84 | @staticmethod 85 | def _compute_max_drawdown(equity_line): 86 | peak = -np.inf # Initialize peak value 87 | drawdown = 0 # Initialize drawdown value 88 | max_drawdown = 0 # Initialize maximum drawdown value 89 | 90 | for i in range(len(equity_line)): 91 | if equity_line[i] > peak: 92 | peak = equity_line[i] 93 | else: 94 | drawdown = (peak - equity_line[i]) / peak 95 | if drawdown > max_drawdown: 96 | max_drawdown = drawdown 97 | 98 | return max_drawdown 99 | 100 | # compute backtest metrics 101 | @staticmethod 102 | def _compute_metrics(trades_df, equity_line_df): 103 | cumulative_return = equity_line_df.iloc[-1]['portfolio_value'] / equity_line_df.iloc[0]['portfolio_value'] - 1 104 | annualised_return = equity_line_df['portfolio_value'].pct_change().mean() * 252 105 | annualised_std = equity_line_df['portfolio_value'].pct_change().std() * np.sqrt(252) 106 | sharpe = annualised_return / annualised_std 107 | max_drawdown = VectorialBacktest._compute_max_drawdown(equity_line_df['portfolio_value'].values) 108 | calmar = annualised_return / abs(max_drawdown) 109 | 110 | return { 111 | 'cumulative_return': cumulative_return, 112 | 'annualised_return': annualised_return, 113 | 'annualised_std': annualised_std, 114 | 'sharpe': sharpe, 115 | 'max_drawdown': max_drawdown, 116 | 'calmar': calmar, 117 | } 118 | 119 | # compute forward returns for each asset. Forward returns are computed open to close 120 | # of t+1 with respect to signal t 121 | def get_forward_returns(self): 122 | # first compute open to close returns 123 | open_to_close_returns = (self.prices['close'] / self.prices['open']) - 1 124 | 125 | # then make them forward 126 | return open_to_close_returns.unstack().shift(-1) 127 | 128 | 129 | def do_backtest(self): 130 | # sort signals dataframe to get the top n and bottom n signals 131 | top_n = self.get_long_leg_instruments_weights() 132 | bottom_n = self.get_short_leg_instruments_weights() 133 | 134 | # create portfolio weights dataframe 135 | portfolio_weights_df = pd.concat([top_n, bottom_n]) 136 | portfolio_weights_df = portfolio_weights_df['signal'].unstack() 137 | portfolio_weights_df = portfolio_weights_df.fillna(0) # Fill NaNs with 0s 138 | 139 | # compute forward returns for each asset 140 | # this is done in order to compute the daily pnl 141 | fwd_returns_df = self.get_forward_returns() 142 | 143 | # commissions 144 | transaction_costs = self.commissions / 10000 # Convert basis points to decimal 145 | 146 | 147 | # now do the backtest 148 | # first instantiate the dataframe containing the necessary info 149 | portfolio_value = pd.DataFrame() 150 | 151 | # compute the weights change with relation to the last period 152 | weights_change = portfolio_weights_df.diff().fillna(0) 153 | 154 | # set the first change as the actual weights. 155 | weights_change.iloc[0] = portfolio_weights_df.iloc[0] 156 | 157 | # now compute the absoulte sum of weights difference, useful to compute commissions 158 | sum_of_absolute_weights_difference = abs(weights_change).sum(axis=1) 159 | 160 | # the gross return of the period is 1 + the sum of weight multiplied by fwd returns 161 | # notice that these returns happen at the end of the next bar, so they happen in the future 162 | # we should shift our final equity line by 1 bar in the future 163 | # we will do this at the end of the computation 164 | portfolio_value['gross_return'] = 1 + (portfolio_weights_df * fwd_returns_df).sum(axis=1) 165 | 166 | # compute the daily costs in percentage 167 | portfolio_value['daily_percentage_costs'] = sum_of_absolute_weights_difference * transaction_costs 168 | 169 | # and the gross daily costs 170 | portfolio_value['gross_daily_percentage_costs'] = 1 - portfolio_value['daily_percentage_costs'] 171 | 172 | # the total return in percentage is the gross return multiplied by gross costs 173 | portfolio_value['total_return'] = portfolio_value['gross_return'] * portfolio_value['gross_daily_percentage_costs'] 174 | 175 | # then we can obtain our equity line by computing the total return for each datapoint 176 | # and multiplying by initial cash. Moreover we shift the curve, as discussed previously 177 | portfolio_value = portfolio_value.shift() 178 | portfolio_value['portfolio_value'] = portfolio_value['total_return'].cumprod() * self.initial_cash 179 | portfolio_value.loc[portfolio_value.index[0], 'portfolio_value'] = self.initial_cash 180 | 181 | # compute metrics 182 | backtest_metrics = VectorialBacktest._compute_metrics(portfolio_weights_df, 183 | portfolio_value[['portfolio_value']] 184 | ) 185 | 186 | return portfolio_weights_df, portfolio_value[['portfolio_value']], backtest_metrics 187 | -------------------------------------------------------------------------------- /notebooks/statistics/wu_for_better_hypothesis_testing.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "name": "stdout", 10 | "output_type": "stream", 11 | "text": [ 12 | "t test p-value: 0.0028\n", 13 | "Cohen d: 0.042\n", 14 | "\n", 15 | "There's a statistical difference between the two groups means.\n", 16 | "The Cohen d is extremely small, it probably isn't relevant from a practical point of view.\n" 17 | ] 18 | }, 19 | { 20 | "data": { 21 | "image/png": "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6bZ3dnZW27Zt9fXXX+vAgQNplv/55593tN0OHTpo586d+vLLL/XXX3/ZDamTpPbt2yslJUWjR49O897r16/bHjndrFkzeXp6asyYMUpMTLRr9+/jmDdv3nSHcLVs2VIxMTF290Bdv35dkyZNkoeHhxo2bHjbfTl//rzda7PZrAoVKsgwDF27du22778XN2/TycnJFlZSH2+dN29eSUrzeO6mTZvKbDZr4sSJdsfoiy++UHx8vMLDwyXdCO/58+fXZ599ZruvR5Lmzp17yyF/N3N2dk7z+V+0aJFtWGVmuXDhQpp51apVk6Q0j/wGgIcJV44A5Cr9+vXT33//rSeffFLlypVTcnKytm/frgULFqhYsWK2e3WaNWtmuxrw/PPP6/Lly/rss8/k5+dnu7qUmVxdXbVq1Sp169ZNISEhWrlypZYvX6433njjlsPWxo4dq40bNyokJETPPfecKlSooAsXLujnn3/WunXr0v1H6s3at2+v1157Ta+99pp8fX3TnNVv2LChnn/+eY0ZM0aRkZFq1qyZXFxcdOTIES1atEiffPKJ2rVrJy8vL40fP169e/dWrVq11LlzZ+XLl0979+7V33//bRtiVqNGDS1YsEADBw5UrVq15OHhoVatWqlPnz6aMWOGunfvrj179qhYsWJavHixtm3bpgkTJsjT0/O2+9KsWTMFBASobt268vf316+//qrJkycrPDz8jt5/L3r37q0LFy6ocePGKlKkiH7//XdNmjRJ1apVs90vVa1aNTk7O+v9999XfHy8LBaL7Tu0hg4dqpEjR6p58+Zq3bq1oqKiNHXqVNWqVUvPPPOMpBshb8SIEerXr58aN26s9u3b68SJE5o1a5ZKlix5Rw+8kKTHH39co0aNUo8ePfToo49q//79mjt3bpr73e7XqFGjtGXLFoWHhys4OFjnzp3T1KlTVaRIEdWrVy9TtwUAmcohz8gDAAdZuXKl0bNnT6NcuXKGh4eHYTabjVKlShn9+vUzYmNj7dp+//33RpUqVQxXV1ejWLFixvvvv298+eWXhiQjOjra1i44ONgIDw9Psy1JRkREhN281MdQf/DBB7Z53bp1M/LmzWscO3bMaNasmeHu7m74+/sbw4cPT/PYZd30KG/DMIzY2FgjIiLCCAoKMlxcXIyAgACjSZMmxqeffnrHx6Vu3bqGJKN3794Ztvn000+NGjVqGG5uboanp6dRuXJlY9CgQcaZM2fs2n3//ffGo48+ari5uRleXl5G7dq1jf/973+25ZcvXzY6d+5s+Pj4GJLsHgEdGxtr9OjRwyhQoIBhNpuNypUrGzNnzrRbf3rHMNWMGTOMBg0aGPnz5zcsFotRsmRJ4/XXXzfi4+Nvuf+pj65etGhRutv6dx9ufpT34sWLjWbNmhl+fn6G2Ww2ihYtajz//PPG2bNn7db12WefGSVKlDCcnZ3TPNZ78uTJRrly5QwXFxfD39/fePHFF42LFy+m6efEiRON4OBgw2KxGLVr1za2bdtm1KhRw2jevPlt98UwbjzK+9VXXzUKFSpkuLm5GXXr1jV27NhhNGzYMN3Hgd+8jpkzZxqSjN27d9vNHz58uN2ju9evX2888cQTRmBgoGE2m43AwECjU6dOxm+//ZamTwDwMDEZRibcFQwAuGfdu3fX4sWLdfnyZUd3BdmM1WpVwYIF9dRTT+mzzz5zdHcAINvjniMAALKBxMTENPcLffXVV7pw4YIaNWrkmE4BQA7DPUcAAGQDO3fu1IABA/T0008rf/78+vnnn/XFF1+oUqVKevrppx3dPQDIEQhHAABkA8WKFVNQUJAmTpyoCxcuyNfXV127dtXYsWNlNpsd3T0AyBG45wgAAAAAxD1HAAAAACCJcAQAAAAAkghHAAAAACCJcAQAAAAAkghHAAAAACCJcAQAAAAAkghHAAAAACCJcAQAAAAAkghHAAAAACCJcATkSidOnJDJZNKsWbMc3RUAACRRm/BwIBzhjkVHR6tv374qU6aM3N3d5e7urgoVKigiIkL79u1zdPfuWVRUlAYMGKBHH31Urq6uMplMOnHihKO7hTswdepUiiiQy+XU2rRkyRJ16NBBJUqUkLu7u8qWLatXX31VcXFxju4aboPalL3lcXQHkD0sW7ZMHTp0UJ48edSlSxdVrVpVTk5OOnz4sJYsWaJp06YpOjpawcHBju7qXduxY4cmTpyoChUqqHz58oqMjHR0l3CHpk6dqgIFCqh79+6O7goAB8jJtalPnz4KDAzUM888o6JFi2r//v2aPHmyVqxYoZ9//llubm6O7iIyQG3K3ghHuK1jx46pY8eOCg4O1vr161WoUCG75e+//76mTp0qJ6dbX4i8cuWK8ubNm5VdvSetW7dWXFycPD099eGHHxKOcrnExESZzebbfp4BOFZOr02LFy9Wo0aN7ObVqFFD3bp109y5c9W7d2/HdAwOQW16cDjCuK1x48bpypUrmjlzZpriI0l58uTRyy+/rKCgINu87t27y8PDQ8eOHVPLli3l6empLl26SLpRiF599VUFBQXJYrGobNmy+vDDD2UYhu39txp3bDKZNGLECNvrESNGyGQy6fDhw2rfvr28vLyUP39+vfLKK0pMTLzt/vn6+srT0/Mujoi9n376SWFhYSpQoIDc3NxUvHhx9ezZ067Nhx9+qEcffVT58+eXm5ubatSoocWLF6e7b3379tWiRYtUoUIFubm5KTQ0VPv375ckzZgxQ6VKlZKrq6saNWqUZvhfo0aNVKlSJe3Zs0ePPvqorT/Tp0+/o305fPiw2rVrJ19fX7m6uqpmzZr6/vvvb/mea9euydfXVz169EizLCEhQa6urnrttdds85KSkjR8+HCVKlVKFotFQUFBGjRokJKSktK8f86cOapdu7bc3d2VL18+NWjQQGvWrJEkFStWTAcPHtTmzZtlMplkMpns/iFx/PhxPf300/L19ZW7u7vq1Kmj5cuX261/06ZNMplMmj9/vt566y0VLlxY7u7uSkhIuKPjBcBxcnptujkYSdKTTz4pSfr1119v+35qE7UJ94YrR7itZcuWqVSpUgoJCbmr912/fl1hYWGqV6+ePvzwQ7m7u8swDLVu3VobN25Ur169VK1aNa1evVqvv/66/vjjD40fP/6e+9m+fXsVK1ZMY8aM0c6dOzVx4kRdvHhRX3311T2v83bOnTunZs2aqWDBghoyZIh8fHx04sQJLVmyxK7dJ598otatW6tLly5KTk7W/Pnz9fTTT2vZsmUKDw+3a/vDDz/o+++/V0REhCRpzJgxevzxxzVo0CBNnTpVL730ki5evKhx48apZ8+e2rBhg937L168qJYtW6p9+/bq1KmTFi5cqBdffFFmszlNYfy3gwcPqm7duipcuLCGDBmivHnzauHChWrTpo2+/vprW1G+mYuLi5588kktWbJEM2bMkNlsti379ttvlZSUpI4dO0qSrFarWrdura1bt6pPnz4qX7689u/fr/Hjx+u3337Tt99+a3vvyJEjNWLECD366KMaNWqUzGazdu3apQ0bNqhZs2aaMGGC+vXrJw8PD7355puSJH9/f0lSbGysHn30Uf399996+eWXlT9/fs2ePVutW7fW4sWL0+zL6NGjZTab9dprrykpKcluHwA8nHJjbYqJiZEkFShQ4JbtqE3UJtwHA7iF+Ph4Q5LRpk2bNMsuXrxo/Pnnn7bp77//ti3r1q2bIckYMmSI3Xu+/fZbQ5Lxzjvv2M1v166dYTKZjKNHjxqGYRjR0dGGJGPmzJlptivJGD58uO318OHDDUlG69at7dq99NJLhiRj7969d7y/H3zwgSHJiI6OvqP233zzjSHJ2L179y3b/fvYGIZhJCcnG5UqVTIaN25sN1+SYbFY7LY/Y8YMQ5IREBBgJCQk2OYPHTo0TV8bNmxoSDI++ugj27ykpCSjWrVqhp+fn5GcnGwYRvrHt0mTJkblypWNxMRE2zyr1Wo8+uijRunSpW+5f6tXrzYkGUuXLrWb37JlS6NEiRK21//9738NJycn44cffrBrN336dEOSsW3bNsMwDOPIkSOGk5OT8eSTTxopKSl2ba1Wq+3nihUrGg0bNkzTn/79+xuS7LZz6dIlo3jx4kaxYsVs69y4caMhyShRokSa3xGAh1duq02pevXqZTg7Oxu//fbbLdtRm26gNuFeMKwOt5R6CdfDwyPNskaNGqlgwYK2acqUKWnavPjii3avV6xYIWdnZ7388st281999VUZhqGVK1fec19Tz2al6tevn22bWcXHx0fSjTOY165dy7Ddv2+cvXjxouLj41W/fn39/PPPado2adJExYoVs71OPSvatm1bu+F/qfOPHz9u9/48efLo+eeft702m816/vnnde7cOe3Zsyfd/l24cEEbNmxQ+/btdenSJf3111/666+/dP78eYWFhenIkSP6448/Mty/xo0bq0CBAlqwYIHdfq5du1YdOnSwzVu0aJHKly+vcuXK2bbx119/qXHjxpKkjRs3SrpxVs9qtWrYsGFpxlebTKYM+5FqxYoVql27turVq2eb5+HhoT59+ujEiRM6dOiQXftu3bpxczOQjeTG2jRv3jx98cUXevXVV1W6dOlbtqU23UBtwr0gHOGWUv/gXb58Oc2yGTNmaO3atZozZ066782TJ4+KFCliN+/3339XYGBgmnt8ypcvb1t+r24uFiVLlpSTk1OWPpa7YcOGatu2rUaOHKkCBQroiSee0MyZM9OMUV62bJnq1KkjV1dX+fr6qmDBgpo2bZri4+PTrLNo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", 22 | "text/plain": [ 23 | "
" 24 | ] 25 | }, 26 | "metadata": {}, 27 | "output_type": "display_data" 28 | } 29 | ], 30 | "source": [ 31 | "import numpy as np\n", 32 | "from scipy import stats\n", 33 | "from matplotlib import pyplot as plt\n", 34 | "\n", 35 | "# generate two samples for two groups with same std and different mean\n", 36 | "np.random.seed(7)\n", 37 | "sample_size = 10000\n", 38 | "mean = 50\n", 39 | "std = 5\n", 40 | "\n", 41 | "# group 1: mean 50 and std 5\n", 42 | "group1 = np.random.normal(loc=mean, scale=std, size=sample_size)\n", 43 | "\n", 44 | "# group 2: mean 50.2 and std 5\n", 45 | "group2 = np.random.normal(loc=mean + 0.2, scale=std, size=sample_size)\n", 46 | "\n", 47 | "# plot the distributions\n", 48 | "fig, axs = plt.subplots(1,2, figsize=(10,5))\n", 49 | "\n", 50 | "props = dict(boxstyle='round', facecolor='wheat', alpha=0.2)\n", 51 | "x_props = 0.03\n", 52 | "y_props = 0.9\n", 53 | "\n", 54 | "axs[0].hist(group1)\n", 55 | "axs[0].set_title('Group 1 sample vector')\n", 56 | "axs[0].text(x_props, y_props, f'mean = {round(np.mean(group1),2)}\\n\\nstd dev = {round(np.std(group1),2)}', transform=axs[0].transAxes, fontsize=9,\n", 57 | " verticalalignment='top', bbox=props)\n", 58 | "\n", 59 | "axs[1].hist(group2)\n", 60 | "axs[1].set_title('Group 2 sample vector')\n", 61 | "axs[1].text(x_props, y_props, f'mean = {round(np.mean(group2),2)}\\n\\nstd dev = {round(np.std(group2),2)}', transform=axs[1].transAxes, fontsize=9,\n", 62 | " verticalalignment='top', bbox=props)\n", 63 | "\n", 64 | "fig.suptitle('Sample vectors histograms')\n", 65 | "\n", 66 | "\n", 67 | "# Student t test\n", 68 | "t_stat, p_value = stats.ttest_ind(group1, group2)\n", 69 | "\n", 70 | "# Cohen d\n", 71 | "mean_diff = np.mean(group1) - np.mean(group2)\n", 72 | "pooled_std = np.sqrt((np.std(group1, ddof=1) ** 2 + np.std(group2, ddof=1) ** 2) / 2)\n", 73 | "cohen_d = mean_diff / pooled_std\n", 74 | "\n", 75 | "# results\n", 76 | "print(f\"t test p-value: {p_value:.2}\")\n", 77 | "print(f\"Cohen d: {abs(cohen_d):.2}\")\n", 78 | "print()\n", 79 | "\n", 80 | "# interpretation\n", 81 | "if p_value < 0.05:\n", 82 | " print(\"There's a statistical difference between the two groups means.\")\n", 83 | "else:\n", 84 | " print(\"There isn't a statistical difference between the two groups means.\")\n", 85 | "\n", 86 | "if abs(cohen_d) < 0.2:\n", 87 | " print(\"The Cohen d is extremely small, it probably isn't relevant from a practical point of view.\")\n", 88 | "elif abs(cohen_d) < 0.5:\n", 89 | " print(\"The Cohen d is small.\")\n", 90 | "elif abs(cohen_d) < 0.8:\n", 91 | " print(\"The Cohen d is moderate.\")\n", 92 | "else:\n", 93 | " print(\"The Cohen d is large.\")" 94 | ] 95 | } 96 | ], 97 | "metadata": { 98 | "kernelspec": { 99 | "display_name": "Python 3", 100 | "language": "python", 101 | "name": "python3" 102 | }, 103 | "language_info": { 104 | "codemirror_mode": { 105 | "name": "ipython", 106 | "version": 3 107 | }, 108 | "file_extension": ".py", 109 | "mimetype": "text/x-python", 110 | "name": "python", 111 | "nbconvert_exporter": "python", 112 | "pygments_lexer": "ipython3", 113 | "version": "3.8.6" 114 | } 115 | }, 116 | "nbformat": 4, 117 | "nbformat_minor": 2 118 | } 119 | --------------------------------------------------------------------------------