Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ib integration #186

Open
wants to merge 37 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
abc3e86
Started an IB PriceHandler and a test case.
ryankennedyio Feb 18, 2017
55df528
Start working on IBService; the master/background/network communicati…
ryankennedyio Feb 25, 2017
63e1710
Remove prebuilt hist data request
ryankennedyio Feb 25, 2017
e673185
Rename references to EWrapper/EClient
ryankennedyio Feb 25, 2017
c46d051
Added stub test, partially implement historic prices and ib_bar price…
ryankennedyio Feb 25, 2017
ffe2938
Remove print statements
ryankennedyio Feb 26, 2017
deaf6b4
IBPriceHandler should now work for at least one historic request
ryankennedyio Feb 26, 2017
7bc7f54
remove debug statement
ryankennedyio Feb 26, 2017
84a1aa6
IBService implements threading interface to run the infinite message …
ryankennedyio Mar 7, 2017
433868d
Merge branch 'master' of github.com:mhallsmoore/qstrader into ib_inte…
ryankennedyio Mar 7, 2017
9519bc0
Moved IBService creation, setup and stop outside of the price handler
ryankennedyio Mar 8, 2017
749e13c
Ensure tradelog is created if it does not exist
ryankennedyio Mar 8, 2017
fec3815
Fix some strange kind of systemerror that appeared using Python 3.5
ryankennedyio Mar 8, 2017
2a2fb4b
Add a few notes
ryankennedyio Mar 8, 2017
657b016
Add a fwe notes
ryankennedyio Mar 8, 2017
c806a64
Add some instructions for usage to the IBService
ryankennedyio Mar 8, 2017
960cf79
Added an example IB Historic backtest
ryankennedyio Mar 8, 2017
e38a59a
The IBPriceHandler must take in a list of symbols for subscription, r…
ryankennedyio Mar 8, 2017
f0a8fc3
Merge branch 'master' into ib_integration
ryankennedyio Mar 11, 2017
b6417f2
Started adding more test cases. Mocking IBService.
ryankennedyio Mar 11, 2017
39734f0
Fix bug in IB Price Handler's timestamps, create a whole mock object …
ryankennedyio Mar 11, 2017
91165fa
Ensure all historical requests are made
ryankennedyio Mar 11, 2017
5a27a3a
More intensive testing on streaming historic prices; fixed bug where …
ryankennedyio Mar 11, 2017
922ffd0
Remove debugging
ryankennedyio Mar 11, 2017
2a6b36a
Remote two test cases that were covered by another test
ryankennedyio Mar 11, 2017
6d9c114
Move some test cases to TODO's because I'm not yet sure the best way …
ryankennedyio Mar 11, 2017
60bfa5b
Linting
ryankennedyio Mar 11, 2017
eba9654
Fix queue compatibility import
ryankennedyio Mar 11, 2017
fe5f5df
Update a few references. Test cases don't cover live IB unfrotunately…
ryankennedyio Mar 12, 2017
393ae63
Started some test cases for live market data requests
ryankennedyio Mar 14, 2017
c05e93a
Live pricehandler should request properly and stream bars when popual…
ryankennedyio Mar 14, 2017
efbf3a5
Live market data working with example
ryankennedyio Mar 14, 2017
1fd72aa
Add displayStrategy for live trading example to see market data comin…
ryankennedyio Mar 15, 2017
8c4f822
Linting
ryankennedyio Mar 15, 2017
8c52055
Try to install ibapi python module manually in Travis build
ryankennedyio Mar 15, 2017
55d62d0
Modifying travis build
ryankennedyio Mar 15, 2017
bc3e347
Modifying travis build
ryankennedyio Mar 15, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions qstrader/price_handler/ib_bar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import os
import datetime
import queue
from .base import AbstractBarPriceHandler

from qstrader.service.ib import IBService

from ibapi.client import EClient
from ibapi.wrapper import EWrapper
from ibapi.utils import iswrapper
from ibapi.contract import *
from ibapi.common import *



#types
from ibapi.utils import (current_fn_name, BadMessage)
from ibapi.common import *
from ibapi.order_condition import *
from ibapi.contract import *
from ibapi.order import *
from ibapi.order_state import *
from ibapi.execution import Execution
from ibapi.execution import ExecutionFilter
from ibapi.commission_report import CommissionReport
from ibapi.scanner import ScannerSubscription
from ibapi.ticktype import *

from ibapi.account_summary_tags import *


class IBBarPriceHandler(AbstractBarPriceHandler):
"""
Designed to feed either live or historic market data bars
from an Interactive Brokers connection.

Uses the IBService to make requests and collect data once responses have returned.

TODO:
* Historic/Live mode to be set by whether QSTrader is in Backtest or Live mode
* IBService should be an initialization parameter
* Ports, etc, connection strings from config
"""
def __init__(
self, events_queue, param_tickers, settings, mode="historic",
hist_end_date = datetime.datetime.now() - datetime.timedelta(days=3),
hist_duration="5 D", hist_barsize="1 min"
):
self.ib_service = IBService()
self.ib_service.connect("127.0.0.1",4001,0)
self.mode = mode
self.continue_backtest = True
self.hist_end_date = hist_end_date
self.hist_duration = hist_duration
self.hist_barsize = hist_barsize

# The position of a ticker in this dict is used as its IB ID.
self.tickers = {} # TODO gross
self.ticker_lookup = {}

for ticker in param_tickers: # TODO gross param_tickers -- combine above?
self._subscribe_ticker(ticker)

self._wait_for_hist_population()


