├── .gitignore ├── LICENSE ├── README.md ├── main.py ├── models ├── __init__.py ├── base_model.py └── hft_model_1.py ├── requirements.txt ├── sample_output ├── run_01_output_1.txt ├── run_01_output_2.txt ├── run_01_screenshot_1.png ├── run_01_screenshot_2.png ├── run_02_output.txt └── run_02_screenshot.png └── util ├── __init__.py ├── dt_util.py └── order_util.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | 3 | # IntelliJ project files 4 | .idea 5 | *.iml 6 | out 7 | gen 8 | 9 | ### Visual Studio Code template 10 | .vscode/* 11 | !.vscode/settings.json 12 | !.vscode/tasks.json 13 | !.vscode/launch.json 14 | !.vscode/extensions.json 15 | 16 | ### Python template 17 | 18 | # Byte-compiled / optimized / DLL files 19 | __pycache__/ 20 | *.py[cod] 21 | *$py.class 22 | 23 | # C extensions 24 | *.so 25 | 26 | # Distribution / packaging 27 | .Python 28 | build/ 29 | develop-eggs/ 30 | dist/ 31 | downloads/ 32 | eggs/ 33 | .eggs/ 34 | lib/ 35 | lib64/ 36 | parts/ 37 | sdist/ 38 | var/ 39 | wheels/ 40 | pip-wheel-metadata/ 41 | share/python-wheels/ 42 | *.egg-info/ 43 | .installed.cfg 44 | *.egg 45 | MANIFEST 46 | 47 | # PyInstaller 48 | # Usually these files are written by a python script from a template 49 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 50 | *.manifest 51 | *.spec 52 | 53 | # Installer logs 54 | pip-log.txt 55 | pip-delete-this-directory.txt 56 | 57 | # Unit test / coverage reports 58 | htmlcov/ 59 | .tox/ 60 | .nox/ 61 | .coverage 62 | .coverage.* 63 | .cache 64 | nosetests.xml 65 | coverage.xml 66 | *.cover 67 | .hypothesis/ 68 | .pytest_cache/ 69 | 70 | # Translations 71 | *.mo 72 | *.pot 73 | 74 | # Django stuff: 75 | *.log 76 | local_settings.py 77 | db.sqlite3 78 | 79 | # Flask stuff: 80 | instance/ 81 | .webassets-cache 82 | 83 | # Scrapy stuff: 84 | .scrapy 85 | 86 | # Sphinx documentation 87 | docs/_build/ 88 | 89 | # PyBuilder 90 | target/ 91 | 92 | # Jupyter Notebook 93 | .ipynb_checkpoints 94 | 95 | # IPython 96 | profile_default/ 97 | ipython_config.py 98 | 99 | # pyenv 100 | .python-version 101 | 102 | # pipenv 103 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 104 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 105 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 106 | # install all needed dependencies. 107 | #Pipfile.lock 108 | 109 | # celery beat schedule file 110 | celerybeat-schedule 111 | 112 | # SageMath parsed files 113 | *.sage.py 114 | 115 | # Environments 116 | .env 117 | .venv 118 | env/ 119 | venv/ 120 | ENV/ 121 | env.bak/ 122 | venv.bak/ 123 | 124 | # Spyder project settings 125 | .spyderproject 126 | .spyproject 127 | 128 | # Rope project settings 129 | .ropeproject 130 | 131 | # mkdocs documentation 132 | /site 133 | 134 | # mypy 135 | .mypy_cache/ 136 | .dmypy.json 137 | dmypy.json 138 | 139 | # Pyre type checker 140 | .pyre/ 141 | 142 | ### JetBrains template 143 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 144 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 145 | 146 | # User-specific stuff 147 | .idea/**/workspace.xml 148 | .idea/**/tasks.xml 149 | .idea/**/usage.statistics.xml 150 | .idea/**/dictionaries 151 | .idea/**/shelf 152 | 153 | # Generated files 154 | .idea/**/contentModel.xml 155 | 156 | # Sensitive or high-churn files 157 | .idea/**/dataSources/ 158 | .idea/**/dataSources.ids 159 | .idea/**/dataSources.local.xml 160 | .idea/**/sqlDataSources.xml 161 | .idea/**/dynamic.xml 162 | .idea/**/uiDesigner.xml 163 | .idea/**/dbnavigator.xml 164 | 165 | # Gradle 166 | .idea/**/gradle.xml 167 | .idea/**/libraries 168 | 169 | # Gradle and Maven with auto-import 170 | # When using Gradle or Maven with auto-import, you should exclude module files, 171 | # since they will be recreated, and may cause churn. Uncomment if using 172 | # auto-import. 173 | # .idea/modules.xml 174 | # .idea/*.iml 175 | # .idea/modules 176 | # *.iml 177 | # *.ipr 178 | 179 | # CMake 180 | cmake-build-*/ 181 | 182 | # Mongo Explorer plugin 183 | .idea/**/mongoSettings.xml 184 | 185 | # File-based project format 186 | *.iws 187 | 188 | # IntelliJ 189 | out/ 190 | 191 | # mpeltonen/sbt-idea plugin 192 | .idea_modules/ 193 | 194 | # JIRA plugin 195 | atlassian-ide-plugin.xml 196 | 197 | # Cursive Clojure plugin 198 | .idea/replstate.xml 199 | 200 | # Crashlytics plugin (for Android Studio and IntelliJ) 201 | com_crashlytics_export_strings.xml 202 | crashlytics.properties 203 | crashlytics-build.properties 204 | fabric.properties 205 | 206 | # Editor-based Rest Client 207 | .idea/httpRequests 208 | 209 | # Android studio 3.1+ serialized cache file 210 | .idea/caches/build_file_checksums.ser 211 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 James Ma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Purpose 2 | === 3 | A simple trading equity trading model on Interactive Brokers' API dealing with (pseudo) high-frequency data studies. 4 | 5 | Requirements 6 | === 7 | 8 | - Python 3.7 9 | - IB Trader Workstation Build 973.2 10 | - IB paper or live trading account 11 | 12 | What's new 13 | === 14 | 15 | *14 Jun 2019* 16 | 17 | - Version 2.0 released 18 | ### - Merged pull request from: https://github.com/chicago-joe/IB_PairsTrading_Algo 19 | 20 | ## Special Thanks to [chicago-joe](https://github.com/chicago-joe) for updating to work with Python 3. 21 | 22 | 23 | *19 Jun 2019* 24 | 25 | - Version 3.0 released 26 | - `ibpy` library is dropped in favour of the newer `ib_insync` library. 27 | - The same code logic is ported over to use the features of `ib_insync`, compatible with Python 3.7. Includes various code cleanup. 28 | - Dropped `matplotlib` charting in favour of headless running inside Docker. 29 | 30 | 31 | Setting up 32 | === 33 | 34 | ## Running on a local Python console 35 | 36 | Steps to run the trading model on your command line: 37 | 38 | - Within a Python 3.7 environment, install the requirements: 39 | 40 | pip install -r requirements.txt 41 | 42 | - In IB Trader Workstation (TWS), go to **Configuration** > **Api** > **Settings** and: 43 | 44 | - enable ActiveX and Socket Clients 45 | - check the port number you will be using 46 | - If using Docker, uncheck **Allow connections from localhost only** and enter the machine IP running this model to **Trusted IPs**. 47 | 48 | - Update `main.py` with the required parameters and run the model with the command: 49 | 50 | python main.py 51 | 52 | ## Running from a Docker container 53 | 54 | This step is optional. You can choose to deploy one or several instances of these algos on a remote machine for execution using Docker. 55 | 56 | A Docker container helps to automatically build your running environment and isolate changes, all in just a few simple commands! 57 | 58 | To run this trading model in headless mode: 59 | 60 | - In TWS, ensure that remote API connections are accepted and the Docker machine's IP is added to **Trusted IPs**. 61 | 62 | - Ensure your machine has docker and docker-compose installed. Build the image with this command: 63 | 64 | docker-compose build 65 | 66 | - Update the parameters in `docker-compose.yml`. I've set the `TWS_HOST` value in my environment variables. This is the IP address of the remote machine running TWS. Or, you can just manually enter the IP address value directly. Then, run the image as a container instance: 67 | 68 | docker-compose up 69 | 70 | To run in headless mode, simply add the detached command `-d`, like this: 71 | 72 | docker-compose up -d 73 | 74 | In headless mode, you would have to start and stop the containers manually. 75 | 76 | Key concepts 77 | === 78 | At the present moment, this model utilizes statistical arbitrage incorporating these methodologies: 79 | - Bootstrapping the model with historical data to derive usable strategy parameters 80 | - Resampling inhomogeneous time series to homogeneous time series 81 | - Selection of highly-correlated tradable pair 82 | - The ability to short one instrument and long the other. 83 | - Using volatility ratio to detect up or down trend. 84 | - Fair valuation of security using beta, or the mean over some past interval. 85 | - One pandas DataFrame to store historical prices 86 | 87 | Other functions: 88 | - Generate trade signals and place buy/sell market orders based on every incoming tick data. 89 | - Re-evaluating beta every some interval in seconds. 90 | 91 | And greatly inspired by these papers: 92 | - MIT - Developing high-frequency equities trading model 93 | @ http://dspace.mit.edu/handle/1721.1/59122 94 | - SMU - Profiting from mean-reverting yield-curve trading strategies 95 | @ http://ink.library.smu.edu.sg/cgi/viewcontent.cgi?article=3488&context=lkcsb_research 96 | 97 | And book: 98 | - Introduction to High-Frequency Finance 99 | @ http://www.amazon.com/Introduction-High-Frequency-Finance-Ramazan-Gen%C3%A7ay/dp/0122796713 100 | 101 | Step-by-step guide to more trading models 102 | === 103 | 104 | Mastering Python for Finance - Second Edition 105 | 106 | I published a book titled 'Mastering Python for Finance - Second Edition', discussing additional algorithmic trading ideas, statistical analysis, machine learning and deep learning, which you might find it useful. 107 | It is available on major sales channels including Amazon, Safari Online and Barnes & Noble, 108 | in paperback, Kindle and ebook. 109 | Get it from: 110 | - https://www.amazon.com/dp/1789346460 111 | 112 | Source codes and table of contents on GitHub: 113 | - https://github.com/jamesmawm/mastering-python-for-finance-second-edition 114 | 115 | Topics covered with source codes: 116 | 117 | - Applying kernel PCA. Forecasting and predicting a time series. 118 | - Replicating the VIX index 119 | - Building a mean-reverting and trend-following trading model 120 | - Implementing a backtesting system 121 | - Predicting returns with a cross-asset momentum machine learning model 122 | - Credit card payment default prediction with Keras. Get started in deep learning with TensorFlow for predicting prices. 123 | 124 | If you would like a **FREE** review copy, drop me an email at jamesmawm@gmail.com. 125 | 126 | Suggested enhancements 127 | === 128 | Some ideas that you can extend this model for better results: 129 | 130 | - Extending to more than 2 securities and trade on optimum prices 131 | - Generate trade signals based on correlation and co-integration 132 | - Using PCA for next-period evaluation. In my book I've described the use of PCA to reconstruct the DOW index. Source codes here. 133 | - Include vector auto-regressions 134 | - Account for regime shifts (trending or mean-reverting states) 135 | - Account for structural breaks 136 | - Using EMA kernels instead of a rectangular one 137 | - Add in alphas(P/E, B/P ratios) and Kalman filter prediction 138 | 139 | Disclaimer 140 | === 141 | - Any securities listed is not a solicitation to trade. 142 | - This model has not been proven to be profitable in a live account. 143 | - I am not liable for any outcome of your trades. 144 | 145 | 146 | Is this HFT? 147 | === 148 | Sure, I had some questions "how is this high-frequency" or "not for UHFT" or "this is not front-running". Let's take a closer look at these definitions: 149 | - High-frequency finance: the studying of incoming tick data arriving at high frequencies, 150 | say hundreds of ticks per second. High frequency finance aims to derive stylized facts from high frequency signals. 151 | - High-frequency trading: the turnover of positions at high frequencies; 152 | positions are typically held at most in seconds, which amounts to hundreds of trades per second. 153 | 154 | This models aims to incorporate the above two functions and present a simplistic view to traders who wish to automate their trades, get started in Python trading or use a free trading platform. 155 | 156 | Other software of interest 157 | === 158 | I write software in my free time. One of them for trading futures was simply called 'The Gateway'. 159 | It is a C# application that exposes a socket and public API method calls for interfacing Python with futures markets including CME, 160 | CBOT, NYSE, Eurex and ICE. Targets the T4 API. 161 | 162 | More information on GitHub: https://github.com/hftstrat/The-Gateway-code-samples or view on the website. 163 | 164 | 165 | Final notes 166 | ======================== 167 | - I haven't come across any complete high-frequency trading model lying around, so here's one to get started off the ground and running. 168 | - This model has never been used with a real account. All testing was done in demo account only. 169 | - The included strategy parameters are theoretical ideal conditions, which have not been adjusted for back-tested results. 170 | - This project is still a work in progress. A good model could take months or even years! 171 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from ib_insync import Forex, Stock 4 | 5 | from models.hft_model_1 import HftModel1 6 | 7 | if __name__ == '__main__': 8 | TWS_HOST = os.environ.get('TWS_HOST', '127.0.0.1') 9 | TWS_PORT = os.environ.get('TWS_PORT', 4002) 10 | 11 | print('Connecting on host:', TWS_HOST, 'port:', TWS_PORT) 12 | 13 | model = HftModel1( 14 | host=TWS_HOST, 15 | port=TWS_PORT, 16 | client_id=1, 17 | ) 18 | 19 | to_trade = [ 20 | ('SPY', Stock('SPY','SMART','USD')), 21 | ('QQQ', Stock('QQQ','SMART','USD')), 22 | ] 23 | 24 | # to_trade = [ 25 | # Stock('QQQ', 'SMART', 'USD'), 26 | # Stock('SPY', 'SMART', 'USD') 27 | # ] 28 | 29 | model.run(to_trade=to_trade, trade_qty=100) 30 | -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chicago-joe/InteractiveBrokers-PairsTrading-Algo/503778577554b3a006991cdff82cabbd83ed42b8/models/__init__.py -------------------------------------------------------------------------------- /models/base_model.py: -------------------------------------------------------------------------------- 1 | from ib_insync import IB, Forex, Stock, MarketOrder 2 | 3 | from util import order_util 4 | 5 | """ 6 | A base model containing common IB functions. 7 | 8 | For other models to extend and use. 9 | """ 10 | 11 | 12 | class BaseModel(object): 13 | def __init__(self, host='127.0.0.1', port=4002, client_id=1): 14 | self.host = host 15 | self.port = port 16 | self.client_id = client_id 17 | 18 | self.__ib = None 19 | self.pnl = None # stores IB PnL object 20 | self.positions = {} # stores IB Position object by symbol 21 | 22 | self.symbol_map = {} # maps contract to symbol 23 | self.symbols, self.contracts = [], [] 24 | 25 | def init_model(self, to_trade): 26 | """ 27 | Initialize the model given inputs before running. 28 | Stores the input symbols and contracts that will be used for reading positions. 29 | 30 | :param to_trade: list of a tuple of symbol and contract, Example: 31 | [('EURUSD', Forex('EURUSD'), ] 32 | """ 33 | self.symbol_map = {str(contract): ident for (ident, contract) in to_trade} 34 | self.contracts = [contract for (_, contract) in to_trade] 35 | self.symbols = list(self.symbol_map.values()) 36 | 37 | def connect_to_ib(self): 38 | self.ib.connect(self.host, self.port, clientId=self.client_id) 39 | 40 | def request_pnl_updates(self): 41 | account = self.ib.managedAccounts()[0] 42 | self.ib.reqPnL(account) 43 | self.ib.pnlEvent += self.on_pnl 44 | 45 | def on_pnl(self, pnl): 46 | """ Simply store a copy of the latest PnL whenever where are changes """ 47 | self.pnl = pnl 48 | 49 | def request_position_updates(self): 50 | self.ib.reqPositions() 51 | self.ib.positionEvent += self.on_position 52 | 53 | def on_position(self, position): 54 | """ Simply store a copy of the latest Position object for the provided contract """ 55 | symbol = self.get_symbol(position.contract) 56 | if symbol not in self.symbols: 57 | print('[warn]symbol not found for position:', position) 58 | return 59 | 60 | self.positions[symbol] = position 61 | 62 | def request_all_contracts_data(self, fn_on_tick): 63 | for contract in self.contracts: 64 | self.ib.reqMktData(contract,) 65 | 66 | self.ib.pendingTickersEvent += fn_on_tick 67 | 68 | def place_market_order(self, contract, qty, fn_on_filled): 69 | order = MarketOrder(order_util.get_order_action(qty), abs(qty)) 70 | trade = self.ib.placeOrder(contract, order) 71 | trade.filledEvent += fn_on_filled 72 | return trade 73 | 74 | def get_symbol(self, contract): 75 | """ 76 | Finds the symbol given the contract. 77 | 78 | :param contract: The Contract object 79 | :return: the symbol given for the specific contract 80 | """ 81 | symbol = self.symbol_map.get(str(contract), None) 82 | if symbol: 83 | return symbol 84 | 85 | symbol = '' 86 | if type(contract) is Forex: 87 | symbol = contract.localSymbol.replace('.', '') 88 | elif type(contract) is Stock: 89 | symbol = contract.symbol 90 | 91 | return symbol if symbol in self.symbols else '' 92 | 93 | @property 94 | def ib(self): 95 | if not self.__ib: 96 | self.__ib = IB() 97 | 98 | return self.__ib 99 | -------------------------------------------------------------------------------- /models/hft_model_1.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import time 3 | 4 | import pandas as pd 5 | 6 | from models.base_model import BaseModel 7 | from util import dt_util 8 | 9 | """ 10 | This is a simple high-frequency model that processes incoming market data at tick level. 11 | 12 | Statistical calculations involved: 13 | - beta: the mean prices of A over B 14 | - volatility ratio: the standard deviation of pct changes of A over B 15 | 16 | The signals are then calculated based on these stats: 17 | - whether it is a downtrend or uptrend 18 | - whether the expected price given from the beta is overbought or oversold 19 | 20 | This model takes a mean-reverting approach: 21 | - On a BUY signal indicating oversold and uptrend, we take a LONG position. 22 | Then close the LONG position on a SELL signal. 23 | - Conversely, on a SELL signal, we take a SHORT position and closeout on a BUY signal. 24 | """ 25 | 26 | 27 | class HftModel1(BaseModel): 28 | def __init__(self, *args, **kwargs): 29 | super().__init__(*args, **kwargs) 30 | 31 | self.df_hist = None # stores mid prices in a pandas DataFrame 32 | 33 | self.pending_order_ids = set() 34 | self.is_orders_pending = False 35 | 36 | # Input params 37 | self.trade_qty = 0 38 | 39 | # Strategy params 40 | self.volatility_ratio = 1 41 | self.beta = 0 42 | self.moving_window_period = dt.timedelta(minutes=1) 43 | self.is_buy_signal, self.is_sell_signal = False, False 44 | 45 | def run(self, to_trade=[], trade_qty=0): 46 | """ Entry point """ 47 | 48 | print('[{time}]started'.format( 49 | time=str(pd.to_datetime('now')), 50 | )) 51 | 52 | # Initialize model based on inputs 53 | self.init_model(to_trade) 54 | self.trade_qty = trade_qty 55 | self.df_hist = pd.DataFrame(columns=self.symbols) 56 | 57 | # Establish connection to IB 58 | self.connect_to_ib() 59 | self.request_pnl_updates() 60 | self.request_position_updates() 61 | self.request_historical_data() 62 | self.request_all_contracts_data(self.on_tick) 63 | 64 | # Recalculate and/or print account updates at intervals 65 | while self.ib.waitOnUpdate(): 66 | self.ib.sleep(1) 67 | self.recalculate_strategy_params() 68 | 69 | if not self.is_position_flat: 70 | self.print_account() 71 | 72 | def on_tick(self, tickers): 73 | """ When a tick data is received, store it and make calculations out of it """ 74 | for ticker in tickers: 75 | self.get_incoming_tick_data(ticker) 76 | 77 | self.perform_trade_logic() 78 | 79 | def perform_trade_logic(self): 80 | """ 81 | This part is the 'secret-sauce' where actual trades takes place. 82 | My take is that great experience, good portfolio construction, 83 | and together with robust backtesting will make your strategy viable. 84 | GOOD PORTFOLIO CONSTRUCTION CAN SAVE YOU FROM BAD RESEARCH, 85 | BUT BAD PORTFOLIO CONSTRUCTION CANNOT SAVE YOU FROM GREAT RESEARCH 86 | 87 | This trade logic uses volatility ratio and beta as our indicators. 88 | - volatility ratio > 1 :: uptrend, volatility ratio < 1 :: downtrend 89 | - beta is calculated as: mean(price A) / mean(price B) 90 | 91 | We use the assumption that price levels will mean-revert. 92 | Expected price A = beta x price B 93 | """ 94 | self.calculate_signals() 95 | 96 | if self.is_orders_pending or self.check_and_enter_orders(): 97 | return # Do nothing while waiting for orders to be filled 98 | 99 | if self.is_position_flat: 100 | self.print_strategy_params() 101 | 102 | def print_account(self): 103 | [symbol_a, symbol_b] = self.symbols 104 | position_a, position_b = self.positions.get(symbol_a), self.positions.get(symbol_b) 105 | 106 | print('[{time}][account]{symbol_a} pos={pos_a} avgPrice={avg_price_a}|' 107 | '{symbol_b} pos={pos_b}|rpnl={rpnl:.2f} upnl={upnl:.2f}|beta:{beta:.2f} volatility:{vr:.2f}'.format( 108 | time=str(pd.to_datetime('now')), 109 | symbol_a=symbol_a, 110 | pos_a=position_a.position if position_a else 0, 111 | avg_price_a=position_a.avgCost if position_a else 0, 112 | symbol_b=symbol_b, 113 | pos_b=position_b.position if position_b else 0, 114 | avg_price_b=position_b.avgCost if position_b else 0, 115 | rpnl=self.pnl.realizedPnL, 116 | upnl=self.pnl.unrealizedPnL, 117 | beta=self.beta, 118 | vr=self.volatility_ratio, 119 | )) 120 | 121 | def print_strategy_params(self): 122 | print('[{time}][strategy params]beta:{beta:.2f} volatility:{vr:.2f}|rpnl={rpnl:.2f}'.format( 123 | time=str(pd.to_datetime('now')), 124 | beta=self.beta, 125 | vr=self.volatility_ratio, 126 | rpnl=self.pnl.realizedPnL, 127 | )) 128 | 129 | def check_and_enter_orders(self): 130 | if self.is_position_flat and self.is_sell_signal: 131 | print('*** OPENING SHORT POSITION ***') 132 | self.place_spread_order(-self.trade_qty) 133 | return True 134 | 135 | if self.is_position_flat and self.is_buy_signal: 136 | print('*** OPENING LONG POSITION ***') 137 | self.place_spread_order(self.trade_qty) 138 | return True 139 | 140 | if self.is_position_short and self.is_buy_signal: 141 | print('*** CLOSING SHORT POSITION ***') 142 | self.place_spread_order(self.trade_qty) 143 | return True 144 | 145 | if self.is_position_long and self.is_sell_signal: 146 | print('*** CLOSING LONG POSITION ***') 147 | self.place_spread_order(-self.trade_qty) 148 | return True 149 | 150 | return False 151 | 152 | def place_spread_order(self, qty): 153 | print('Placing spread orders...') 154 | 155 | [contract_a, contract_b] = self.contracts 156 | 157 | trade_a = self.place_market_order(contract_a, qty, self.on_filled) 158 | print('Order placed:', trade_a) 159 | 160 | trade_b = self.place_market_order(contract_b, -qty, self.on_filled) 161 | print('Order placed:', trade_b) 162 | 163 | self.is_orders_pending = True 164 | 165 | self.pending_order_ids.add(trade_a.order.orderId) 166 | self.pending_order_ids.add(trade_b.order.orderId) 167 | print('Order IDs pending execution:', self.pending_order_ids) 168 | 169 | def on_filled(self, trade): 170 | print('Order filled:', trade) 171 | self.pending_order_ids.remove(trade.order.orderId) 172 | print('Order IDs pending execution:', self.pending_order_ids) 173 | 174 | # Update flag when all pending orders are filled 175 | if not self.pending_order_ids: 176 | self.is_orders_pending = False 177 | 178 | def recalculate_strategy_params(self): 179 | """ Calculating beta and volatility ratio for our signal indicators """ 180 | [symbol_a, symbol_b] = self.symbols 181 | 182 | resampled = self.df_hist.resample('30s').ffill().dropna() 183 | mean = resampled.mean() 184 | self.beta = mean[symbol_a] / mean[symbol_b] 185 | 186 | stddevs = resampled.pct_change().dropna().std() 187 | self.volatility_ratio = stddevs[symbol_a] / stddevs[symbol_b] 188 | 189 | def calculate_signals(self): 190 | self.trim_historical_data() 191 | 192 | is_up_trend, is_down_trend = self.volatility_ratio > 1, self.volatility_ratio < 1 193 | is_overbought, is_oversold = self.is_overbought_or_oversold() 194 | 195 | # Our final trade signals 196 | self.is_buy_signal = is_up_trend and is_oversold 197 | self.is_sell_signal = is_down_trend and is_overbought 198 | 199 | def trim_historical_data(self): 200 | """ Ensure historical data don't grow beyond a certain size """ 201 | cutoff_time = dt.datetime.now(tz=dt_util.LOCAL_TIMEZONE) - self.moving_window_period 202 | self.df_hist = self.df_hist[self.df_hist.index >= cutoff_time] 203 | 204 | def is_overbought_or_oversold(self): 205 | [symbol_a, symbol_b] = self.symbols 206 | last_price_a = self.df_hist[symbol_a].dropna().values[-1] 207 | last_price_b = self.df_hist[symbol_b].dropna().values[-1] 208 | 209 | expected_last_price_a = last_price_b * self.beta 210 | 211 | is_overbought = last_price_a < expected_last_price_a # Cheaper than expected 212 | is_oversold = last_price_a > expected_last_price_a # Higher than expected 213 | 214 | return is_overbought, is_oversold 215 | 216 | def get_incoming_tick_data(self, ticker): 217 | """ 218 | Stores the midpoint of incoming price data to a pandas DataFrame `df_hist`. 219 | 220 | :param ticker: The incoming tick data as a Ticker object. 221 | """ 222 | symbol = self.get_symbol(ticker.contract) 223 | 224 | dt_obj = dt_util.convert_utc_datetime(ticker.time) 225 | bid = ticker.bid 226 | ask = ticker.ask 227 | mid = (bid + ask) / 2 228 | 229 | self.df_hist.loc[dt_obj, symbol] = mid 230 | 231 | def request_historical_data(self): 232 | """ 233 | Bootstrap our model by downloading historical data for each contract. 234 | 235 | The midpoint of prices are stored in the pandas DataFrame `df_hist`. 236 | """ 237 | for contract in self.contracts: 238 | self.set_historical_data(contract) 239 | 240 | def set_historical_data(self, contract): 241 | symbol = self.get_symbol(contract) 242 | 243 | bars = self.ib.reqHistoricalData( 244 | contract, 245 | endDateTime=time.strftime('%Y%m%d %H:%M:%S'), 246 | durationStr='3600 S', 247 | barSizeSetting='5 secs', 248 | whatToShow='MIDPOINT', 249 | useRTH=True, 250 | formatDate=1 251 | ) 252 | for bar in bars: 253 | dt_obj = dt_util.convert_local_datetime(bar.date) 254 | self.df_hist.loc[dt_obj, symbol] = bar.close 255 | 256 | @property 257 | def is_position_flat(self): 258 | position_obj = self.positions.get(self.symbols[0]) 259 | if not position_obj: 260 | return True 261 | 262 | return position_obj.position == 0 263 | 264 | @property 265 | def is_position_short(self): 266 | position_obj = self.positions.get(self.symbols[0]) 267 | return position_obj and position_obj.position < 0 268 | 269 | @property 270 | def is_position_long(self): 271 | position_obj = self.positions.get(self.symbols[0]) 272 | return position_obj and position_obj.position > 0 273 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | eventkit==0.8.5 2 | ib-insync==0.9.53 3 | nest-asyncio==1.0.0 4 | numpy==1.16.4 5 | pandas==0.24.2 6 | python-dateutil==2.8.0 7 | pytz==2019.1 8 | six==1.12.0 9 | -------------------------------------------------------------------------------- /sample_output/run_01_screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chicago-joe/InteractiveBrokers-PairsTrading-Algo/503778577554b3a006991cdff82cabbd83ed42b8/sample_output/run_01_screenshot_1.png -------------------------------------------------------------------------------- /sample_output/run_01_screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chicago-joe/InteractiveBrokers-PairsTrading-Algo/503778577554b3a006991cdff82cabbd83ed42b8/sample_output/run_01_screenshot_2.png -------------------------------------------------------------------------------- /sample_output/run_02_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chicago-joe/InteractiveBrokers-PairsTrading-Algo/503778577554b3a006991cdff82cabbd83ed42b8/sample_output/run_02_screenshot.png -------------------------------------------------------------------------------- /util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chicago-joe/InteractiveBrokers-PairsTrading-Algo/503778577554b3a006991cdff82cabbd83ed42b8/util/__init__.py -------------------------------------------------------------------------------- /util/dt_util.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from dateutil import tz 3 | 4 | UTC_TIMEZONE = tz.tzutc() 5 | LOCAL_TIMEZONE = tz.tzlocal() 6 | 7 | 8 | def convert_utc_datetime(datetime): 9 | utc = datetime.replace(tzinfo=UTC_TIMEZONE) 10 | local_time = utc.astimezone(LOCAL_TIMEZONE) 11 | return pd.to_datetime(local_time) 12 | 13 | 14 | def convert_local_datetime(datetime): 15 | local_time = datetime.replace(tzinfo=LOCAL_TIMEZONE) 16 | return pd.to_datetime(local_time) 17 | -------------------------------------------------------------------------------- /util/order_util.py: -------------------------------------------------------------------------------- 1 | def get_order_action(qty): 2 | return 'BUY' if qty >= 0 else 'SELL' 3 | --------------------------------------------------------------------------------