├── .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 |
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 |
--------------------------------------------------------------------------------