def _subscribe_ticker(self, ticker):
"""
Request ticker data from IB
"""
if ticker not in self.tickers:
# Set up an IB ContractS
contract = Contract()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When previously trying to write a service to store historical options data I found it useful to pass a custom object with a unique ID and the contract object.
When you submit a type request for futures or options on the root symbol IB will return a stream of contract objects. If the functions to subscribe and request can accept the connection objects or a data structure containing the objects it saves you from having to pull out the relevant information required to recreate the objects later.

contract.exchange = "SMART"
contract.symbol = ticker
contract.secType = "STK"
contract.currency = "AUD"

if self.mode == "historic":
ib_ticker_id = len(self.tickers)
end_time = datetime.datetime.strftime(self.hist_end_date, "%Y%m%d 17:00:00")
self.ib_service.reqHistoricalData(
ib_ticker_id, contract, end_time, self.hist_duration, self.hist_barsize,
"TRADES", True, 2, None)

# TODO gross
self.ticker_lookup[len(self.tickers)] = ticker
self.tickers[ticker] = {}


def _wait_for_hist_population(self):
"""
# TODO this *needs* to be an infinite loop running inside the service,
# with the service launched on a new Thread or Process
"""
while (self.ib_service.conn.isConnected() or not self.ib_service.msg_queue.empty()) and len(self.ib_service.waitingHistoricalData) != 0:
try:
text = self.ib_service.msg_queue.get(block=True, timeout=0.2)
if len(text) > MAX_MSG_LEN:
self.ib_service.wrapper.error(NO_VALID_ID, BAD_LENGTH.code(),
"%s:%d:%s" % (BAD_LENGTH.msg(), len(text), text))
self.ib_service.disconnect()
break
except queue.Empty:
print("queue.get: empty")
else:
fields = comm.read_fields(text)
print("fields %s", fields)
self.ib_service.decoder.interpret(fields)

print("conn:%d queue.sz:%d",
self.ib_service.conn.isConnected(),
self.ib_service.msg_queue.qsize())

self.ib_service.disconnect()
92 changes: 92 additions & 0 deletions qstrader/service/ib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import sys
import argparse
import datetime
import collections
import inspect
import queue

import logging
import time
import os.path

from ibapi.wrapper import EWrapper
from ibapi.client import EClient
from ibapi.utils import iswrapper

#types
from ibapi.utils import (current_fn_name, BadMessage)
from ibapi.common import *
from ibapi.order_condition import *
from ibapi.contract import *
from ibapi.order import *
from ibapi.order_state import *
from ibapi.execution import Execution
from ibapi.execution import ExecutionFilter
from ibapi.commission_report import CommissionReport
from ibapi.scanner import ScannerSubscription
from ibapi.ticktype import *

from ibapi.account_summary_tags import *


class IBService(EWrapper, EClient):
"""
The IBService is the primary conduit of data from QStrader to Interactive Brokers.
This service provides functions to request data, and allows for
callbacks to be triggered, which populates "data queues" with the response.

All methods of the EClient are available (i.e. API Requests), as are
the callbacks for EWrapper (i.e. API responses). It also provides a set of Queues
which are populated with the responses from EWrapper. Other components in the
system should use these queues collect the API response data.

Any module or component that wishes to interact with IB should do so by using
methods offered in this class. This ensures that the logic required to talk with IB
is contained within this class exclusively, with the added benefit that we
can easily create mock instances of the IBService for testing.
"""
def __init__(self):
EWrapper.__init__(self)
EClient.__init__(self, wrapper=self)

self.historicalDataQueue = queue.Queue()
self.waitingHistoricalData = []


def error(self, reqId:TickerId, errorCode:int, errorString:str):
super().error(reqId, errorCode, errorString)
print("Error. Id: " , reqId, " Code: " , errorCode , " Msg: " , errorString)


"""
Append `reqId` to waitingHistoricalData, then call the super method.
"""
def reqHistoricalData(self, reqId:TickerId , contract:Contract, endDateTime:str,
durationStr:str, barSizeSetting:str, whatToShow:str,
useRTH:int, formatDate:int, chartOptions:TagValueList):
self.waitingHistoricalData.append(reqId)
print("REQUESTING HISTORIC, WAITING FOR %s" % len(self.waitingHistoricalData))
super().reqHistoricalData( reqId, contract, endDateTime,
durationStr, barSizeSetting, whatToShow,
useRTH, formatDate, chartOptions)


"""
Populate the HistoricalData queue.
"""
def historicalData(self, reqId:TickerId , date:str, open:float, high:float,
low:float, close:float, volume:int, barCount:int,
WAP:float, hasGaps:int):
print("HistoricalData. ", reqId, " Date:", date, "Open:", open,
"High:", high, "Low:", low, "Close:", close, "Volume:", volume,
"Count:", barCount, "WAP:", WAP, "HasGaps:", hasGaps)
self.historicalDataQueue.put((reqId, date, open, high, low, close,
volume, barCount, WAP, hasGaps))

"""
Remove `reqId` from waitingHistoricalData
TODO: Will it work with multiple historical requests for same symbol?
"""
def historicalDataEnd(self, reqId:int, start:str, end:str):
print("FINISHED FOR %s" % reqId)
self.waitingHistoricalData.remove(reqId)
27 changes: 27 additions & 0 deletions tests/test_ib_price_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import unittest

from qstrader.price_handler.ib_bar import IBBarPriceHandler
from qstrader.compat import queue
from qstrader import settings

class TestPriceHandlerSimpleCase(unittest.TestCase):
def setUp(self):
"""
Set up the PriceHandler object with a small
set of initial tickers for a backtest in historic mode.
"""
self.config = settings.TEST
fixtures_path = self.config.CSV_DATA_DIR
events_queue = queue.Queue()
init_tickers = ["CBA"]
self.price_handler = IBBarPriceHandler(
events_queue, init_tickers, self.config
)

def test(self):
self.assertEqual(1,2)



if __name__ == "__main__":
unittest.main()